doctrine silently fails to delete entities on M side of 1:M

107 views Asked by At

I am beginning to suspect that this doesn't work because in my use case it just doesn't -- as opposed to me missing something -- but I have to consult your expertise to make sure, and to see if anyone can suggesta workaround.

I have a Many-to-Many situation that I am implementing with an association class, so we have one-to-many/many-to-one associations between the 3 participating classes. There is an Interpreter entity representing a person, and a Language entity representing a spoken language (actually a working language pair, but one half of the pair is understood to be English in this anglocentric application). An Interpreter can have multiple languages, and a Language is among the working languages of multiple interpreters. We need to manage other attributes of the interpreter-language, hence the InterpreterLanguage class.

When I call $interpreter->removeInterpreterLanguage($interpreterLanguage); followed by $entityManager->flush(), the in-memory Interpreter entity has one fewer elements in its $interpreterLanguages collection as you would expect, and there is no error or Exception thrown, but in the database here's what happens: nothing.

I have tried this in an MVC context, with ZendFramework 3 and a bound Zend\Form\Form with fieldsets, and when that drove me nuts I wrote a CLI script to try to examine the problem -- same result. Maybe it's worth noting that for updating scalar properties it's working fine.

I apologize for not including a link to the discussion of this issue that I read earlier -- can't find it now, for some reason. But I recall someone saying that it just doesn't work because Doctrine sees the M:1 on the other side, and therefore won't delete, and you have to say $entityManager->remove($object) to get it done. My experimental CLI script appears to confirm this. Nevertheless, I'd like to rule out the possibility that I am doing something wrong.

Any ideas? Suggestions for solving?

So here's my Language entity:

/** module/InterpretersOffice/src/Entity/Language.php */

namespace InterpretersOffice\Entity;

use Doctrine\ORM\Mapping as ORM;
use Zend\Form\Annotation;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Entity class representing a language used by an Interpreter.
 *
 * @Annotation\Name("language")
 * @ORM\Entity(repositoryClass="InterpretersOffice\Entity\Repository\LanguageRepository")
 * @ORM\Table(name="languages",uniqueConstraints={@ORM\UniqueConstraint(name="unique_language",columns={"name"})})
 */
class Language
{
    /**
     * entity id.
     *     
     * @ORM\Id
     * @ORM\GeneratedValue @ORM\Column(type="smallint",options={"unsigned":true})
     */
    protected $id;

    /**
     * name of the language.
     *
     * @ORM\Column(type="string",length=50,nullable=false)
     *
     * @var string
     */
    protected $name;

    /**
     * comments.
     *
     * @ORM\Column(type="string",length=300,nullable=false,options={"default":""})
     *
     * @var string
     */
    protected $comments = '';

    /**
     *
     * @ORM\OneToMany(targetEntity="InterpreterLanguage",mappedBy="language")
     */
    protected $interpreterLanguages;

    /**
     * constructor
     */
    public function __construct()
    {
        $this->interpreterLanguages = new ArrayCollection();
    }
    // setters and getters omitted for brevity

}

Here is the Interpreter entity:

<?php
/** module/InterpretersOffice/src/Entity/Interpreter.php */

namespace InterpretersOffice\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

/**
 * Entity representing an Interpreter.
 *
 * @ORM\Entity(repositoryClass="InterpretersOffice\Entity\Repository\InterpreterRepository")
 * @ORM\Table(name="interpreters")
 */
class Interpreter extends Person
{
    /**
     * entity id.
     *
     * @ORM\Id @ORM\GeneratedValue @ORM\Column(type="smallint",options={"unsigned":true})
     */
    protected $id;

    /**
     * phone number.
     *
     * @ORM\Column(type="string",length=16,nullable=true)
     *
     * @var string
     */
    protected $phone;

    /**
     * date of birth.
     *
     * @ORM\Column(type="date",nullable=true)
     *
     * @var string
     */
    protected $dob;

    /**
     * working languages.
     *
     * @ORM\OneToMany(targetEntity="InterpreterLanguage",mappedBy="interpreter", cascade={"persist", "remove"})
     * 
     *
     * @var ArrayCollection of InterpreterLanguage
     */
    protected $interpreterLanguages;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->interpreterLanguages = new ArrayCollection();
    }

    // some boring setters and getters omitted.... 

    /**
     * Add interpreterLanguage.
     *
     * @param InterpreterLanguage $interpreterLanguage
     *
     * @return Interpreter
     */
    public function addInterpreterLanguage(InterpreterLanguage $interpreterLanguage)
    {
        $this->interpreterLanguages->add($interpreterLanguage);
        return $this;
    }

    /**
     * Remove interpreterLanguage.
     *
     * @param \InterpretersOffice\Entity\InterpreterLanguage $interpreterLanguage
     *
     * @return Interpreter
     */
    public function removeInterpreterLanguage(InterpreterLanguage $interpreterLanguage)
    {       

        $this->interpreterLanguages->removeElement($interpreterLanguage);
        //$interpreterLanguage->setInterpreter(null)->setLanguage(null);
        return $this;
    }

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

    /*
     because "AllowRemove strategy for DoctrineModule hydrator requires both addInterpreterLanguages and  removeInterpreterLanguages to be defined in InterpretersOffice\Entity\Interpreter entity domain code, but one or both 
     [seemed] to be missing"
     */


    public function addInterpreterLanguages(Collection $interpreterLanguages)
    {
        foreach ($interpreterLanguages as $interpreterLanguage) {

            $interpreterLanguage->setInterpreter($this);
            $this->interpreterLanguages->add($interpreterLanguage);
        }
    }

    public function removeInterpreterLanguages(Collection $interpreterLanguages)
    {
        foreach ($interpreterLanguages as $interpreterLanguage) {

            $this->interpreterLanguages->removeElement($interpreterLanguage);

        }
    }

}

