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 clone
s 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.