Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,256 @@
Advanced field value conversion using custom mapping types
==========================================================
.. sectionauthor:: Jan Sorgalla <jsorgalla@googlemail.com>
When creating entities, you sometimes have the need to transform field values
before they are saved to the database. In Doctrine you can use Custom Mapping
Types to solve this (see: :ref:`reference-basic-mapping-custom-mapping-types`).
There are several ways to achieve this: converting the value inside the Type
class, converting the value on the database-level or a combination of both.
This article describes the third way by implementing the MySQL specific column
type `Point <https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html>`_.
The ``Point`` type is part of the `Spatial extension <https://dev.mysql.com/doc/refman/8.0/en/spatial-extensions.html>`_
of MySQL and enables you to store a single location in a coordinate space by
using x and y coordinates. You can use the Point type to store a
longitude/latitude pair to represent a geographic location.
The entity
----------
We create a simple entity with a field ``$point`` which holds a value object
``Point`` representing the latitude and longitude of the position.
The entity class:
.. code-block:: php
<?php
namespace Geo\Entity;
/**
* @Entity
*/
class Location
{
/**
* @Column(type="point")
*
* @var \Geo\ValueObject\Point
*/
private $point;
/**
* @Column(type="string")
*
* @var string
*/
private $address;
/**
* @param \Geo\ValueObject\Point $point
*/
public function setPoint(\Geo\ValueObject\Point $point)
{
$this->point = $point;
}
/**
* @return \Geo\ValueObject\Point
*/
public function getPoint()
{
return $this->point;
}
/**
* @param string $address
*/
public function setAddress($address)
{
$this->address = $address;
}
/**
* @return string
*/
public function getAddress()
{
return $this->address;
}
}
We use the custom type ``point`` in the ``@Column`` docblock annotation of the
``$point`` field. We will create this custom mapping type in the next chapter.
The point class:
.. code-block:: php
<?php
namespace Geo\ValueObject;
class Point
{
/**
* @param float $latitude
* @param float $longitude
*/
public function __construct($latitude, $longitude)
{
$this->latitude = $latitude;
$this->longitude = $longitude;
}
/**
* @return float
*/
public function getLatitude()
{
return $this->latitude;
}
/**
* @return float
*/
public function getLongitude()
{
return $this->longitude;
}
}
The mapping type
----------------
Now we're going to create the ``point`` type and implement all required methods.
.. code-block:: php
<?php
namespace Geo\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Geo\ValueObject\Point;
class PointType extends Type
{
const POINT = 'point';
public function getName()
{
return self::POINT;
}
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'POINT';
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
list($longitude, $latitude) = sscanf($value, 'POINT(%f %f)');
return new Point($latitude, $longitude);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof Point) {
$value = sprintf('POINT(%F %F)', $value->getLongitude(), $value->getLatitude());
}
return $value;
}
public function canRequireSQLConversion()
{
return true;
}
public function convertToPHPValueSQL($sqlExpr, AbstractPlatform $platform)
{
return sprintf('AsText(%s)', $sqlExpr);
}
public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
{
return sprintf('PointFromText(%s)', $sqlExpr);
}
}
We do a 2-step conversion here. In the first step, we convert the ``Point``
object into a string representation before saving to the database (in the
``convertToDatabaseValue`` method) and back into an object after fetching the
value from the database (in the ``convertToPHPValue`` method).
The format of the string representation format is called
`Well-known text (WKT) <http://en.wikipedia.org/wiki/Well-known_text>`_.
The advantage of this format is, that it is both human readable and parsable by MySQL.
Internally, MySQL stores geometry values in a binary format that is not
identical to the WKT format. So, we need to let MySQL transform the WKT
representation into its internal format.
This is where the ``convertToPHPValueSQL`` and ``convertToDatabaseValueSQL``
methods come into play.
This methods wrap a sql expression (the WKT representation of the Point) into
MySQL functions `ST_PointFromText <https://dev.mysql.com/doc/refman/8.0/en/gis-wkt-functions.html#function_st-pointfromtext>`_
and `ST_AsText <https://dev.mysql.com/doc/refman/8.0/en/gis-format-conversion-functions.html#function_st-astext>`_
which convert WKT strings to and from the internal format of MySQL.
.. note::
When using DQL queries, the ``convertToPHPValueSQL`` and
``convertToDatabaseValueSQL`` methods only apply to identification variables
and path expressions in SELECT clauses. Expressions in WHERE clauses are
**not** wrapped!
If you want to use Point values in WHERE clauses, you have to implement a
:doc:`user defined function <dql-user-defined-functions>` for
``PointFromText``.
Example usage
-------------
.. code-block:: php
<?php
// Bootstrapping stuff...
// $em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);
// Setup custom mapping type
use Doctrine\DBAL\Types\Type;
Type::addType('point', 'Geo\Types\PointType');
$em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('point', 'point');
// Store a Location object
use Geo\Entity\Location;
use Geo\ValueObject\Point;
$location = new Location();
$location->setAddress('1600 Amphitheatre Parkway, Mountain View, CA');
$location->setPoint(new Point(37.4220761, -122.0845187));
$em->persist($location);
$em->flush();
$em->clear();
// Fetch the Location object
$query = $em->createQuery("SELECT l FROM Geo\Entity\Location l WHERE l.address = '1600 Amphitheatre Parkway, Mountain View, CA'");
$location = $query->getSingleResult();
/* @var Geo\ValueObject\Point */
$point = $location->getPoint();

View File

