April 25th, 2014 · php zend framework 2 doctrine orm

Doctrine collections and nested ZF2 fieldset collections

On a recent project, which uses a ZF2 Form+Fieldset+InputFilter confab to hydrate a matching Doctrine ORM object model, I encountered something new: collection instances get shared between collections when using collections inside of collections (Yes, you read that right :P)

The Situation:

In our domain model, Competitions contain one or more Divisions, which themselves contain one or more Teams:

Competition <-- Division <-- Team

As implemented with ZF2 forms, the CompetitionFieldset contains a Zend\Form\Element\Collection element, which holds all the divisions of the competition as DivisionFieldset instances:

// Competition Fieldset
class CompetitionFieldset extends Fieldset
{
    public function init()
    {
        // snipped for brevity
        // $fsDivision instanceof DivisionFieldset

        $this->add(array(
            'type'    => 'Zend\Form\Element\Collection',
            'name'    => 'divisions',
            'options' => array(
                'target_element' => $fsDivision,
            )
        ));
    }
}

// Competition Entity
class CompetitionEntity
{
    protected $divisions;

    public function __construct()
    {
        $this->divisions = new ArrayCollection();
    }
}

The Division fieldset itself contains a Collection element holding all of the teams assigned to the division, as instances of TeamFieldset

// Division fieldset
class DivisionFieldset extends Fieldset
{
    public function init()
    {
        // snipped for brevity
        // $fsTeam instanceof TeamFieldset

        $this->add(array(
            'type'    => 'Zend\Form\Element\Collection',
            'name'    => 'teams',
            'options' => array(
                'target_element' => $fsTeam,
            )
        ));
    }
}

// Division entity
class DivisionEntity
{
    protected $teams;

    public function __construct()
    {
        $this->teams = new ArrayCollection();
    }
}

Once everything is collected together into a form and the DoctrineObject hydrators and prototype entity instances are all assigned to the form and it's respective fieldsets, an issue popped up:

When I add multiple divisions to the competition, assign some teams to each one, submit the form and wait for Zend\Form and Doctrine to do their turn-that-POST-nonsense-into-a-PHP-object thing, it goes sideways. All the divisions I added are properly represented in the CompetitionEntity::divisions collection as distinct DivisionEntity instances, but they all have the same set of TeamEntity objects in their teams collection. Indeed, all the DivisionEntity objects have the same ArrayCollection. Why?

The answer is, in hindsight, simple: Zend\Form\Element\Collection takes the prototype object we gave it's fieldset and clones it for each element of the divisions array in the POST data. However, PHP doesn't deep-clone, and so all those clones of DivisionEntity share the same internal instance of ArrayCollection that was assigned to teams in the prototype object. To prevent this we have to give each clone it's own ArrayCollection instance by adding a __clone method to the class:

class DivisionEntity
{
    protected $teams;

    public function __construct()
    {
        $this->teams = new ArrayCollection();
    }

    public function __clone()
    {
        $this->teams = new ArrayCollection();
    }
}

And Bob's your uncle, all is well once again in ZF2+Doctrine land :)

Summary & TL;DR: if the object you bind to a fieldset placed in an Zend\Form\Element\Collection element composes other objects, you need to add an __clone method to give each clone their own instance of each associated object, as in the DivisionEntity example above.