DDC-3947: Issue in collections with Doctrine 2.5 & Symfony 2.6 #4827

Open
opened 2026-01-22 14:50:03 +01:00 by admin · 1 comment
Owner

Originally created by @doctrinebot on GitHub (Oct 10, 2015).

Originally assigned to: @beberlei on GitHub.

Jira issue originally created by user Keen:

Hey Doctrine guys,

Here is a bug report I've made a few weeks ago to Symfony project but with no response link https://github.com/symfony/symfony/issues/15797. I don't really know which team should correct the issue so I share it with you too.

After upgrading Doctrine from 2.4 to 2.5, I've been facing a new problem with my collections in Symfony which were not working well when I tried to add/remove an item from my collection.
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  <ins>
at UnitOfWork ->cancelOrphanRemoval (null)
in vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php at line 475  </ins>
at PersistentCollection ->set ('9', null)
in vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php at line 522  <ins>
at PersistentCollection ->offsetSet ('9', null)
in vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php at line 226  </ins>
at PropertyAccessor ->readPropertiesUntil (object(PersistentCollection), object(PropertyPath), '1', true)
in vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php at line 58  <ins>
at PropertyAccessor ->getValue (object(PersistentCollection), object(PropertyPath))
in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php at line 57  </ins>
at PropertyPathMapper ->mapDataToForms (object(PersistentCollection), object(RecursiveIteratorIterator))
in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 921  <ins>
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  </ins>
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  <ins>
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  </ins>
at EventDispatcher ->dispatch ('form.pre_bind', object(FormEvent))
in vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/ImmutableEventDispatcher.php at line 43  <ins>
at ImmutableEventDispatcher ->dispatch ('form.pre_bind', object(FormEvent))
in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 551  </ins>

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 <ins> 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

Originally created by @doctrinebot on GitHub (Oct 10, 2015). Originally assigned to: @beberlei on GitHub. Jira issue originally created by user Keen: Hey Doctrine guys, Here is a bug report I've made a few weeks ago to Symfony project but with no response [link https://github.com/symfony/symfony/issues/15797](https://github.com/symfony/symfony/issues/15797). I don't really know which team should correct the issue so I share it with you too. After upgrading Doctrine from 2.4 to 2.5, I've been facing a new problem with my collections in Symfony which were not working well when I tried to add/remove an item from my collection. 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 <ins> at UnitOfWork ->cancelOrphanRemoval (null) in vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php at line 475 </ins> at PersistentCollection ->set ('9', null) in vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php at line 522 <ins> at PersistentCollection ->offsetSet ('9', null) in vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php at line 226 </ins> at PropertyAccessor ->readPropertiesUntil (object(PersistentCollection), object(PropertyPath), '1', true) in vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php at line 58 <ins> at PropertyAccessor ->getValue (object(PersistentCollection), object(PropertyPath)) in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php at line 57 </ins> at PropertyPathMapper ->mapDataToForms (object(PersistentCollection), object(RecursiveIteratorIterator)) in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 921 <ins> 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 </ins> 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 <ins> 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 </ins> at EventDispatcher ->dispatch ('form.pre_bind', object(FormEvent)) in vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/ImmutableEventDispatcher.php at line 43 <ins> at ImmutableEventDispatcher ->dispatch ('form.pre_bind', object(FormEvent)) in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 551 </ins> ``` 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. ``` php // 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 <ins> 1 < $propertyPath->getLength() ? array() : null; } ``` As we can see, the missing columns in the array are added thanks to this line : $objectOrArray[$property] = $i </ins> 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. ``` php 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 : ``` php 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 : ``` php 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 : ``` php if ($this->em) { $this->em->getUnitOfWork()->cancelOrphanRemoval($value); } ``` And cancelOrphanRemoval results in : ``` php 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 ``` php 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
admin added the Bug label 2026-01-22 14:50:03 +01:00
Author
Owner

@beberlei commented on GitHub (Feb 16, 2020):

Tricky, not sure thta calling set with a $value === null should be allowed, for me its a problem with Symfony Forms. But adding the workaround also makes sense, from a temporary perspective.

@beberlei commented on GitHub (Feb 16, 2020): Tricky, not sure thta calling set with a `$value === null` should be allowed, for me its a problem with Symfony Forms. But adding the workaround also makes sense, from a temporary perspective.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#4827