@@ -0,0 +1,402 @@
Aggregate Fields
================
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
You will often come across the requirement to display aggregate
values of data that can be computed by using the MIN, MAX, COUNT or
SUM SQL functions. For any ORM this is a tricky issue
traditionally. Doctrine 2 offers several ways to get access to
these values and this article will describe all of them from
different perspectives.
You will see that aggregate fields can become very explicit
features in your domain model and how this potentially complex
business rules can be easily tested.
An example model
----------------
Say you want to model a bank account and all their entries. Entries
into the account can either be of positive or negative money
values. Each account has a credit limit and the account is never
allowed to have a balance below that value.
For simplicity we live in a world where money is composed of
integers only. Also we omit the receiver/sender name, stated reason
for transfer and the execution date. These all would have to be
added on the ``Entry`` object.
Our entities look like:
.. code-block:: php
<?php
namespace Bank\Entities;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Account
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private ?int $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private string $no;
/**
* @ORM\OneToMany(targetEntity="Entry", mappedBy="account", cascade={"persist"})
*/
private array $entries;
/**
* @ORM\Column(type="integer")
*/
private int $maxCredit = 0;
public function __construct(string $no, int $maxCredit = 0)
{
$this->no = $no;
$this->maxCredit = $maxCredit;
$this->entries = new \Doctrine\Common\Collections\ArrayCollection();
}
}
/**
* @ORM\Entity
*/
class Entry
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private ?int $id;
/**
* @ORM\ManyToOne(targetEntity="Account", inversedBy="entries")
*/
private Account $account;
/**
* @ORM\Column(type="integer")
*/
private int $amount;
public function __construct(Account $account, int $amount)
{
$this->account = $account;
$this->amount = $amount;
// more stuff here, from/to whom, stated reason, execution date and such
}
public function getAmount(): Amount
{
return $this->amount;
}
}
Using DQL
---------
The Doctrine Query Language allows you to select for aggregate
values computed from fields of your Domain Model. You can select
the current balance of your account by calling:
.. code-block:: php
<?php
$dql = "SELECT SUM(e.amount) AS balance FROM Bank\Entities\Entry e " .
"WHERE e.account = ?1";
$balance = $em->createQuery($dql)
->setParameter(1, $myAccountId)
->getSingleScalarResult();
The ``$em`` variable in this (and forthcoming) example holds the
Doctrine ``EntityManager``. We create a query for the SUM of all
amounts (negative amounts are withdraws) and retrieve them as a
single scalar result, essentially return only the first column of
the first row.
This approach is simple and powerful, however it has a serious
drawback. We have to execute a specific query for the balance
whenever we need it.
To implement a powerful domain model we would rather have access to
the balance from our ``Account`` entity during all times (even if
the Account was not persisted in the database before!).
Also an additional requirement is the max credit per ``Account``
rule.
We cannot reliably enforce this rule in our ``Account`` entity with
the DQL retrieval of the balance. There are many different ways to
retrieve accounts. We cannot guarantee that we can execute the
aggregation query for all these use-cases, let alone that a
userland programmer checks this balance against newly added
entries.
Using your Domain Model
-----------------------
``Account`` and all the ``Entry`` instances are connected through a
collection, which means we can compute this value at runtime:
.. code-block:: php
<?php
class Account
{
// .. previous code
public function getBalance(): int
{
$balance = 0;
foreach ($this->entries as $entry) {
$balance += $entry->getAmount();
}
return $balance;
}
}
Now we can always call ``Account::getBalance()`` to access the
current account balance.
To enforce the max credit rule we have to implement the "Aggregate
Root" pattern as described in Eric Evans book on Domain Driven
Design. Described with one sentence, an aggregate root controls the
instance creation, access and manipulation of its children.
In our case we want to enforce that new entries can only added to
the ``Account`` by using a designated method. The ``Account`` is
the aggregate root of this relation. We can also enforce the
correctness of the bi-directional ``Account`` <-> ``Entry``
relation with this method:
.. code-block:: php
<?php
class Account
{
public function addEntry(int $amount): void
{
$this->assertAcceptEntryAllowed($amount);
$e = new Entry($this, $amount);
$this->entries[] = $e;
}
}
Now look at the following test-code for our entities:
.. code-block:: php
<?php
use PHPUnit\Framework\TestCase;
class AccountTest extends TestCase
{
public function testAddEntry()
{
$account = new Account("123456", $maxCredit = 200);
$this->assertEquals(0, $account->getBalance());
$account->addEntry(500);
$this->assertEquals(500, $account->getBalance());
$account->addEntry(-700);
$this->assertEquals(-200, $account->getBalance());
}
public function testExceedMaxLimit()
{
$account = new Account("123456", $maxCredit = 200);
$this->expectException(Exception::class);
$account->addEntry(-1000);
}
}
To enforce our rule we can now implement the assertion in
``Account::addEntry``:
.. code-block:: php
<?php
class Account
{
// .. previous code
private function assertAcceptEntryAllowed(int $amount): void
{
$futureBalance = $this->getBalance() + $amount;
$allowedMinimalBalance = ($this->maxCredit * -1);
if ($futureBalance < $allowedMinimalBalance) {
throw new Exception("Credit Limit exceeded, entry is not allowed!");
}
}
}
We haven't talked to the entity manager for persistence of our
account example before. You can call
``EntityManager::persist($account)`` and then
``EntityManager::flush()`` at any point to save the account to the
database. All the nested ``Entry`` objects are automatically
flushed to the database also.
.. code-block:: php
<?php
$account = new Account("123456", 200);
$account->addEntry(500);
$account->addEntry(-200);
$em->persist($account);
$em->flush();
The current implementation has a considerable drawback. To get the
balance, we have to initialize the complete ``Account::$entries``
collection, possibly a very large one. This can considerably hurt
the performance of your application.
Using an Aggregate Field
------------------------
To overcome the previously mentioned issue (initializing the whole
entries collection) we want to add an aggregate field called
"balance" on the Account and adjust the code in
``Account::getBalance()`` and ``Account:addEntry()``:
.. code-block:: php
<?php
class Account
{
/**
* @ORM\Column(type="integer")
*/
private int $balance = 0;
public function getBalance(): int
{
return $this->balance;
}
public function addEntry(int $amount): void
{
$this->assertAcceptEntryAllowed($amount);
$e = new Entry($this, $amount);
$this->entries[] = $e;
$this->balance += $amount;
}
}
This is a very simple change, but all the tests still pass. Our
account entities return the correct balance. Now calling the
``Account::getBalance()`` method will not occur the overhead of
loading all entries anymore. Adding a new Entry to the
``Account::$entities`` will also not initialize the collection
internally.
Adding a new entry is therefore very performant and explicitly
hooked into the domain model. It will only update the account with
the current balance and insert the new entry into the database.
Tackling Race Conditions with Aggregate Fields
----------------------------------------------
Whenever you denormalize your database schema race-conditions can
potentially lead to inconsistent state. See this example:
.. code-block:: php
<?php
use Bank\Entities\Account;
// The Account $accId has a balance of 0 and a max credit limit of 200:
// request 1 account
$account1 = $em->find(Account::class, $accId);
// request 2 account
$account2 = $em->find(Account::class, $accId);
$account1->addEntry(-200);
$account2->addEntry(-200);
// now request 1 and 2 both flush the changes.
The aggregate field ``Account::$balance`` is now -200, however the
SUM over all entries amounts yields -400. A violation of our max
credit rule.
You can use both optimistic or pessimistic locking to safe-guard
your aggregate fields against this kind of race-conditions. Reading
Eric Evans DDD carefully he mentions that the "Aggregate Root"
(Account in our example) needs a locking mechanism.
Optimistic locking is as easy as adding a version column:
.. code-block:: php
<?php
class Account
{
/**
* @ORM\Column(type="integer")
* @ORM\Version
*/
private int $version;
}
The previous example would then throw an exception in the face of
whatever request saves the entity last (and would create the
inconsistent state).
Pessimistic locking requires an additional flag set on the
``EntityManager::find()`` call, enabling write locking directly in
the database using a FOR UPDATE.
.. code-block:: php
<?php
use Bank\Entities\Account;
use Doctrine\DBAL\LockMode;
$account = $em->find(Account::class, $accId, LockMode::PESSIMISTIC_READ);
Keeping Updates and Deletes in Sync
-----------------------------------
The example shown in this article does not allow changes to the
value in ``Entry``, which considerably simplifies the effort to
keep ``Account::$balance`` in sync. If your use-case allows fields
to be updated or related entities to be removed you have to
encapsulate this logic in your "Aggregate Root" entity and adjust
the aggregate field accordingly.
Conclusion
----------
This article described how to obtain aggregate values using DQL or
your domain model. It showed how you can easily add an aggregate
field that offers serious performance benefits over iterating all
the related objects that make up an aggregate value. Finally I
showed how you can ensure that your aggregate fields do not get out
of sync due to race-conditions and concurrent access.

View File

@@ -0,0 +1,101 @@
Custom Mapping Types
====================
Doctrine allows you to create new mapping types. This can come in
handy when you're missing a specific mapping type or when you want
to replace the existing implementation of a mapping type.
In order to create a new mapping type you need to subclass
``Doctrine\DBAL\Types\Type`` and implement/override the methods as
you wish. Here is an example skeleton of such a custom type class:
.. code-block:: php
<?php
namespace My\Project\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
/**
* My custom datatype.
*/
class MyType extends Type
{
const MYTYPE = 'mytype'; // modify to match your type name
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
// return the SQL used to create your column type. To create a portable column type, use the $platform.
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
// This is executed when the value is read from the database. Make your conversions here, optionally using the $platform.
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
// This is executed when the value is written to the database. Make your conversions here, optionally using the $platform.
}
public function getName()
{
return self::MYTYPE; // modify to match your constant name
}
}
.. note::
The following assumptions are applied to mapping types by the ORM:
- The ``UnitOfWork`` never passes values to the database convert
method that did not change in the request.
- The ``UnitOfWork`` internally assumes that entity identifiers are
castable to string. Hence, when using custom types that map to PHP
objects as IDs, such objects must implement the ``__toString()`` magic
method.
When you have implemented the type you still need to let Doctrine
know about it. This can be achieved through the
``Doctrine\DBAL\Types\Type#addType($name, $className)``
method. See the following example:
.. code-block:: php
<?php
// in bootstrapping code
// ...
use Doctrine\DBAL\Types\Type;
// ...
// Register my type
Type::addType('mytype', 'My\Project\Types\MyType');
To convert the underlying database type of your
new "mytype" directly into an instance of ``MyType`` when performing
schema operations, the type has to be registered with the database
platform as well:
.. code-block:: php
<?php
$conn = $em->getConnection();
$conn->getDatabasePlatform()->registerDoctrineTypeMapping('db_mytype', 'mytype');
When registering the custom types in the configuration you specify a unique
name for the mapping type and map that to the corresponding fully qualified
class name. Now the new type can be used when mapping columns:
.. code-block:: php
<?php
class MyPersistentClass
{
/** @Column(type="mytype") */
private $field;
}

View File

