Working with Associations ========================= Associations between entities are represented just like in regular object-oriented PHP code using references to other objects or collections of objects. Changes to associations in your code are not synchronized to the database directly, only when calling ``EntityManager#flush()``. There are other concepts you should know about when working with associations in Doctrine: - If an entity is removed from a collection, the association is removed, not the entity itself. A collection of entities always only represents the association to the containing entities, not the entity itself. - When a bidirectional assocation is updated, Doctrine only checks on one of both sides for these changes. This is called the :doc:`owning side ` of the association. - A property with a reference to many entities has to be instances of the ``Doctrine\Common\Collections\Collection`` interface. Association Example Entities ---------------------------- We will use a simple comment system with Users and Comments as entities to show examples of association management. See the PHP docblocks of each association in the following example for information about its type and if it's the owning or inverse side. .. code-block:: php commentsRead; } public function setFirstComment(Comment $c) { $this->firstComment = $c; } } The interaction code would then look like in the following snippet (``$em`` here is an instance of the EntityManager): .. code-block:: php find('User', $userId); // unidirectional many to many $comment = $em->find('Comment', $readCommentId); $user->getReadComments()->add($comment); $em->flush(); // unidirectional many to one $myFirstComment = new Comment(); $user->setFirstComment($myFirstComment); $em->persist($myFirstComment); $em->flush(); In the case of bi-directional associations you have to update the fields on both sides: .. code-block:: php commentsAuthored; } public function getFavoriteComments() { return $this->favorites; } } class Comment { // ... public function getUserFavorites() { return $this->userFavorites; } public function setAuthor(User $author = null) { $this->author = $author; } } // Many-to-Many $user->getFavorites()->add($favoriteComment); $favoriteComment->getUserFavorites()->add($user); $em->flush(); // Many-To-One / One-To-Many Bidirectional $newComment = new Comment(); $user->getAuthoredComments()->add($newComment); $newComment->setAuthor($user); $em->persist($newComment); $em->flush(); Notice how always both sides of the bidirectional association are updated. The previous unidirectional associations were simpler to handle. Removing Associations --------------------- Removing an association between two entities is similarly straight-forward. There are two strategies to do so, by key and by element. Here are some examples: .. code-block:: php getComments()->removeElement($comment); $comment->setAuthor(null); $user->getFavorites()->removeElement($comment); $comment->getUserFavorites()->removeElement($user); // Remove by Key $user->getComments()->remove($ithComment); $comment->setAuthor(null); You need to call ``$em->flush()`` to make persist these changes in the database permanently. Notice how both sides of the bidirectional association are always updated. Unidirectional associations are consequently simpler to handle. Also note that if you use type-hinting in your methods, i.e. ``setAddress(Address $address)``, PHP will only allow null values if ``null`` is set as default value. Otherwise setAddress(null) will fail for removing the association. If you insist on type-hinting a typical way to deal with this is to provide a special method, like ``removeAddress()``. This can also provide better encapsulation as it hides the internal meaning of not having an address. When working with collections, keep in mind that a Collection is essentially an ordered map (just like a PHP array). That is why the ``remove`` operation accepts an index/key. ``removeElement`` is a separate method that has O(n) complexity using ``array_search``, where n is the size of the map. .. note:: Since Doctrine always only looks at the owning side of a bidirectional association for updates, it is not necessary for write operations that an inverse collection of a bidirectional one-to-many or many-to-many association is updated. This knowledge can often be used to improve performance by avoiding the loading of the inverse collection. You can also clear the contents of a whole collection using the ``Collections::clear()`` method. You should be aware that using this method can lead to a straight and optimized database delete or update call during the flush operation that is not aware of entities that have been re-added to the collection. Say you clear a collection of tags by calling ``$post->getTags()->clear();`` and then call ``$post->getTags()->add($tag)``. This will not recognize the tag having already been added previously and will consequently issue two separate database calls. Association Management Methods ------------------------------ It is generally a good idea to encapsulate proper association management inside the entity classes. This makes it easier to use the class correctly and can encapsulate details about how the association is maintained. The following code shows updates to the previous User and Comment example that encapsulate much of the association management code: .. code-block:: php commentsRead[] = $comment; } public function addComment(Comment $comment) { if (count($this->commentsAuthored) == 0) { $this->setFirstComment($comment); } $this->comments[] = $comment; $comment->setAuthor($this); } private function setFirstComment(Comment $c) { $this->firstComment = $c; } public function addFavorite(Comment $comment) { $this->favorites->add($comment); $comment->addUserFavorite($this); } public function removeFavorite(Comment $comment) { $this->favorites->removeElement($comment); $comment->removeUserFavorite($this); } } class Comment { // .. public function addUserFavorite(User $user) { $this->userFavorites[] = $user; } public function removeUserFavorite(User $user) { $this->userFavorites->removeElement($user); } } You will notice that ``addUserFavorite`` and ``removeUserFavorite`` do not call ``addFavorite`` and ``removeFavorite``, thus the bidirectional association is strictly-speaking still incomplete. However if you would naively add the ``addFavorite`` in ``addUserFavorite``, you end up with an infinite loop, so more work is needed. As you can see, proper bidirectional association management in plain OOP is a non-trivial task and encapsulating all the details inside the classes can be challenging. .. note:: If you want to make sure that your collections are perfectly encapsulated you should not return them from a ``getCollectionName()`` method directly, but call ``$collection->toArray()``. This way a client programmer for the entity cannot circumvent the logic you implement on your entity for association management. For example: .. code-block:: php commentsRead->toArray(); } } This will however always initialize the collection, with all the performance penalties given the size. In some scenarios of large collections it might even be a good idea to completely hide the read access behind methods on the EntityRepository. There is no single, best way for association management. It greatly depends on the requirements of your concrete domain model as well as your preferences. Synchronizing Bidirectional Collections --------------------------------------- In the case of Many-To-Many associations you as the developer have the responsibility of keeping the collections on the owning and inverse side in sync when you apply changes to them. Doctrine can only guarantee a consistent state for the hydration, not for your client code. Using the User-Comment entities from above, a very simple example can show the possible caveats you can encounter: .. code-block:: php getFavorites()->add($favoriteComment); // not calling $favoriteComment->getUserFavorites()->add($user); $user->getFavorites()->contains($favoriteComment); // TRUE $favoriteComment->getUserFavorites()->contains($user); // FALSE There are two approaches to handle this problem in your code: 1. Ignore updating the inverse side of bidirectional collections, BUT never read from them in requests that changed their state. In the next Request Doctrine hydrates the consistent collection state again. 2. Always keep the bidirectional collections in sync through association management methods. Reads of the Collections directly after changes are consistent then. Transitive persistence / Cascade Operations ------------------------------------------- When working with many associated entities, you sometimes don't want to "expose" all entities to your PHP application. Therefore Doctrine 2 provides a mechanism for transitive persistence through cascading of certain operations. Each association to another entity or a collection of entities can be configured to automatically cascade the following operations to the associated entities: ``persist``, ``remove``, ``merge``, ``detach``, ``refresh`` or ``all``. .. note:: Cascade operations are performed in memory. That means collections and related entities are fetched into memory, even if they are still marked as lazy when the cascade operation is about to be performed. However this approach allows entity lifecycle events to be performed for each of these operations. However, pulling objects graph into memory on cascade can cause considerable performance overhead, especially when cascading collections are large. Makes sure to weigh the benefits and downsides of each cascade operation that you define. To rely on the database level cascade operations for the delete operation instead, you can configure each join column with the **onDelete** option. See the respective mapping driver chapters for more information. The following example is an extension to the User-Comment example of this chapter. Suppose in our application a user is created whenever they write their first comment. In this case we would use the following code: .. code-block:: php addComment($myFirstComment); $em->persist($user); $em->persist($myFirstComment); // required, if `cascade: persist` isn't set $em->flush(); Even if you *persist* a new User that contains our new Comment this code requires an explicit call to ``EntityManager#persist($myFirstComment)``. Doctrine 2 does not cascade the persist operation to all nested entities that are new as well. More complicated is the deletion of all user's comments when the user is removed from the system: .. code-block:: php find('User', $deleteUserId); foreach ($user->getAuthoredComments() as $comment) { $em->remove($comment); } $em->remove($user); $em->flush(); Without the loop over all the authored comments Doctrine would use an UPDATE statement only to set the foreign key to NULL and only the User would be deleted from the database during the flush()-Operation. To have Doctrine handle both cases automatically we can change the ``User#commentsAuthored`` property to cascade both the "persist" and the "remove" operation. .. code-block:: php addComment($myFirstComment); $myFirstComment->setUser($user); $em->persist($user); $em->flush(); .. note:: This code does not associate the newly created comment with the user. To achieve this, you *always* have to call ``$myFirstComment->setUser($user);`` – no matter if ``cascade: persist`` is set or not. Even though automatic cascading is convenient, it should be used with care. Do not blindly apply ``cascade=all`` to all associations as it will unnecessarily degrade the performance of your application. For each cascade operation that gets activated Doctrine also applies that operation to the association, be it single or collection valued. Persistence by Reachability: Cascade Persist ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are additional semantics that apply to the Cascade Persist operation. During each flush() operation Doctrine detects if there are new entities in any collection and three possible cases can happen: 1. New entities in a collection marked as cascade persist will be directly persisted by Doctrine. 2. New entities in a collection not marked as cascade persist will produce an Exception and rollback the flush() operation. 3. Collections without new entities are skipped. This concept is called Persistence by Reachability: New entities that are found on already managed entities are automatically persisted as long as the association is defined as cascade persist. Orphan Removal -------------- There is another concept of cascading that is relevant only when removing entities from collections. If an Entity of type ``A`` contains references to privately owned Entities ``B`` then if the reference from ``A`` to ``B`` is removed the entity ``B`` should also be removed, because it is not used anymore. OrphanRemoval works with one-to-one, one-to-many and many-to-many associations. .. note:: When using the ``orphanRemoval=true`` option Doctrine makes the assumption that the entities are privately owned and will **NOT** be reused by other entities. If you neglect this assumption your entities will get deleted by Doctrine even if you assigned the orphaned entity to another one. As a better example consider an Addressbook application where you have Contacts, Addresses and StandingData: .. code-block:: php addresses = new ArrayCollection(); } public function newStandingData(StandingData $sd) { $this->standingData = $sd; } public function removeAddress($pos) { unset($this->addresses[$pos]); } } Now two examples of what happens when you remove the references: .. code-block:: php find("Addressbook\Contact", $contactId); $contact->newStandingData(new StandingData("Firstname", "Lastname", "Street")); $contact->removeAddress(1); $em->flush(); In this case you have not only changed the ``Contact`` entity itself but you have also removed the references for standing data and as well as one address reference. When flush is called not only are the references removed but both the old standing data and the one address entity are also deleted from the database. Filtering Collections --------------------- .. filtering-collections: Collections have a filtering API that allows to slice parts of data from a collection. If the collection has not been loaded from the database yet, the filtering API can work on the SQL level to make optimized access to large collections. .. code-block:: php find('Group', $groupId); $userCollection = $group->getUsers(); $criteria = Criteria::create() ->where(Criteria::expr()->eq("birthday", "1982-02-17")) ->orderBy(array("username" => Criteria::ASC)) ->setFirstResult(0) ->setMaxResults(20) ; $birthdayUsers = $userCollection->matching($criteria); .. tip:: You can move the access of slices of collections into dedicated methods of an entity. For example ``Group#getTodaysBirthdayUsers()``. The Criteria has a limited matching language that works both on the SQL and on the PHP collection level. This means you can use collection matching interchangeably, independent of in-memory or sql-backed collections. .. code-block:: php