Description
Hey Symfony guys,
After upgrading Doctrine from 2.4 to 2.5, I've been facing a new problem with my collections which were not working well.
The entity association is simple : OneToMany; in my case one User linked to many Skills.
At first sight, this message appeared " 'spl_object_hash() expects parameter 1 to be object, null given' " with no reason.
Here is the stack trace :
in vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php at line 2445 -
*/
public function cancelOrphanRemoval($entity)
{
unset($this->orphanRemovals[spl_object_hash($entity)]);
}
/**
at ErrorHandler ->handleError ('2', 'spl_object_hash() expects parameter 1 to be object, null given', '/home/kevin/Dev/Yogosha/Web/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php', '2445', array('entity' => null))
at spl_object_hash (null)
in vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php at line 2445 +
at UnitOfWork ->cancelOrphanRemoval (null)
in vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php at line 475 +
at PersistentCollection ->set ('9', null)
in vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php at line 522 +
at PersistentCollection ->offsetSet ('9', null)
in vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php at line 226 +
at PropertyAccessor ->readPropertiesUntil (object(PersistentCollection), object(PropertyPath), '1', true)
in vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php at line 58 +
at PropertyAccessor ->getValue (object(PersistentCollection), object(PropertyPath))
in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php at line 57 +
at PropertyPathMapper ->mapDataToForms (object(PersistentCollection), object(RecursiveIteratorIterator))
in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 921 +
at Form ->add ('9', object(RatedSkillType), array('property_path' => '[9]', 'block_name' => 'entry'))
in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php at line 128 +
at ResizeFormListener ->preSubmit (object(FormEvent), 'form.pre_bind', object(EventDispatcher))
at call_user_func (array(object(ResizeFormListener), 'preSubmit'), object(FormEvent), 'form.pre_bind', object(EventDispatcher))
in app/cache/dev/classes.php at line 1791 +
at EventDispatcher ->doDispatch (array(array(object(BindRequestListener), 'preBind'), array(object(TrimListener), 'preSubmit'), array(object(CsrfValidationListener), 'preSubmit'), array(object(ResizeFormListener), 'preSubmit')), 'form.pre_bind', object(FormEvent))
in app/cache/dev/classes.php at line 1724 +
at EventDispatcher ->dispatch ('form.pre_bind', object(FormEvent))
in vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/ImmutableEventDispatcher.php at line 43 +
at ImmutableEventDispatcher ->dispatch ('form.pre_bind', object(FormEvent))
in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 551 +
After looking at the Doctrine and Symfony code, I think I've spotted the problem.
If my understandings are good, when working with collections, Symfony has to prepare the field of the data_class object to receive potential new elements. This preparation is done at PRE_BIND state by the method "readPropertiesUntil" of PropertyAccessor class (line 191) by initializing new column with "null" value.
// Create missing nested arrays on demand
if ($isIndex &&
(
($objectOrArray instanceof \ArrayAccess && !isset($objectOrArray[$property])) ||
(is_array($objectOrArray) && !array_key_exists($property, $objectOrArray))
)
) {
if (!$ignoreInvalidIndices) {
if (!is_array($objectOrArray)) {
if (!$objectOrArray instanceof \Traversable) {
throw new NoSuchIndexException(sprintf(
'Cannot read index "%s" while trying to traverse path "%s".',
$property,
(string) $propertyPath
));
}
$objectOrArray = iterator_to_array($objectOrArray);
}
throw new NoSuchIndexException(sprintf(
'Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".',
$property,
(string) $propertyPath,
print_r(array_keys($objectOrArray), true)
));
}
$objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null;
}
As we can see, the missing columns in the array are added thanks to this line :
$objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null;
As Doctrine uses ArrayAccess implementation for the PersistentCollection, the use of $objectOrArray[$property]
is equivalent to a call to the offsetSet
method located at line 522 in the PersistentCollection class.
public function offsetSet($offset, $value)
{
if ( ! isset($offset)) {
return $this->add($value);
}
return $this->set($offset, $value);
}
Then a call to the set
method is issued :
public function set($key, $value)
{
parent::set($key, $value);
$this->changed();
if ($this->em && $value != null) {
$this->em->getUnitOfWork()->cancelOrphanRemoval($value);
}
}
And in my opinion, the problem comes here.
In Doctrine 2.4, the set
method was :
public function set($key, $value)
{
$this->initialize();
$this->coll->set($key, $value);
$this->changed();
}
Between these two versions, we can see the introduction of this piece of code :
if ($this->em) {
$this->em->getUnitOfWork()->cancelOrphanRemoval($value);
}
And cancelOrphanRemoval
results in :
public function cancelOrphanRemoval($entity)
{
unset($this->orphanRemovals[spl_object_hash($entity)]);
}
So, when the PropertyAccessor initializes the object to prepare it to receive the new value which will be inserted during the write phase, it issues unset($this->orphanRemovals[spl_object_hash(null)])
indirectly through the cancelOrphanRemoval method. That's the reason why the exception is produced. This call was not issued in Doctrine 2.4 which explains why there was no problem.
As a quick workaround, I've used this
if ($this->em && $value != null) {
$this->em->getUnitOfWork()->cancelOrphanRemoval($value);
}
because I don't know if it's the role of Doctrine to restrict null values or the role of Symfony to initialize the PersistentCollection in another way.
Sorry for this very long bugreport,
Keen