@@ -0,0 +1,273 @@
Persisting the Decorator Pattern
================================
.. sectionauthor:: Chris Woodford <chris.woodford@gmail.com>
This recipe will show you a simple example of how you can use
Doctrine 2 to persist an implementation of the
`Decorator Pattern <http://en.wikipedia.org/wiki/Decorator_pattern>`_
Component
---------
The ``Component`` class needs to be persisted, so it's going to
be an ``Entity``. As the top of the inheritance hierarchy, it's going
to have to define the persistent inheritance. For this example, we
will use Single Table Inheritance, but Class Table Inheritance
would work as well. In the discriminator map, we will define two
concrete subclasses, ``ConcreteComponent`` and ``ConcreteDecorator``.
.. code-block:: php
<?php
namespace Test;
/**
* @Entity
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"cc" = "Test\Component\ConcreteComponent",
"cd" = "Test\Decorator\ConcreteDecorator"})
*/
abstract class Component
{
/**
* @Id @Column(type="integer")
* @GeneratedValue(strategy="AUTO")
*/
protected $id;
/** @Column(type="string", nullable=true) */
protected $name;
/**
* Get id
* @return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* Set name
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* Get name
* @return string $name
*/
public function getName()
{
return $this->name;
}
}
ConcreteComponent
-----------------
The ``ConcreteComponent`` class is pretty simple and doesn't do much
more than extend the abstract ``Component`` class (only for the
purpose of keeping this example simple).
.. code-block:: php
<?php
namespace Test\Component;
use Test\Component;
/** @Entity */
class ConcreteComponent extends Component
{}
Decorator
---------
The ``Decorator`` class doesn't need to be persisted, but it does
need to define an association with a persisted ``Entity``. We can
use a ``MappedSuperclass`` for this.
.. code-block:: php
<?php
namespace Test;
/** @MappedSuperclass */
abstract class Decorator extends Component
{
/**
* @OneToOne(targetEntity="Test\Component", cascade={"all"})
* @JoinColumn(name="decorates", referencedColumnName="id")
*/
protected $decorates;
/**
* initialize the decorator
* @param Component $c
*/
public function __construct(Component $c)
{
$this->setDecorates($c);
}
/**
* (non-PHPdoc)
* @see Test.Component::getName()
*/
public function getName()
{
return 'Decorated ' . $this->getDecorates()->getName();
}
/**
* the component being decorated
* @return Component
*/
protected function getDecorates()
{
return $this->decorates;
}
/**
* sets the component being decorated
* @param Component $c
*/
protected function setDecorates(Component $c)
{
$this->decorates = $c;
}
}
All operations on the ``Decorator`` (i.e. persist, remove, etc) will
cascade from the ``Decorator`` to the ``Component``. This means that
when we persist a ``Decorator``, Doctrine will take care of
persisting the chain of decorated objects for us. A ``Decorator`` can
be treated exactly as a ``Component`` when it comes time to
persisting it.
The ``Decorator's`` constructor accepts an instance of a
``Component``, as defined by the ``Decorator`` pattern. The
setDecorates/getDecorates methods have been defined as protected to
hide the fact that a ``Decorator`` is decorating a ``Component`` and
keeps the ``Component`` interface and the ``Decorator`` interface
identical.
To illustrate the intended result of the ``Decorator`` pattern, the
getName() method has been overridden to append a string to the
``Component's`` getName() method.
ConcreteDecorator
-----------------
The final class required to complete a simple implementation of the
Decorator pattern is the ``ConcreteDecorator``. In order to further
illustrate how the ``Decorator`` can alter data as it moves through
the chain of decoration, a new field, "special", has been added to
this class. The getName() has been overridden and appends the value
of the getSpecial() method to its return value.
.. code-block:: php
<?php
namespace Test\Decorator;
use Test\Decorator;
/** @Entity */
class ConcreteDecorator extends Decorator
{
/** @Column(type="string", nullable=true) */
protected $special;
/**
* Set special
* @param string $special
*/
public function setSpecial($special)
{
$this->special = $special;
}
/**
* Get special
* @return string $special
*/
public function getSpecial()
{
return $this->special;
}
/**
* (non-PHPdoc)
* @see Test.Component::getName()
*/
public function getName()
{
return '[' . $this->getSpecial()
. '] ' . parent::getName();
}
}
Examples
--------
Here is an example of how to persist and retrieve your decorated
objects
.. code-block:: php
<?php
use Test\Component\ConcreteComponent,
Test\Decorator\ConcreteDecorator;
// assumes Doctrine 2 is configured and an instance of
// an EntityManager is available as $em
// create a new concrete component
$c = new ConcreteComponent();
$c->setName('Test Component 1');
$em->persist($c); // assigned unique ID = 1
// create a new concrete decorator
$c = new ConcreteComponent();
$c->setName('Test Component 2');
$d = new ConcreteDecorator($c);
$d->setSpecial('Really');
$em->persist($d);
// assigns c as unique ID = 2, and d as unique ID = 3
$em->flush();
$c = $em->find('Test\Component', 1);
$d = $em->find('Test\Component', 3);
echo get_class($c);
// prints: Test\Component\ConcreteComponent
echo $c->getName();
// prints: Test Component 1
echo get_class($d)
// prints: Test\Component\ConcreteDecorator
echo $d->getName();
// prints: [Really] Decorated Test Component 2

View File

@@ -0,0 +1,217 @@
Extending DQL in Doctrine 2: Custom AST Walkers
===============================================
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
The Doctrine Query Language (DQL) is a proprietary sql-dialect that
substitutes tables and columns for Entity names and their fields.
Using DQL you write a query against the database using your
entities. With the help of the metadata you can write very concise,
compact and powerful queries that are then translated into SQL by
the Doctrine ORM.
In Doctrine 1 the DQL language was not implemented using a real
parser. This made modifications of the DQL by the user impossible.
Doctrine 2 in contrast has a real parser for the DQL language,
which transforms the DQL statement into an
`Abstract Syntax Tree <http://en.wikipedia.org/wiki/Abstract_syntax_tree>`_
and generates the appropriate SQL statement for it. Since this
process is deterministic Doctrine heavily caches the SQL that is
generated from any given DQL query, which reduces the performance
overhead of the parsing process to zero.
You can modify the Abstract syntax tree by hooking into DQL parsing
process by adding a Custom Tree Walker. A walker is an interface
that walks each node of the Abstract syntax tree, thereby
generating the SQL statement.
There are two types of custom tree walkers that you can hook into
the DQL parser:
- An output walker. This one actually generates the SQL, and there
is only ever one of them. We implemented the default SqlWalker
implementation for it.
- A tree walker. There can be many tree walkers, they cannot
generate the sql, however they can modify the AST before its
rendered to sql.
Now this is all awfully technical, so let me come to some use-cases
fast to keep you motivated. Using walker implementation you can for
example:
- Modify the AST to generate a Count Query to be used with a
paginator for any given DQL query.
- Modify the Output Walker to generate vendor-specific SQL
(instead of ANSI).
- Modify the AST to add additional where clauses for specific
entities (example ACL, country-specific content...)
- Modify the Output walker to pretty print the SQL for debugging
purposes.
In this cookbook-entry I will show examples on the first two
points. There are probably much more use-cases.
Generic count query for pagination
----------------------------------
Say you have a blog and posts all with one category and one author.
A query for the front-page or any archive page might look something
like:
.. code-block:: sql
SELECT p, c, a FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...
Now in this query the blog post is the root entity, meaning its the
one that is hydrated directly from the query and returned as an
array of blog posts. In contrast the comment and author are loaded
for deeper use in the object tree.
A pagination for this query would want to approximate the number of
posts that match the WHERE clause of this query to be able to
predict the number of pages to show to the user. A draft of the DQL
query for pagination would look like:
.. code-block:: sql
SELECT count(DISTINCT p.id) FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...
Now you could go and write each of these queries by hand, or you
can use a tree walker to modify the AST for you. Lets see how the
API would look for this use-case:
.. code-block:: php
<?php
$pageNum = 1;
$query = $em->createQuery($dql);
$query->setFirstResult( ($pageNum-1) * 20)->setMaxResults(20);
$totalResults = Paginate::count($query);
$results = $query->getResult();
The ``Paginate::count(Query $query)`` looks like:
.. code-block:: php
<?php
class Paginate
{
static public function count(Query $query)
{
/* @var $countQuery Query */
$countQuery = clone $query;
$countQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('DoctrineExtensions\Paginate\CountSqlWalker'));
$countQuery->setFirstResult(null)->setMaxResults(null);
return $countQuery->getSingleScalarResult();
}
}
It clones the query, resets the limit clause first and max results
and registers the ``CountSqlWalker`` custom tree walker which
will modify the AST to execute a count query. The walkers
implementation is:
.. code-block:: php
<?php
class CountSqlWalker extends TreeWalkerAdapter
{
/**
* Walks down a SelectStatement AST node, thereby generating the appropriate SQL.
*
* @return string The SQL.
*/
public function walkSelectStatement(SelectStatement $AST)
{
$parent = null;
$parentName = null;
foreach ($this->_getQueryComponents() as $dqlAlias => $qComp) {
if ($qComp['parent'] === null && $qComp['nestingLevel'] == 0) {
$parent = $qComp;
$parentName = $dqlAlias;
break;
}
}
$pathExpression = new PathExpression(
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $parentName,
$parent['metadata']->getSingleIdentifierFieldName()
);
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
$AST->selectClause->selectExpressions = array(
new SelectExpression(
new AggregateExpression('count', $pathExpression, true), null
)
);
}
}
This will delete any given select expressions and replace them with
a distinct count query for the root entities primary key. This will
only work if your entity has only one identifier field (composite
keys won't work).
Modify the Output Walker to generate Vendor specific SQL
--------------------------------------------------------
Most RMDBS have vendor-specific features for optimizing select
query execution plans. You can write your own output walker to
introduce certain keywords using the Query Hint API. A query hint
can be set via ``Query::setHint($name, $value)`` as shown in the
previous example with the ``HINT_CUSTOM_TREE_WALKERS`` query hint.
We will implement a custom Output Walker that allows to specify the
``SQL_NO_CACHE`` query hint.
.. code-block:: php
<?php
$dql = "SELECT p, c, a FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...";
$query = $m->createQuery($dql);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'DoctrineExtensions\Query\MysqlWalker');
$query->setHint("mysqlWalker.sqlNoCache", true);
$results = $query->getResult();
Our ``MysqlWalker`` will extend the default ``SqlWalker``. We will
modify the generation of the SELECT clause, adding the
``SQL_NO_CACHE`` on those queries that need it:
.. code-block:: php
<?php
class MysqlWalker extends SqlWalker
{
/**
* Walks down a SelectClause AST node, thereby generating the appropriate SQL.
*
* @param $selectClause
* @return string The SQL.
*/
public function walkSelectClause($selectClause)
{
$sql = parent::walkSelectClause($selectClause);
if ($this->getQuery()->getHint('mysqlWalker.sqlNoCache') === true) {
if ($selectClause->isDistinct) {
$sql = str_replace('SELECT DISTINCT', 'SELECT DISTINCT SQL_NO_CACHE', $sql);
} else {
$sql = str_replace('SELECT', 'SELECT SQL_NO_CACHE', $sql);
}
}
return $sql;
}
}
Writing extensions to the Output Walker requires a very deep
understanding of the DQL Parser and Walkers, but may offer your
huge benefits with using vendor specific features. This would still
allow you write DQL queries instead of NativeQueries to make use of
vendor specific features.

