views:

106

answers:

2

Just starting to work with Doctrine2, and am wondering how/if I can use a custom collection class. Searches point me to this part of the documentation:

Collection-valued persistent fields and properties must be defined in terms of the Doctrine\Common\Collections\Collection interface. The collection implementation type may be used by the application to initialize fields or properties before the entity is made persistent. Once the entity becomes managed (or detached), subsequent access must be through the interface type.

While I'm sure that's crystal clear to someone, I'm a little fuzzy on it.

If I setup my Entity to initialize (say in __construct()) the collection variable to a class that implements the correct interface - will Doctrine2 continue to use that class as the collection? Am I understanding that correctly?

Update: Also, I gather from various threads that the placeholder object used in lazy loading may influence how a custom collection can be used.

+1  A: 

No, whenever Doctrine gives you back an implementation of the Doctrine\Common\Collections\Collection interface, it will be a Doctrine\ORM\PersistentCollection instance. You cannot put more custom logic on the collection. However that is not even necessary.

Say you have an entity (Order has many OrderItems), then a method to calculate the total sum of the order should not be located on the collection, but on the Order item. Since that is the location where the sum has meaning in your domain model:

class Order
{
    private $items;

    public function getTotalSum()
    {
        $total = 0;
        foreach ($this->items AS $item) {
            $total += $item->getSum();
        }
        return $total;
    }
}

Collections however are only technical parts of the ORM, they help to implement and manage references between objects, nothing more.

beberlei
My primary thought was to provide order/sorting/filtering, not running calculations on the entities.
Tim Lytle
Even sorting/filtering methods are best put on the entities that hold the collection. However there are also methods that accept closures and do the work for you, for example: $collection->map(function($element) { return $element->getProperty(); });
beberlei
@beberlei But if multiple entities have the same collection, you end up duplicating code (unless you use anonymous functions).
Tim Lytle
I agree with @Tim. Being able to work with custom collections would be great. There are plenty of reasons that a collection of one type of object might have a set of behaviors that a single instance of that object may not. A very common feature in strongly typed languages.
Bryan M.
There are other OO ways to avoid duplicate code than extending objects. Composition (A helper object) would surely suffice for this. Additionally Collections are not to be confused with lists of objects of one type. Collections only have meaning in the reference between two entities. DQL or Repository Finder methods however will always return arrays of objects.
beberlei
+3  A: 

Let me try to clarify what is possible, not possible and planned with examples.

The quote from the manual basically means you could have the following custom implementation type:

use Doctrine\Common\Collections\Collection;

// MyCollection is the "implementation type"
class MyCollection implements Collection {
    // ... interface implementation

    // This is not on the Collection interface
    public function myCustomMethod() { ... }
}

Now you could use it as follows:

class MyEntity {
    private $items;
    public function __construct() {
        $this->items = new MyCollection;
    }
    // ... accessors/mutators ...
}

$e = new MyEntity;
$e->getItems()->add(new Item);
$e->getItems()->add(new Item);
$e->getItems()->myCustomMethod(); // calling method on implementation type

// $em instanceof EntityManager
$em->persist($e);

// from now on $e->getItems() may only be used through the interface type

In other words, as long as an entity is NEW (not MANAGED, DETACHED or REMOVED) you are free to use the concrete implementation type of collections, even if its not pretty. If it is not NEW, you must access only the interface type (and ideally type-hint on it). That means the implementation type does not really matter. When a persistent MyEntity instance is retrieved from the database, it will not use a MyCollection (constructors are not invoked by Doctrine, ever, since Doctrine only reconstitutes already existing/persistent objects, it never creates "new" ones). And since such an entity is MANAGED, access must happen through the interface type anyways.

Now to what is planned. The more beautiful way to have custom collections is to also have a custom interface type, say IMyCollection and MyCollection as the implementation type. Then, to make it work perfectly with the Doctrine 2 persistence services you would need to implement a custom PersistentCollection implementation, say, MyPersistentCollection which looks like this:

class MyPersistentCollection implements IMyCollection {
    // ...
}

Then you would tell Doctrine in the mapping to use the MyPersistentCollection wrapper for that collection (remember, a PersistentCollection wraps a collection implementation type, implementing the same interface, so that it can do all the persistence work before/after delegating to the underlying collection implementation type).

So a custom collection implementation would consist of 3 parts:

  1. Interface type
  2. Implementation type (implements interface type)
  3. Persistent wrapper type (implements interface type)

This will not only make it possible to write custom collections that work seemlessly with the Doctrine 2 ORM but also to write only a custom persistent wrapper type, for example to optimise the lazy-loading/initialization behavior of a particular collection to specific application needs.

It is not yet possible to do this but it will be. This is the only really elegant and fully functional way to write and use completely custom collections that integrate flawlessly in the transparent persistence scheme provided by Doctrine 2.

Did not know this was planned. That's pretty sweet.
Bryan M.