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!
In Interpreter.php, orphanRemoval=true !!