View File

@@ -0,0 +1,251 @@
DQL User Defined Functions
==========================
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
By default DQL supports a limited subset of all the vendor-specific
SQL functions common between all the vendors. However in many cases
once you have decided on a specific database vendor, you will never
change it during the life of your project. This decision for a
specific vendor potentially allows you to make use of powerful SQL
features that are unique to the vendor.
It is worth to mention that Doctrine 2 also allows you to handwrite
your SQL instead of extending the DQL parser. Extending DQL is sort of an
advanced extension point. You can map arbitrary SQL to your objects
and gain access to vendor specific functionalities using the
``EntityManager#createNativeQuery()`` API as described in
the :doc:`Native Query <../reference/native-sql>` chapter.
The DQL Parser has hooks to register functions that can then be
used in your DQL queries and transformed into SQL, allowing to
extend Doctrines Query capabilities to the vendors strength. This
post explains the User-Defined Functions API (UDF) of the Dql
Parser and shows some examples to give you some hints how you would
extend DQL.
There are three types of functions in DQL, those that return a
numerical value, those that return a string and those that return a
Date. Your custom method has to be registered as either one of
those. The return type information is used by the DQL parser to
check possible syntax errors during the parsing process, for
example using a string function return value in a math expression.
Registering your own DQL functions
----------------------------------
You can register your functions adding them to the ORM
configuration:
.. code-block:: php
<?php
$config = new \Doctrine\ORM\Configuration();
$config->addCustomStringFunction($name, $class);
$config->addCustomNumericFunction($name, $class);
$config->addCustomDatetimeFunction($name, $class);
$em = EntityManager::create($dbParams, $config);
The ``$name`` is the name the function will be referred to in the
DQL query. ``$class`` is a string of a class-name which has to
extend ``Doctrine\ORM\Query\Node\FunctionNode``. This is a class
that offers all the necessary API and methods to implement a UDF.
Instead of providing the function class name, you can also provide
a callable that returns the function object:
.. code-block:: php
<?php
$config = new \Doctrine\ORM\Configuration();
$config->addCustomStringFunction($name, function () {
return new MyCustomFunction();
});
In this post we will implement some MySql specific Date calculation
methods, which are quite handy in my opinion:
Date Diff
---------
`Mysql's DateDiff function <https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_datediff>`_
takes two dates as argument and calculates the difference in days
with ``date1-date2``.
The DQL parser is a top-down recursive descent parser to generate
the Abstract-Syntax Tree (AST) and uses a TreeWalker approach to
generate the appropriate SQL from the AST. This makes reading the
Parser/TreeWalker code manageable in a finite amount of time.
The ``FunctionNode`` class I referred to earlier requires you to
implement two methods, one for the parsing process (obviously)
called ``parse`` and one for the TreeWalker process called
``getSql()``. I show you the code for the DateDiff method and
discuss it step by step:
.. code-block:: php
<?php
/**
* DateDiffFunction ::= "DATEDIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
*/
class DateDiff extends FunctionNode
{
// (1)
public $firstDateExpression = null;
public $secondDateExpression = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER); // (2)
$parser->match(Lexer::T_OPEN_PARENTHESIS); // (3)
$this->firstDateExpression = $parser->ArithmeticPrimary(); // (4)
$parser->match(Lexer::T_COMMA); // (5)
$this->secondDateExpression = $parser->ArithmeticPrimary(); // (6)
$parser->match(Lexer::T_CLOSE_PARENTHESIS); // (3)
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'DATEDIFF(' .
$this->firstDateExpression->dispatch($sqlWalker) . ', ' .
$this->secondDateExpression->dispatch($sqlWalker) .
')'; // (7)
}
}
The Parsing process of the DATEDIFF function is going to find two
expressions the date1 and the date2 values, whose AST Node
representations will be saved in the variables of the DateDiff
FunctionNode instance at (1).
The parse() method has to cut the function call "DATEDIFF" and its
argument into pieces. Since the parser detects the function using a
lookahead the T\_IDENTIFIER of the function name has to be taken
from the stack (2), followed by a detection of the arguments in
(4)-(6). The opening and closing parenthesis have to be detected
also. This happens during the Parsing process and leads to the
generation of a DateDiff FunctionNode somewhere in the AST of the
dql statement.
The ``ArithmeticPrimary`` method call is the most common
denominator of valid EBNF tokens taken from the
`DQL EBNF grammar <http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#ebnf>`_
that matches our requirements for valid input into the DateDiff Dql
function. Picking the right tokens for your methods is a tricky
business, but the EBNF grammar is pretty helpful finding it, as is
looking at the Parser source code.
Now in the TreeWalker process we have to pick up this node and
generate SQL from it, which apparently is quite easy looking at the
code in (7). Since we don't know which type of AST Node the first
and second Date expression are we are just dispatching them back to
the SQL Walker to generate SQL from and then wrap our DATEDIFF
function call around this output.
Now registering this DateDiff FunctionNode with the ORM using:
.. code-block:: php
<?php
$config = new \Doctrine\ORM\Configuration();
$config->addCustomStringFunction('DATEDIFF', 'DoctrineExtensions\Query\MySql\DateDiff');
We can do fancy stuff like:
.. code-block:: sql
SELECT p FROM DoctrineExtensions\Query\BlogPost p WHERE DATEDIFF(CURRENT_TIME(), p.created) < 7
Date Add
--------
Often useful it the ability to do some simple date calculations in
your DQL query using
`MySql's DATE_ADD function <https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-add>`_.
I'll skip the blah and show the code for this function:
.. code-block:: php
<?php
/**
* DateAddFunction ::=
* "DATE_ADD" "(" ArithmeticPrimary ", INTERVAL" ArithmeticPrimary Identifier ")"
*/
class DateAdd extends FunctionNode
{
public $firstDateExpression = null;
public $intervalExpression = null;
public $unit = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstDateExpression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA);
$parser->match(Lexer::T_IDENTIFIER);
$this->intervalExpression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_IDENTIFIER);
/* @var $lexer Lexer */
$lexer = $parser->getLexer();
$this->unit = $lexer->token['value'];
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'DATE_ADD(' .
$this->firstDateExpression->dispatch($sqlWalker) . ', INTERVAL ' .
$this->intervalExpression->dispatch($sqlWalker) . ' ' . $this->unit .
')';
}
}
The only difference compared to the DATEDIFF here is, we
additionally need the ``Lexer`` to access the value of the
``T_IDENTIFIER`` token for the Date Interval unit, for example the
MONTH in:
.. code-block:: sql
SELECT p FROM DoctrineExtensions\Query\BlogPost p WHERE DATE_ADD(CURRENT_TIME(), INTERVAL 4 MONTH) > p.created
The above method now only supports the specification using
``INTERVAL``, to also allow a real date in DATE\_ADD we need to add
some decision logic to the parsing process (makes up for a nice
exercise).
Now as you see, the Parsing process doesn't catch all the possible
SQL errors, here we don't match for all the valid inputs for the
interval unit. However where necessary we rely on the database
vendors SQL parser to show us further errors in the parsing
process, for example if the Unit would not be one of the supported
values by MySql.
Conclusion
----------
Now that you all know how you can implement vendor specific SQL
functionalities in DQL, we would be excited to see user extensions
that add vendor specific function packages, for example more math
functions, XML + GIS Support, Hashing functions and so on.
For 2.0 we will come with the current set of functions, however for
a future version we will re-evaluate if we can abstract even more
vendor sql functions and extend the DQL languages scope.
Code for this Extension to DQL and other Doctrine Extensions can be
found
`in the GitHub DoctrineExtensions repository <http://github.com/beberlei/DoctrineExtensions>`_.