and the association class:

/** module/InterpretersOffice/src/Entity/InterpreterLanguage.php  */

namespace InterpretersOffice\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Entity representing an Interpreter's Language.
 *
 * Technically, it is a language *pair*, but in this system it is understood that
 * the other language of the pair is English. There is a many-to-many relationship
 * between interpreters and languages. But because there is also metadata to record
 * about the language (federal certification), it is implemented as a Many-To-One
 * relationship on either side.
 *
 * @ORM\Entity
 * @ORM\Table(name="interpreters_languages")
 */
class InterpreterLanguage
{
    /**
     * constructor.
     *
     * @param Interpreter $interpreter
     * @param Language    $language
     *
     * @todo a lifecycle callback to ensure certified languages have a boolean
     * $federalCertification set
     */
    public function __construct(
        Interpreter $interpreter = null,
        Language $language = null
    ) {
        if ($interpreter) {
            $this->setInterpreter($interpreter);
        }
        if ($language) {
            $this->setLanguage($language);
        }
    }

    /**
     * The Interpreter who works in this language.
     *
     * @ORM\ManyToOne(targetEntity="Interpreter",inversedBy="interpreterLanguages")
     * @ORM\Id
     *
     * @var Interpreter
     */
    protected $interpreter;

    /**
     * The language in which this interpreter works.
     *
     * @ORM\ManyToOne(targetEntity="Language",inversedBy="interpreterLanguages")
     * @ORM\Id
     *
     * @var Language
     */
    protected $language;

    /**
     * Whether the Interpreter holds federal court interpreter certification in this language.
     *
     * The only certified languages in the US District Court system are Spanish,
     * Navajo and Haitian Creole. Of these, only the Spanish certification
     * program is active. This field should be a boolean for the certified
     * languages and null for everything else.
     *
     * @link http://www.uscourts.gov/services-forms/federal-court-interpreters/federal-court-interpreter-certification-examination the federal court certification program
     *
     * @ORM\Column(name="federal_certification",type="boolean",nullable=true)
     *
     * @var bool
     */
    protected $federalCertification;

    /**
     * Set interpreter.
     *
     * @param \InterpretersOffice\Entity\Interpreter $interpreter
     *
     * @return InterpreterLanguage
     */
    public function setInterpreter(Interpreter $interpreter = null)
    {
        $this->interpreter = $interpreter;

        return $this;
    }

    /**
     * Get interpreter.
     *
     * @return Interpreter
     */
    public function getInterpreter()
    {
        return $this->interpreter;
    }

    /**
     * Set language.
     *
     * @param Language $language
     *
     * @return InterpreterLanguage
     */
    public function setLanguage(Language $language = null)
    {
        $this->language = $language;

        return $this;
    }

    /**
     * Get language.
     *
     * @return Language
     */
    public function getLanguage()
    {
        return $this->language;
    }

    /**
     * Set federalCertification.
     *
     * @param bool $federalCertification
     *
     * @return InterpreterLanguage
     */
    public function setFederalCertification($federalCertification)
    {
        $this->federalCertification = $federalCertification;

        return $this;
    }

    /**
     * Get federalCertification.
     *
     * @return bool
     */
    public function getFederalCertification()
    {
        return $this->federalCertification;
    }
}

For brevity's sake, I will leave out the code for the Form and the Fieldset classes -- they do seem to be working fine (look tasteful, too. Thank you Bootstrap). I load the form, I remove one of the InterpreterLanguages and submit... Here's the controller action:

/**
 * updates an Interpreter entity.
 */
public function editAction()
{
    $viewModel = (new ViewModel())
            ->setTemplate('interpreters-office/admin/interpreters/form.phtml')
            ->setVariable('title', 'edit an interpreter');
    $id = $this->params()->fromRoute('id');

    $entity = $this->entityManager->find('InterpretersOffice\Entity\Interpreter', $id);
    if (!$entity) {
        return $viewModel->setVariables(['errorMessage' => "interpreter with id $id not found"]);
    }
    $form = new InterpreterForm($this->entityManager, ['action' => 'update']);
    $form->bind($entity);
    $viewModel->setVariables(['form' => $form, 'id' => $id ]);

    $request = $this->getRequest();
    if ($request->isPost()) {
        $form->setData($request->getPost());
        if (!$form->isValid()) {
            return $viewModel;
        }
        $this->entityManager->flush();
        $this->flashMessenger()
              ->addSuccessMessage(sprintf(
                  'The interpreter <strong>%s %s</strong> has been updated.',
                  $entity->getFirstname(),
                  $entity->getLastname()
              ));
        // dump the entity and see how it looksa after update
        echo "NOT redirecting. entity:<pre>";
        \Doctrine\Common\Util\Debug::dump($entity); echo "</pre>";
        //$this->redirect()->toRoute('interpreters');
    } else { 
        // dump the entity fresh from the database
        echo "loaded:<pre> "; \Doctrine\Common\Util\Debug::dump($entity);echo "</pre>";}

    return $viewModel;
}

Again, the data looks right as it's dumped to the screen, but you reload the form and the collection has as many elements as it did before.

Thanks!

1

There are 1 answers

1
David On BEST ANSWER

In Interpreter.php, orphanRemoval=true !!

/**
 * working languages.
 *
 * @ORM\OneToMany(targetEntity="InterpreterLanguage",mappedBy="interpreter", 
 * cascade={"persist", "remove"},orphanRemoval=true)
 *
 * @var ArrayCollection of InterpreterLanguage
 */
protected $interpreterLanguages;