Search code examples
doctrine-ormzend-framework3self-referencepersistself-referencing-table

Doctrine 2 ZF3 self referencing entity is not updating all fields of parent


I have a self referencing entity with parent and children. The strange thing is, when I add the form elements (DoctrineModule ObjectSelect) for parent and children to the form, some other fields of the parent entity doesn't update, when I persist the entity. The child entities are updating fine.

In the update query of the parent aren't these fields which I want to update. It's like doctrine doesn't recognize the changes for the parent (owning side) anymore. Before the persist I get the right entity with the actual changes from the form, but than doctrine doesn't update the changed fields in the query.

When I delete the form elements for parent and children in the form, everything works fine and the parent entity update/persist all fields.

/**
 * @var string
 *
 * @Gedmo\Translatable
 * @ORM\Column(type="text")
 */
private $teaser;

/**
 * @var string
 *
 * @Gedmo\Translatable
 * @ORM\Column(type="text")
 */

private $description;

/**
 * One Category has Many Categories.
 * @var Collection
 * @ORM\OneToMany(targetEntity="Rental\Entity\Rental", mappedBy="parent")
 */
private $children;

/**
 * Many Categories have One Category.
 * @ORM\ManyToOne(targetEntity="Rental\Entity\Rental", inversedBy="children")
 * @ORM\JoinColumn(name="parent_id", referencedColumnName="houses_id", nullable=true, onDelete="SET NULL")
 */
private $parent;

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

/**
 * Get teaser
 *
 * @return string
 */
public function getTeaser()
{
    return $this->teaser;
}

/**
 * Set teaser
 *
 * @param string $teaser
 */
public function setTeaser($teaser)
{
    $this->teaser = $teaser;
}

/**
 * Get description
 *
 * @return string
 */
public function getDescription()
{
    return $this->description;
}

/**
 * Set description
 *
 * @param string $description
 */
public function setDescription($description)
{
    $this->description = $description;
}

/**
 * Add $child
 * @param Collection $children
 */
public function addChildren(Collection $children)
{
    foreach ($children as $child) {
        $this->addChild($child);
    }
}


/**
 * @param Rental $child
 * @return void
 */
public function addChild(Rental $child)
{
    if ($this->children->contains($child)) {
        return;
    }
    $child->setParent($this);
    $this->children[] = $child;
}

/**
 * Remove children
 * @param Rental $children
 */
public function removeChildren(Collection $children)
{
    foreach ($children as $child) {
        $this->removeChild($child);
    }
}

/**
 * Remove child.
 *
 * @param \Rental\Entity\Rental $child
 *
 * @return boolean TRUE if this collection contained the specified element, FALSE otherwise.
 */
public function removeChild(Rental $child)
{
    return $this->children->removeElement($child);
}

/**
 * Get children.
 *
 * @return \Doctrine\Common\Collections\Collection
 */
public function getChildren()
{
    return $this->children;
}

/**
 * Set parent.
 *
 * @param \Rental\Entity\Rental|null $parent
 */
public function setParent(Rental $parent = null)
{
    $this->parent = $parent;
}

/**
 * Get parent.
 *
 * @return \Rental\Entity\Rental|null
 */
public function getParent()
{
    return $this->parent;
}

Solution

  • Answer based (and might still change) based on discussion in comments with OP under question.


    Simple use case: self-referencing Doctrine Entity does not correctly update parent/child related Entity object properties on on-save action.


    OP provided Entity.

    I'm assuming you have a Form setup for this Entity. I'm also assuming it's correctly setup, as you did not provide it after being asked (because, rightly so, it would mean a lot of code). However, assuming stuff with code makes for a lot of mistakes, which is why I mention it here.

    As such, I'm assuming the handling of your Form in the Controller might be faulty. To check, please use the following simplified addAction function and give it a shot (Factory code below).

    /**
     * @var RentalForm
     */
    protected $form;
    
    /**
     * @var ObjectManager
     */
    protected $objectManager;
    
    public function __construct(ObjectManager $objectManager, RentalForm $form)
    {
        $this->form = $form;
        $this->objectManager = $objectManager;
    }
    
    public function addAction()
    {
        /** @var RentalForm $form */
        $form = $this->getForm();
    
        /** @var Request $request */
        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());
    
            if ($form->isValid()) {
                $entity = $form->getObject();
    
                $this->getObjectManager()->persist($entity);
    
                try {
                    $this->getObjectManager()->flush();
                } catch (\Exception $e) {
                    $message = sprintf(
                        'Was unable to save the data. Saving threw an error. <br />Code: %s. <br />Message: %s',
                        $e->getCode(),
                        $e->getMessage()
                    );
    
                    $this->flashMessenger()->addErrorMessage($message);
    
                    return [
                        'form' => $form,
                        'validationMessages' => $form->getMessages() ?: '',
                    ];
                }
    
                $this->flashMessenger()->addSuccessMessage(
                    $this->getTranslator()->translate('Successfully created object.')
                );
    
                // TODO replace vars with your own: return $this->redirect()->route($route, $routeParams);
            }
    
            $this->flashMessenger()->addWarningMessage(
                'Your form contains errors. Please correct them and try again.'
            );
        }
    
        return [
            'form' => $form,
            'validationMessages' => $form->getMessages() ?: '',
        ];
    }
    

    Factory for class with the above, modify as needed for you situation

    class RentalControllerFactory implements FactoryInterface
    {
        public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
        {
            /** @var ObjectManager $objectManager */
            $objectManager = $container->get(EntityManager::class);
    
            /** @var FormElementManagerV3Polyfill $formElementManager */
            $formElementManager = $container->get('FormElementManager');
    
            /** @var RentalForm $form */
            $form = $formElementManager->get(RentalForm::class);
    
            return new RentalController($objectManager, $form);
        }
    }