View File

@@ -0,0 +1,68 @@
Entities in the Session
=======================
There are several use-cases to save entities in the session, for example:
1. User object
2. Multi-step forms
To achieve this with Doctrine you have to pay attention to some details to get
this working.
Merging entity into an EntityManager
------------------------------------
In Doctrine an entity objects has to be "managed" by an EntityManager to be
updateable. Entities saved into the session are not managed in the next request
anymore. This means that you have to register these entities with an
EntityManager again if you want to change them or use them as part of
references between other entities. You can achieve this by calling
``EntityManager#merge()``.
For a representative User object the code to get turn an instance from
the session into a managed Doctrine object looks like this:
.. code-block:: php
<?php
require_once 'bootstrap.php';
$em = GetEntityManager(); // creates an EntityManager
session_start();
if (isset($_SESSION['user']) && $_SESSION['user'] instanceof User) {
$user = $_SESSION['user'];
$user = $em->merge($user);
}
.. note::
A frequent mistake is not to get the merged user object from the return
value of ``EntityManager#merge()``. The entity object passed to merge is
not necessarily the same object that is returned from the method.
Serializing entity into the session
-----------------------------------
Entities that are serialized into the session normally contain references to
other entities as well. Think of the user entity has a reference to their
articles, groups, photos or many other different entities. If you serialize
this object into the session then you don't want to serialize the related
entities as well. This is why you should call ``EntityManager#detach()`` on this
object or implement the __sleep() magic method on your entity.
.. code-block:: php
<?php
require_once 'bootstrap.php';
$em = GetEntityManager(); // creates an EntityManager
$user = $em->find("User", 1);
$em->detach($user);
$_SESSION['user'] = $user;
.. note::
When you called detach on your objects they get "unmanaged" with that
entity manager. This means you cannot use them as part of write operations
during ``EntityManager#flush()`` anymore in this request.

View File

@@ -0,0 +1,112 @@
Implementing ArrayAccess for Domain Objects
===========================================
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
This recipe will show you how to implement ArrayAccess for your
domain objects in order to allow more uniform access, for example
in templates. In these examples we will implement ArrayAccess on a
`Layer Supertype <http://martinfowler.com/eaaCatalog/layerSupertype.html>`_
for all our domain objects.
Option 1
--------
In this implementation we will make use of PHPs highly dynamic
nature to dynamically access properties of a subtype in a supertype
at runtime. Note that this implementation has 2 main caveats:
- It will not work with private fields
- It will not go through any getters/setters
.. code-block:: php
<?php
abstract class DomainObject implements ArrayAccess
{
public function offsetExists($offset) {
return isset($this->$offset);
}
public function offsetSet($offset, $value) {
$this->$offset = $value;
}
public function offsetGet($offset) {
return $this->$offset;
}
public function offsetUnset($offset) {
$this->$offset = null;
}
}
Option 2
--------
In this implementation we will dynamically invoke getters/setters.
Again we use PHPs dynamic nature to invoke methods on a subtype
from a supertype at runtime. This implementation has the following
caveats:
- It relies on a naming convention
- The semantics of offsetExists can differ
- offsetUnset will not work with typehinted setters
.. code-block:: php
<?php
abstract class DomainObject implements ArrayAccess
{
public function offsetExists($offset) {
// In this example we say that exists means it is not null
$value = $this->{"get$offset"}();
return $value !== null;
}
public function offsetSet($offset, $value) {
$this->{"set$offset"}($value);
}
public function offsetGet($offset) {
return $this->{"get$offset"}();
}
public function offsetUnset($offset) {
$this->{"set$offset"}(null);
}
}
Read-only
---------
You can slightly tweak option 1 or option 2 in order to make array
access read-only. This will also circumvent some of the caveats of
each option. Simply make offsetSet and offsetUnset throw an
exception (i.e. BadMethodCallException).
.. code-block:: php
<?php
abstract class DomainObject implements ArrayAccess
{
public function offsetExists($offset) {
// option 1 or option 2
}
public function offsetSet($offset, $value) {
throw new BadMethodCallException("Array access of class " . get_class($this) . " is read-only!");
}
public function offsetGet($offset) {
// option 1 or option 2
}
public function offsetUnset($offset) {
throw new BadMethodCallException("Array access of class " . get_class($this) . " is read-only!");
}
}

View File

@@ -0,0 +1,72 @@
Implementing the Notify ChangeTracking Policy
=============================================
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
The NOTIFY change-tracking policy is the most effective
change-tracking policy provided by Doctrine but it requires some
boilerplate code. This recipe will show you how this boilerplate
code should look like. We will implement it on a
`Layer Supertype <http://martinfowler.com/eaaCatalog/layerSupertype.html>`_
for all our domain objects.
Implementing NotifyPropertyChanged
----------------------------------
The NOTIFY policy is based on the assumption that the entities
notify interested listeners of changes to their properties. For
that purpose, a class that wants to use this policy needs to
implement the ``NotifyPropertyChanged`` interface from the
``Doctrine\Common`` namespace.
.. code-block:: php
<?php
use Doctrine\Persistence\NotifyPropertyChanged;
use Doctrine\Persistence\PropertyChangedListener;
abstract class DomainObject implements NotifyPropertyChanged
{
private $listeners = array();
public function addPropertyChangedListener(PropertyChangedListener $listener) {
$this->listeners[] = $listener;
}
/** Notifies listeners of a change. */
protected function onPropertyChanged($propName, $oldValue, $newValue) {
if ($this->listeners) {
foreach ($this->listeners as $listener) {
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
}
}
}
}
Then, in each property setter of concrete, derived domain classes,
you need to invoke onPropertyChanged as follows to notify
listeners:
.. code-block:: php
<?php
// Mapping not shown, either in annotations, xml or yaml as usual
class MyEntity extends DomainObject
{
private $data;
// ... other fields as usual
public function setData($data) {
if ($data != $this->data) { // check: is it actually modified?
$this->onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
}
The check whether the new value is different from the old one is
not mandatory but recommended. That way you can avoid unnecessary
updates and also have full control over when you consider a
property changed.

View File

@@ -0,0 +1,78 @@
Implementing Wakeup or Clone
============================
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
As explained in the
`restrictions for entity classes in the manual <http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/architecture.html#entities>`_,
it is usually not allowed for an entity to implement ``__wakeup``
or ``__clone``, because Doctrine makes special use of them.
However, it is quite easy to make use of these methods in a safe
way by guarding the custom wakeup or clone code with an entity
identity check, as demonstrated in the following sections.
Safely implementing __wakeup
----------------------------
To safely implement ``__wakeup``, simply enclose your
implementation code in an identity check as follows:
.. code-block:: php
<?php
class MyEntity
{
private $id; // This is the identifier of the entity.
//...
public function __wakeup()
{
// If the entity has an identity, proceed as normal.
if ($this->id) {
// ... Your code here as normal ...
}
// otherwise do nothing, do NOT throw an exception!
}
//...
}
Safely implementing __clone
---------------------------
Safely implementing ``__clone`` is pretty much the same:
.. code-block:: php
<?php
class MyEntity
{
private $id; // This is the identifier of the entity.
//...
public function __clone()
{
// If the entity has an identity, proceed as normal.
if ($this->id) {
// ... Your code here as normal ...
}
// otherwise do nothing, do NOT throw an exception!
}
//...
}
Summary
-------
As you have seen, it is quite easy to safely make use of
``__wakeup`` and ``__clone`` in your entities without adding any
really Doctrine-specific or Doctrine-dependant code.
These implementations are possible and safe because when Doctrine
invokes these methods, the entities never have an identity (yet).
Furthermore, it is possibly a good idea to check for the identity
in your code anyway, since it's rarely the case that you want to
unserialize or clone an entity with no identity.

View File

@@ -0,0 +1,199 @@
Mysql Enums
===========
The type system of Doctrine 2 consists of flyweights, which means there is only
one instance of any given type. Additionally types do not contain state. Both
assumptions make it rather complicated to work with the Enum Type of MySQL that
is used quite a lot by developers.
When using Enums with a non-tweaked Doctrine 2 application you will get
errors from the Schema-Tool commands due to the unknown database type "enum".
By default Doctrine does not map the MySQL enum type to a Doctrine type.
This is because Enums contain state (their allowed values) and Doctrine
types don't.
This cookbook entry shows two possible solutions to work with MySQL enums.
But first a word of warning. The MySQL Enum type has considerable downsides:
- Adding new values requires to rebuild the whole table, which can take hours
depending on the size.
- Enums are ordered in the way the values are specified, not in their "natural" order.
- Enums validation mechanism for allowed values is not necessarily good,
specifying invalid values leads to an empty enum for the default MySQL error
settings. You can easily replicate the "allow only some values" requirement
in your Doctrine entities.
Solution 1: Mapping to Varchars
-------------------------------
You can map ENUMs to varchars. You can register MySQL ENUMs to map to Doctrine
varchars. This way Doctrine always resolves ENUMs to Doctrine varchars. It
will even detect this match correctly when using SchemaTool update commands.
.. code-block:: php
<?php
$conn = $em->getConnection();
$conn->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
In this case you have to ensure that each varchar field that is an enum in the
database only gets passed the allowed values. You can easily enforce this in your
entities:
.. code-block:: php
<?php
/** @Entity */
class Article
{
const STATUS_VISIBLE = 'visible';
const STATUS_INVISIBLE = 'invisible';
/** @Column(type="string") */
private $status;
public function setStatus($status)
{
if (!in_array($status, array(self::STATUS_VISIBLE, self::STATUS_INVISIBLE))) {
throw new \InvalidArgumentException("Invalid status");
}
$this->status = $status;
}
}
If you want to actively create enums through the Doctrine Schema-Tool by using
the **columnDefinition** attribute.
.. code-block:: php
<?php
/** @Entity */
class Article
{
/** @Column(type="string", columnDefinition="ENUM('visible', 'invisible')") */
private $status;
}
In this case however Schema-Tool update will have a hard time not to request changes for this column on each call.
Solution 2: Defining a Type
---------------------------
You can make a stateless ENUM type by creating a type class for each unique set of ENUM values.
For example for the previous enum type:
.. code-block:: php
<?php
namespace MyProject\DBAL;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
class EnumVisibilityType extends Type
{
const ENUM_VISIBILITY = 'enumvisibility';
const STATUS_VISIBLE = 'visible';
const STATUS_INVISIBLE = 'invisible';
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return "ENUM('visible', 'invisible')";
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $value;
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (!in_array($value, array(self::STATUS_VISIBLE, self::STATUS_INVISIBLE))) {
throw new \InvalidArgumentException("Invalid status");
}
return $value;
}
public function getName()
{
return self::ENUM_VISIBILITY;
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
You can register this type with ``Type::addType('enumvisibility', 'MyProject\DBAL\EnumVisibilityType');``.
Then in your entity you can just use this type:
.. code-block:: php
<?php
/** @Entity */
class Article
{
/** @Column(type="enumvisibility") */
private $status;
}
You can generalize this approach easily to create a base class for enums:
.. code-block:: php
<?php
namespace MyProject\DBAL;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
abstract class EnumType extends Type
{
protected $name;
protected $values = array();
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
$values = array_map(function($val) { return "'".$val."'"; }, $this->values);
return "ENUM(".implode(", ", $values).")";
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $value;
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (!in_array($value, $this->values)) {
throw new \InvalidArgumentException("Invalid '".$this->name."' value.");
}
return $value;
}
public function getName()
{
return $this->name;
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
With this base class you can define an enum as easily as:
.. code-block:: php
<?php
namespace MyProject\DBAL;
class EnumVisibilityType extends EnumType
{
protected $name = 'enumvisibility';
protected $values = array('visible', 'invisible');
}

View File

@@ -0,0 +1,142 @@
Keeping your Modules independent
=================================
.. versionadded:: 2.2
One of the goals of using modules is to create discrete units of functionality
that do not have many (if any) dependencies, allowing you to use that
functionality in other applications without including unnecessary items.
Doctrine 2.2 includes a new utility called the ``ResolveTargetEntityListener``,
that functions by intercepting certain calls inside Doctrine and rewrite
targetEntity parameters in your metadata mapping at runtime. It means that
in your bundle you are able to use an interface or abstract class in your
mappings and expect correct mapping to a concrete entity at runtime.
This functionality allows you to define relationships between different entities
but not making them hard dependencies.
Background
----------
In the following example, the situation is we have an `InvoiceModule`
which provides invoicing functionality, and a `CustomerModule` that
contains customer management tools. We want to keep these separated,
because they can be used in other systems without each other, but for
our application we want to use them together.
In this case, we have an ``Invoice`` entity with a relationship to a
non-existent object, an ``InvoiceSubjectInterface``. The goal is to get
the ``ResolveTargetEntityListener`` to replace any mention of the interface
with a real object that implements that interface.
Set up
------
We're going to use the following basic entities (which are incomplete
for brevity) to explain how to set up and use the RTEL.
A Customer entity
.. code-block:: php
<?php
// src/Acme/AppModule/Entity/Customer.php
namespace Acme\AppModule\Entity;
use Doctrine\ORM\Mapping as ORM;
use Acme\CustomerModule\Entity\Customer as BaseCustomer;
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
/**
* @ORM\Entity
* @ORM\Table(name="customer")
*/
class Customer extends BaseCustomer implements InvoiceSubjectInterface
{
// In our example, any methods defined in the InvoiceSubjectInterface
// are already implemented in the BaseCustomer
}
An Invoice entity
.. code-block:: php
<?php
// src/Acme/InvoiceModule/Entity/Invoice.php
namespace Acme\InvoiceModule\Entity;
use Doctrine\ORM\Mapping AS ORM;
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
/**
* Represents an Invoice.
*
* @ORM\Entity
* @ORM\Table(name="invoice")
*/
class Invoice
{
/**
* @ORM\ManyToOne(targetEntity="Acme\InvoiceModule\Model\InvoiceSubjectInterface")
* @var InvoiceSubjectInterface
*/
protected $subject;
}
An InvoiceSubjectInterface
.. code-block:: php
<?php
// src/Acme/InvoiceModule/Model/InvoiceSubjectInterface.php
namespace Acme\InvoiceModule\Model;
/**
* An interface that the invoice Subject object should implement.
* In most circumstances, only a single object should implement
* this interface as the ResolveTargetEntityListener can only
* change the target to a single object.
*/
interface InvoiceSubjectInterface
{
// List any additional methods that your InvoiceModule
// will need to access on the subject so that you can
// be sure that you have access to those methods.
/**
* @return string
*/
public function getName();
}
Next, we need to configure the listener. Add this to the area you set up Doctrine. You
must set this up in the way outlined below, otherwise you can not be guaranteed that
the targetEntity resolution will occur reliably:
.. code-block:: php
<?php
$evm = new \Doctrine\Common\EventManager;
$rtel = new \Doctrine\ORM\Tools\ResolveTargetEntityListener;
// Adds a target-entity class
$rtel->addResolveTargetEntity('Acme\\InvoiceModule\\Model\\InvoiceSubjectInterface', 'Acme\\CustomerModule\\Entity\\Customer', array());
// Add the ResolveTargetEntityListener
$evm->addEventListener(Doctrine\ORM\Events::loadClassMetadata, $rtel);
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config, $evm);
Final Thoughts
--------------
With the ``ResolveTargetEntityListener``, we are able to decouple our
bundles, keeping them usable by themselves, but still being able to
define relationships between different objects. By using this method,
I've found my bundles end up being easier to maintain independently.

View File

@@ -0,0 +1,86 @@
SQL-Table Prefixes
==================
This recipe is intended as an example of implementing a
loadClassMetadata listener to provide a Table Prefix option for
your application. The method used below is not a hack, but fully
integrates into the Doctrine system, all SQL generated will include
the appropriate table prefix.
In most circumstances it is desirable to separate different
applications into individual databases, but in certain cases, it
may be beneficial to have a table prefix for your Entities to
separate them from other vendor products in the same database.
Implementing the listener
-------------------------
The listener in this example has been set up with the
DoctrineExtensions namespace. You create this file in your
library/DoctrineExtensions directory, but will need to set up
appropriate autoloaders.
.. code-block:: php
<?php
namespace DoctrineExtensions;
use \Doctrine\ORM\Event\LoadClassMetadataEventArgs;
class TablePrefix
{
protected $prefix = '';
public function __construct($prefix)
{
$this->prefix = (string) $prefix;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (!$classMetadata->isInheritanceTypeSingleTable() || $classMetadata->getName() === $classMetadata->rootEntityName) {
$classMetadata->setPrimaryTable([
'name' => $this->prefix . $classMetadata->getTableName()
]);
}
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
if ($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY && $mapping['isOwningSide']) {
$mappedTableName = $mapping['joinTable']['name'];
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
}
}
}
}
Telling the EntityManager about our listener
--------------------------------------------
A listener of this type must be set up before the EntityManager has
been initialised, otherwise an Entity might be created or cached
before the prefix has been set.
.. note::
If you set this listener up, be aware that you will need
to clear your caches and drop then recreate your database schema.
.. code-block:: php
<?php
// $connectionOptions and $config set earlier
$evm = new \Doctrine\Common\EventManager;
// Table Prefix
$tablePrefix = new \DoctrineExtensions\TablePrefix('prefix_');
$evm->addEventListener(\Doctrine\ORM\Events::loadClassMetadata, $tablePrefix);
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config, $evm);

View File

@@ -0,0 +1,254 @@
Strategy-Pattern
================
This recipe will give you a short introduction on how to design
similar entities without using expensive (i.e. slow) inheritance
but with not more than *the well-known strategy pattern* event
listeners
Scenario / Problem
------------------
Given a Content-Management-System, we probably want to add / edit
some so-called "blocks" and "panels". What are they for?
- A block might be a registration form, some text content, a table
with information. A good example might also be a small calendar.
- A panel is by definition a block that can itself contain blocks.
A good example for a panel might be a sidebar box: You could easily
add a small calendar into it.
So, in this scenario, when building your CMS, you will surely add
lots of blocks and panels to your pages and you will find yourself
highly uncomfortable because of the following:
- Every existing page needs to know about the panels it contains -
therefore, you'll have an association to your panels. But if you've
got several types of panels - what do you do? Add an association to
every panel-type? This wouldn't be flexible. You might be tempted
to add an AbstractPanelEntity and an AbstractBlockEntity that use
class inheritance. Your page could then only confer to the
AbstractPanelType and Doctrine 2 would do the rest for you, i.e.
load the right entities. But - you'll for sure have lots of panels
and blocks, and even worse, you'd have to edit the discriminator
map *manually* every time you or another developer implements a new
block / entity. This would tear down any effort of modular
programming.
Therefore, we need something that's far more flexible.
Solution
--------
The solution itself is pretty easy. We will have one base class
that will be loaded via the page and that has specific behaviour -
a Block class might render the front-end and even the backend, for
example. Now, every block that you'll write might look different or
need different data - therefore, we'll offer an API to these
methods but internally, we use a strategy that exactly knows what
to do.
First of all, we need to make sure that we have an interface that
contains every needed action. Such actions would be rendering the
front-end or the backend, solving dependencies (blocks that are
supposed to be placed in the sidebar could refuse to be placed in
the middle of your page, for example).
Such an interface could look like this:
.. code-block:: php
<?php
/**
* This interface defines the basic actions that a block / panel needs to support.
*
* Every blockstrategy is *only* responsible for rendering a block and declaring some basic
* support, but *not* for updating its configuration etc. For this purpose, use controllers
* and models.
*/
interface BlockStrategyInterface {
/**
* This could configure your entity
*/
public function setConfig(Config\EntityConfig $config);
/**
* Returns the config this strategy is configured with.
* @return Core\Model\Config\EntityConfig
*/
public function getConfig();
/**
* Set the view object.
* @param \Zend_View_Interface $view
* @return \Zend_View_Helper_Interface
*/
public function setView(\Zend_View_Interface $view);
/**
* @return \Zend_View_Interface
*/
public function getView();
/**
* Renders this strategy. This method will be called when the user
* displays the site.
*
* @return string
*/
public function renderFrontend();
/**
* Renders the backend of this block. This method will be called when
* a user tries to reconfigure this block instance.
*
* Most of the time, this method will return / output a simple form which in turn
* calls some controllers.
*
* @return string
*/
public function renderBackend();
/**
* Returns all possible types of panels this block can be stacked onto
*
* @return array
*/
public function getRequiredPanelTypes();
/**
* Determines whether a Block is able to use a given type or not
* @param string $typeName The typename
* @return boolean
*/
public function canUsePanelType($typeName);
public function setBlockEntity(AbstractBlock $block);
public function getBlockEntity();
}
As you can see, we have a method "setBlockEntity" which ties a potential strategy to an object of type AbstractBlock. This type will simply define the basic behaviour of our blocks and could potentially look something like this:
.. code-block:: php
<?php
/**
* This is the base class for both Panels and Blocks.
* It shouldn't be extended by your own blocks - simply write a strategy!
*/
abstract class AbstractBlock {
/**
* The id of the block item instance
* This is a doctrine field, so you need to setup generation for it
* @var integer
*/
private $id;
// Add code for relation to the parent panel, configuration objects, ....
/**
* This var contains the classname of the strategy
* that is used for this blockitem. (This string (!) value will be persisted by Doctrine 2)
*
* This is a doctrine field, so make sure that you use an @column annotation or setup your
* yaml or xml files correctly
* @var string
*/
protected $strategyClassName;
/**
* This var contains an instance of $this->blockStrategy. Will not be persisted by Doctrine 2.
*
* @var BlockStrategyInterface
*/
protected $strategyInstance;
/**
* Returns the strategy that is used for this blockitem.
*
* The strategy itself defines how this block can be rendered etc.
*
* @return string
*/
public function getStrategyClassName() {
return $this->strategyClassName;
}
/**
* Returns the instantiated strategy
*
* @return BlockStrategyInterface
*/
public function getStrategyInstance() {
return $this->strategyInstance;
}
/**
* Sets the strategy this block / panel should work as. Make sure that you've used
* this method before persisting the block!
*
* @param BlockStrategyInterface $strategy
*/
public function setStrategy(BlockStrategyInterface $strategy) {
$this->strategyInstance = $strategy;
$this->strategyClassName = get_class($strategy);
$strategy->setBlockEntity($this);
}
Now, the important point is that $strategyClassName is a Doctrine 2
field, i.e. Doctrine will persist this value. This is only the
class name of your strategy and not an instance!
Finishing your strategy pattern, we hook into the Doctrine postLoad
event and check whether a block has been loaded. If so, you will
initialize it - i.e. get the strategies classname, create an
instance of it and set it via setStrategyBlock().
This might look like this:
.. code-block:: php
<?php
use \Doctrine\ORM,
\Doctrine\Common;
/**
* The BlockStrategyEventListener will initialize a strategy after the
* block itself was loaded.
*/
class BlockStrategyEventListener implements Common\EventSubscriber {
protected $view;
public function __construct(\Zend_View_Interface $view) {
$this->view = $view;
}
public function getSubscribedEvents() {
return array(ORM\Events::postLoad);
}
public function postLoad(ORM\Event\LifecycleEventArgs $args) {
$blockItem = $args->getEntity();
// Both blocks and panels are instances of Block\AbstractBlock
if ($blockItem instanceof Block\AbstractBlock) {
$strategy = $blockItem->getStrategyClassName();
$strategyInstance = new $strategy();
if (null !== $blockItem->getConfig()) {
$strategyInstance->setConfig($blockItem->getConfig());
}
$strategyInstance->setView($this->view);
$blockItem->setStrategy($strategyInstance);
}
}
}
In this example, even some variables are set - like a view object
or a specific configuration object.

View File

@@ -0,0 +1,137 @@
Validation of Entities
======================
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
Doctrine 2 does not ship with any internal validators, the reason
being that we think all the frameworks out there already ship with
quite decent ones that can be integrated into your Domain easily.
What we offer are hooks to execute any kind of validation.
.. note::
You don't need to validate your entities in the lifecycle
events. Its only one of many options. Of course you can also
perform validations in value setters or any other method of your
entities that are used in your code.
Entities can register lifecycle event methods with Doctrine that
are called on different occasions. For validation we would need to
hook into the events called before persisting and updating. Even
though we don't support validation out of the box, the
implementation is even simpler than in Doctrine 1 and you will get
the additional benefit of being able to re-use your validation in
any other part of your domain.
Say we have an ``Order`` with several ``OrderLine`` instances. We
never want to allow any customer to order for a larger sum than they
are allowed to:
.. code-block:: php
<?php
class Order
{
public function assertCustomerAllowedBuying()
{
$orderLimit = $this->customer->getOrderLimit();
$amount = 0;
foreach ($this->orderLines as $line) {
$amount += $line->getAmount();
}
if ($amount > $orderLimit) {
throw new CustomerOrderLimitExceededException();
}
}
}
Now this is some pretty important piece of business logic in your
code, enforcing it at any time is important so that customers with
a unknown reputation don't owe your business too much money.
We can enforce this constraint in any of the metadata drivers.
First Annotations:
.. code-block:: php
<?php
/**
* @Entity
* @HasLifecycleCallbacks
*/
class Order
{
/**
* @PrePersist @PreUpdate
*/
public function assertCustomerAllowedBuying() {}
}
In XML Mappings:
.. code-block:: xml
<doctrine-mapping>
<entity name="Order">
<lifecycle-callbacks>
<lifecycle-callback type="prePersist" method="assertCustomerallowedBuying" />
<lifecycle-callback type="preUpdate" method="assertCustomerallowedBuying" />
</lifecycle-callbacks>
</entity>
</doctrine-mapping>
YAML needs some little change yet, to allow multiple lifecycle
events for one method, this will happen before Beta 1 though.
Now validation is performed whenever you call
``EntityManager#persist($order)`` or when you call
``EntityManager#flush()`` and an order is about to be updated. Any
Exception that happens in the lifecycle callbacks will be caught by
the EntityManager and the current transaction is rolled back.
Of course you can do any type of primitive checks, not null,
email-validation, string size, integer and date ranges in your
validation callbacks.
.. code-block:: php
<?php
class Order
{
/**
* @PrePersist @PreUpdate
*/
public function validate()
{
if (!($this->plannedShipDate instanceof DateTime)) {
throw new ValidateException();
}
if ($this->plannedShipDate->format('U') < time()) {
throw new ValidateException();
}
if ($this->customer == null) {
throw new OrderRequiresCustomerException();
}
}
}
What is nice about lifecycle events is, you can also re-use the
methods at other places in your domain, for example in combination
with your form library. Additionally there is no limitation in the
number of methods you register on one particular event, i.e. you
can register multiple methods for validation in "PrePersist" or
"PreUpdate" or mix and share them in any combinations between those
two events.
There is no limit to what you can and can't validate in
"PrePersist" and "PreUpdate" as long as you don't create new entity
instances. This was already discussed in the previous blog post on
the Versionable extension, which requires another type of event
called "onFlush".
Further readings: :ref:`reference-events-lifecycle-events`

View File

@@ -0,0 +1,197 @@
Working with DateTime Instances
===============================
There are many nitty gritty details when working with PHPs DateTime instances. You have to know their inner
workings pretty well not to make mistakes with date handling. This cookbook entry holds several
interesting pieces of information on how to work with PHP DateTime instances in Doctrine 2.
DateTime changes are detected by Reference
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When calling ``EntityManager#flush()`` Doctrine computes the changesets of all the currently managed entities
and saves the differences to the database. In case of object properties (@Column(type="datetime") or @Column(type="object"))
these comparisons are always made **BY REFERENCE**. That means the following change will **NOT** be saved into the database:
.. code-block:: php
<?php
/** @Entity */
class Article
{
/** @Column(type="datetime") */
private $updated;
public function setUpdated()
{
// will NOT be saved in the database
$this->updated->modify("now");
}
}
The way to go would be:
.. code-block:: php
<?php
class Article
{
public function setUpdated()
{
// WILL be saved in the database
$this->updated = new \DateTime("now");
}
}
Default Timezone Gotcha
~~~~~~~~~~~~~~~~~~~~~~~
By default Doctrine assumes that you are working with a default timezone. Each DateTime instance that
is created by Doctrine will be assigned the timezone that is currently the default, either through
the ``date.timezone`` ini setting or by calling ``date_default_timezone_set()``.
This is very important to handle correctly if your application runs on different servers or is moved from one to another server
(with different timezone settings). You have to make sure that the timezone is the correct one
on all this systems.
Handling different Timezones with the DateTime Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you first come across the requirement to save different timezones you may be still optimistic about how
to manage this mess,
however let me crush your expectations fast. There is not a single database out there (supported by Doctrine 2)
that supports timezones correctly. Correctly here means that you can cover all the use-cases that
can come up with timezones. If you don't believe me you should read up on `Storing DateTime
in Databases <http://derickrethans.nl/storing-date-time-in-database.html>`_.
The problem is simple. Not a single database vendor saves the timezone, only the differences to UTC.
However with frequent daylight saving and political timezone changes you can have a UTC offset that moves
in different offset directions depending on the real location.
The solution for this dilemma is simple. Don't use timezones with DateTime and Doctrine 2. However there is a workaround
that even allows correct date-time handling with timezones:
1. Always convert any DateTime instance to UTC.
2. Only set Timezones for displaying purposes
3. Save the Timezone in the Entity for persistence.
Say we have an application for an international postal company and employees insert events regarding postal-package
around the world, in their current timezones. To determine the exact time an event occurred means to save both
the UTC time at the time of the booking and the timezone the event happened in.
.. code-block:: php
<?php
namespace DoctrineExtensions\DBAL\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\DateTimeType;
class UTCDateTimeType extends DateTimeType
{
/**
* @var \DateTimeZone
*/
private static $utc;
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof \DateTime) {
$value->setTimezone(self::getUtc());
}
return parent::convertToDatabaseValue($value, $platform);
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if (null === $value || $value instanceof \DateTime) {
return $value;
}
$converted = \DateTime::createFromFormat(
$platform->getDateTimeFormatString(),
$value,
self::getUtc()
);
if (! $converted) {
throw ConversionException::conversionFailedFormat(
$value,
$this->getName(),
$platform->getDateTimeFormatString()
);
}
return $converted;
}
private static function getUtc(): \DateTimeZone
{
return self::$utc ?: self::$utc = new \DateTimeZone('UTC');
}
}
This database type makes sure that every DateTime instance is always saved in UTC, relative
to the current timezone that the passed DateTime instance has.
To actually use this new type instead of the default ``datetime`` type, you need to run following
code before bootstrapping the ORM:
.. code-block:: php
<?php
use Doctrine\DBAL\Types\Type;
use DoctrineExtensions\DBAL\Types\UTCDateTimeType;
Type::overrideType('datetime', UTCDateTimeType::class);
Type::overrideType('datetimetz', UTCDateTimeType::class);
To be able to transform these values
back into their real timezone you have to save the timezone in a separate field of the entity
requiring timezoned datetimes:
.. code-block:: php
<?php
namespace Shipping;
/**
* @Entity
*/
class Event
{
/** @Column(type="datetime") */
private $created;
/** @Column(type="string") */
private $timezone;
/**
* @var bool
*/
private $localized = false;
public function __construct(\DateTime $createDate)
{
$this->localized = true;
$this->created = $createDate;
$this->timezone = $createDate->getTimeZone()->getName();
}
public function getCreated()
{
if (!$this->localized) {
$this->created->setTimeZone(new \DateTimeZone($this->timezone));
}
return $this->created;
}
}
This snippet makes use of the previously discussed "changeset by reference only" property of
objects. That means a new DateTime will only be used during updating if the reference
changes between retrieval and flush operation. This means we can easily go and modify
the instance by setting the previous local timezone.