I have the following two tables and corresponding two entities shown at the bottom of this post. time_unit only consists of several preset records which are s/second/1, m/minute/60, h/hour/360, etc.
I need to create a new Schedule. While not shown, I have several types of schedules which use the provided data differently and as such wish to place the setters inside the entity (either the constructor or some interface method) instead of in the service. To create the new schedule, I execute $scheduleService->create(['name'=>'the schedule name', 'other_data'=>123, 'time_unit'=>'h']);.
<?php
namespace Michael\App\Service;
use Michael\App\Entity;
class ScheduleService
{
public function create(array $params):int {
//validation as applicable
$schedule=new Entity\Schedule($params);
$this->em->persist($schedule);
$this->em->flush();
return $schedule->getId();
}
}
And then add the following constructor in the Schedule entity:
public function __construct(array $params) {
$this->setName($params['name']);
$this->setOtherData($params['other_data']);
$timeUnit=new TimeUnit();
$timeUnit->setUnit($params['time_unit']);
$this->setTimeUnit($timeUnit);
}
But this will not work because I am creating a new instance of TimeUnit and Doctrine will complain.
As an alternative, I can pass Schedule the entity manager, but everything I've read states that doing so is bad practice.
How should one create a new entity which contains another existing entity?
Schema and basic entities without additional logic are shown below:
CREATE TABLE schedule (id INT NOT NULL, time_unit VARCHAR(1) NOT NULL, name VARCHAR(45) NOT NULL, other_data VARCHAR(45) NOT NULL, INDEX fk_schedule_time_unit_idx (time_unit), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;
CREATE TABLE time_unit (unit VARCHAR(1) NOT NULL, name VARCHAR(45) NOT NULL, seconds INT NOT NULL, PRIMARY KEY(unit)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;
ALTER TABLE schedule ADD CONSTRAINT FK_5A3811FB7106057E FOREIGN KEY (time_unit) REFERENCES time_unit (unit);
schedule.php
<?php
namespace Michael\App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Schedule
*
* #ORM\Table(name="schedule", indexes={#ORM\Index(name="fk_schedule_time_unit_idx", columns={"time_unit"})})
* #ORM\Entity
*/
class Schedule
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=45)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="other_data", type="string", length=45)
*/
private $other_data;
//Not included since docs state one shouldn't map foreign keys to fields in an entity
//private $time_unit;
/**
* #var \TimeUnit
*
* #ORM\ManyToOne(targetEntity="TimeUnit")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="time_unit", referencedColumnName="unit")
* })
*/
private $timeUnit;
/**
* Set id.
*
* #param int $id
*
* #return Schedule
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get id.
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name.
*
* #param string $name
*
* #return Schedule
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set otherData.
*
* #param string $otherData
*
* #return Schedule
*/
public function setOtherData($otherData)
{
$this->other_data = $otherData;
return $this;
}
/**
* Get otherData.
*
* #return string
*/
public function getOtherData()
{
return $this->other_data;
}
/**
* Set timeUnit.
*
* #param TimeUnit $timeUnit (not a string)
*
* #return Schedule
*/
public function setTimeUnit($timeUnit)
{
$this->timeUnit = $timeUnit;
return $this;
}
/**
* Get timeUnit.
*
* #return TimeUnit (not a string)
*/
public function getTimeUnit()
{
return $this->timeUnit;
}
}
time_unit.php
<?php
namespace Michael\App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* TimeUnit
*
* #ORM\Table(name="time_unit")
* #ORM\Entity
*/
class TimeUnit
{
/**
* #var string
*
* #ORM\Column(name="unit", type="string", length=1)
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
*/
private $unit;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=45)
*/
private $name;
/**
* #var int
*
* #ORM\Column(name="seconds", type="integer")
*/
private $seconds;
/**
* Set unit.
*
* #param string $unit
*
* #return TimeUnit
*/
public function setUnit($unit)
{
$this->unit = $unit;
return $this;
}
/**
* Get unit.
*
* #return string
*/
public function getUnit()
{
return $this->unit;
}
/**
* Set name.
*
* #param string $name
*
* #return TimeUnit
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set seconds.
*
* #param int $seconds
*
* #return TimeUnit
*/
public function setSeconds($seconds)
{
$this->seconds = $seconds;
return $this;
}
/**
* Get seconds.
*
* #return int
*/
public function getSeconds()
{
return $this->seconds;
}
}
Passing EntityManager to entities is a bad practice because entities in Doctrine are used as data objects and hence should contain minimum amount of logic. All application logic related to entities should be moved to either custom repositories or to separate classes that belongs to application's service layer.
In your case you need to either pass instance of TimeUnit directly to constructor without attempting to construct it inside entity or expect it to be set through setter method.
Instead you need to modify your ScheduleService::create() to allow entity creation logic to be customizable. Since your ScheduleService basically implements Factory method pattern you need to make one step further towards implementation of Abstract factory pattern.
Abstract factory basically relies on list of concrete factories that are responsible for construction of concrete class instances instead of attempting to include all possible logic inside itself. Please find below example of implementation of such pattern in your case. It may look overcomplicated because I've extracted 2 interfaces and abstract class and this scheme can be simplified by use of 2 separate interfaces allows abstract and concrete factories to share common base while retaining necessary differences. Abstract class for concrete factories is used to allow extraction of basic entity configuration logic to avoid code duplication.
/**
* Interface for Schedule entity factories
*/
interface AbstractScheduleFactoryInterface
{
/**
* Create schedule entity by given params
*
* #param array $params
* #return Schedule
*/
public function create(array $params = []): Schedule;
}
/**
* Interface for concrete Schedule entity factories
*/
interface ScheduleFactoryInterface extends AbstractScheduleFactoryInterface
{
/**
* Decide if this factory can create schedule entity with given params
*
* #param array $params
* #return bool
*/
public function canCreate(array $params): bool;
}
/**
* Implementation of "Abstract Factory" pattern that relies on concrete factories for constructing Schedule entities
*/
class ScheduleFactory implements AbstractScheduleFactoryInterface
{
/**
* #var ScheduleFactoryInterface[]
*/
private $factories;
/**
* #param ScheduleFactoryInterface[] $factories
*/
public function __construct(array $factories)
{
$this->factories = $factories;
}
/**
* {#inheritdoc}
*/
public function create(array $params = []): Schedule
{
// Select factory that is able to create Schedule entity by given params
/** #var ScheduleFactoryInterface $factory */
$factory = array_reduce($this->factories, function (?ScheduleFactoryInterface $selected, ScheduleFactoryInterface $current) use ($params) {
if ($selected) {
return $selected;
}
return $current->canCreate($params) ? $current : null;
});
if (!$factory) {
// We have no factory to construct Schedule entity by given params
throw new \InvalidArgumentException('Unable to construct Schedule entity by given params');
}
// Construct entity by using selected concrete factory
return $factory->create($params);
}
}
/**
* Base implementation of concrete Schedule entity factory
* to allow sharing some common code between factories
*/
abstract class AbstractScheduleFactory implements ScheduleFactoryInterface
{
/**
* Basic entity configuration to avoid code duplication in concrete factories
*
* #param Schedule $entity
* #param array $params
*/
protected function configure(Schedule $entity, array $params = []): void
{
// This code is more or less copied from your code snippet
$entity->setName($params['name'] ?? '');
$entity->setOtherData($params['other_data'] ?? '');
}
}
/**
* Example implementation of Schedule entity factory with Schedules with TimeUnit
*/
class TimeUnitScheduleFactory extends AbstractScheduleFactory
{
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* {#inheritdoc}
*/
public function canCreate(array $params): bool
{
return array_key_exists('time_unit', $params);
}
/**
* Create schedule entity by given params
*
* #param array $params
* #return Schedule
* #throws \RuntimeException
*/
public function create(array $params = []): Schedule
{
$schedule = new Schedule();
// Perform basic Schedule configuration using shared base code
$this->configure($schedule, $params);
try {
// Attempt to assign time unit
$timeUnit = $this->em->find(TimeUnit::class, $params['time_unit']);
if (!$timeUnit instanceof TimeUnit) {
// No TimeUnit is available in database - create one
$timeUnit = new TimeUnit();
$timeUnit->setUnit($params['time_unit']);
$this->em->persist($timeUnit);
}
$schedule->setTimeUnit($timeUnit);
} catch (ORMException $e) {
throw new \RuntimeException('Failed to get TimeUnit entity', 0, $e);
}
return $schedule;
}
}
As you can see - this scheme allows you to have arbitrary amount of concrete factories for Schedule entities that needs to be passed to ScheduleFactory as constructor argument. After that ScheduleFactory::create() can be used to create any kind of Schedule entities with different construction logic.
Related
I tried find solution to my issue but didn't find anything.
I use: Symfony, Doctrine, PhpUnit
I have one entity class InvoiceNumerator:
/**
* InvoiceNumerator
*
* #ORM\Table(name="invoice_numerator")
* #ORM\Entity(repositoryClass="AppBundle\Repository\InvoiceNumeratorRepository")
*/
class InvoiceNumerator
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="translatedFormat", type="string", length=64)
*/
private $translatedFormat;
/**
* #var int
*
* #ORM\Column(name="currentValue", type="integer", options={"default": 0})
*/
private $currentValue = 0;
/**
* #var string
*
* #ORM\Column(name="current_number", type="string", length=64)
*/
private $currentNumber = '';
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set translatedFormat
*
* #param string $translatedFormat
*
* #return InvoiceNumerator
*/
public function setTranslatedFormat($translatedFormat)
{
$this->translatedFormat = $translatedFormat;
return $this;
}
/**
* Get translatedFormat
*
* #return string
*/
public function getTranslatedFormat()
{
return $this->translatedFormat;
}
/**
* Set currentValue
*
* #param integer $currentValue
*
* #return InvoiceNumerator
*/
public function setCurrentValue($currentValue)
{
$this->currentValue = $currentValue;
return $this;
}
/**
* Get currentValue
*
* #return int
*/
public function getCurrentValue()
{
return $this->currentValue;
}
/**
* #return string
*/
public function getCurrentNumber(): string
{
return $this->currentNumber;
}
/**
* #param string $currentNumber
* #return InvoiceNumerator
*/
public function setCurrentNumber(string $currentNumber): InvoiceNumerator
{
$this->currentNumber = $currentNumber;
return $this;
}
}
and I need in my tests to mock this class, but my setters should be left the same - no stubs - working code.
To mock this class, I made simple mock method:
public function getInvoiceNumerator()
{
$invoiceNumerator = $this->createMock(InvoiceNumerator::class);
$invoiceNumerator->method('getTranslatedFormat')
->willReturn('FS-CM/{n}/2018/01');
$invoiceNumerator->method('getCurrentValue')
->willReturn('1');
$invoiceNumerator->method('getCurrentNumber')
->willReturn('FS-CM/1/2018/01');
return $invoiceNumerator;
}
but in this case my setters are not working.
I can also set values on new Entity object:
public function getInvoiceNumerator()
{
$invoiceNumerator = new InvoiceNumerator();
$invoiceNumerator->setTranslatedFormat('FS-CM/{n}/2018/01');
$invoiceNumerator->setCurrentValue(1);
$invoiceNumerator->setCurrentNumber('FS-CM/1/2018/01');
return $invoiceNumerator;
}
In this case my setters working properly.
Question:
Is there any better way to do this? What is the best practice?
You almost have the answer in your question “Phpunit partial mock + proxy Entity”: there is a createPartialMock() method which you can use like this:
$invoiceNumerator = $this-> createPartialMock(
InvoiceNumerator::class,
['nameOfMockedMethod1', 'nameOfMockedMethod2']
);
This method has been available in PHPUnit 5.5 and newer. If you are using an older version, you can use setMethods(), but have to call it on the result returned by getMockBuilder(), not on the object returned by createMock() (which is the reason of the error you got after trying the approach from the 1st answer):
$subject = $this->getMockBuilder(MyClass::class)
->setMethods(['method1', 'method2'])
->getMock();
However, please note that createPartialMock() does slightly more. For instance, it will automatically disable the original constructor – which is almost always what you want in your tests (and what you have to do explicitly when using setMethods()). See documentation for exact information.
Basically you can set your mock to only mock specific methods:
$invoiceNumerator = $this->getMockBuilder(InvoiceNumerator::class)
->setMethods(["getTranslatedFormat","getCurrentValue", "getCurrentNumber"])
->getMock();
according to the documentation
setMethods(array $methods) can be called on the Mock Builder object to specify the methods that are to be replaced with a configurable test double. The behavior of the other methods is not changed. If you call setMethods(null), then no methods will be replaced.
Update:
Since PHPUnit 8 the setMethods function has been deprecated and replaced with onlyMethods for methods that exist on the mocked class and addMethods for methods that don't yet exist on the mock class (e.g. they will be implemented in the future but you want to text their dependencies assuming they already exist).
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 !!
/**
* working languages.
*
* #ORM\OneToMany(targetEntity="InterpreterLanguage",mappedBy="interpreter",
* cascade={"persist", "remove"},orphanRemoval=true)
*
* #var ArrayCollection of InterpreterLanguage
*/
protected $interpreterLanguages;
I have a situation where I need to add columns to a many-to-many join table, so I'm trying to follow the recommended practice of having the join table represented by an entity with ManyToOne relationships with each of the other two entities.
In this case, we have a court interpreter management system where there's an entity called Event, another called Interpreter. The InterpreterAssignment entity is one-to-many with both of these, but it also needs two metadata columns: a created datetime, and the Application\Entity\User who created it (I leave out the latter for simplicity's sake).
So, this works just fine:
$interpreter = $entityManager->getRepository('Application\Entity\Interpreter')
->findOneBy(['lastname'=>'Mintz']);
$assignment = new Entity\InterpreterAssignment();
$assignment->setInterpreter($interpreter)->setEvent($event);
$event->addInterpretersAssigned($assignment);
$em->flush();
...and I don't even need to say persist() because of the cascade={"persist","remove"}) on Event#interpretersAssigned.
However, when I try to do the reverse, that is,
use the removeInterpretersAssigned() method that Doctrine wrote for me:
$event = $entityManager->find('Application\Entity\Event',103510);
$assignment = $event->getInterpretersAssigned()[0];
$event->removeInterpretersAssigned($assignment);
$em->flush();
the database is untouched; Doctrine does not delete the row in the join table.
I can work around by saying $entityManager->remove($assignment). But I can't help but think that $event->removeInterpretersAssigned($assignment) is supposed to work.
So, I must be missing something but I can't see what. The Doctrine cli tool says my mappings are OK. Here are the entities, in relevant part:
/* namespace declarations and use statements omitted */
class Event
{
/* other fields and methods omitted */
/**
* #ORM\OneToMany(targetEntity="InterpreterAssignment",mappedBy="event",cascade={"persist","remove"})
* #var InterpreterAssignment[]
*/
protected $interpretersAssigned;
/* the following created by the Doctrine cli tool */
/**
* Remove interpretersAssigned
*
* #param \Application\Entity\InterpreterAssignment $interpretersAssigned
*/
public function removeInterpretersAssigned(\Application\Entity\InterpreterAssignment $interpretersAssigned)
{
$this->interpretersAssigned->removeElement($interpretersAssigned);
}
/**
* Get interpretersAssigned
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getInterpretersAssigned()
{
return $this->interpretersAssigned;
}
}
class Interpreter
{
/**
* #ORM\OneToMany(targetEntity="InterpreterAssignment",mappedBy="interpreter")
* #var InterpreterAssignment[]
*/
protected $assignments;
/**
* Remove assignment
*
* #param \Application\Entity\InterpreterAssignment $assignment
*/
public function removeAssignment(\Application\Entity\InterpreterAssignment $assignment)
{
$this->assignments->removeElement($assignment);
}
/**
* Get assignments
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getAssignments()
{
return $this->assignments;
}
}
and here is the InterpreterAssignment
/**
* #ORM\Entity
* #ORM\Table(name="interp_events", uniqueConstraints={#ORM\UniqueConstraint(name="unique_deft_event",columns={"interp_id","event_id"})})
* #ORM\HasLifeCycleCallbacks
*/
class InterpreterAssignment
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Interpreter",inversedBy="assignments")
* #ORM\JoinColumn(name="interp_id", referencedColumnName="interp_id")
* #var Interpreter
*/
protected $interpreter;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Event",inversedBy="interpretersAssigned")
* #ORM\JoinColumn(name="event_id", referencedColumnName="event_id")
* #var Event
*/
protected $event;
/**
* #ORM\Column(type="datetime",nullable=false)
* #var \DateTime
*/
protected $created;
/**
* #ORM\PrePersist
*/
public function onPrePersist()
{
$this->created = new \DateTime();
}
/**
* Set interpreter
*
* #param \Application\Entity\Interpreter $interpreter
*
* #return InterpreterAssignment
*/
public function setInterpreter(\Application\Entity\Interpreter $interpreter)
{
$this->interpreter = $interpreter;
return $this;
}
/**
* Get interpreter
*
* #return \Application\Entity\Interpreter
*/
public function getInterpreter()
{
return $this->interpreter;
}
/**
* Set event
*
* #param \Application\Entity\Event $event
*
* #return InterpreterAssignment
*/
public function setEvent(\Application\Entity\Event $event)
{
$this->event = $event;
return $this;
}
/**
* Get event
*
* #return \Application\Entity\Event
*/
public function getEvent()
{
return $this->event;
}
/* other stuff ommitted */
}
Many thanks.
I think you need to do 2 things:
(optional) You need to call $assignment->setEvent(null) after calling $event->removeInterpretersAssigned($assignment);
Also you may want to use Orphan Removal to remove the entity from the many to many table. and so the entity code should changed to (notice the addition of , orphanRemoval=true to the mapping code):
/**
* #ORM\OneToMany(targetEntity="InterpreterAssignment",mappedBy="event",cascade={"persist","remove"}, orphanRemoval=true)
* #var InterpreterAssignment[]
*/
protected $interpretersAssigned;
I am working on a system that is build using Zend Framework 2 and Doctrine 2.
In this system I am working on the contracts part where I want a list with some data (a partial query) from all contracts and I need to fill in a form with all data (entity find) from a specific contract.
However, since the contract to be filled in the form is also a result of the partial query, the properties that had not been loaded in the PARTIAL query will not be loaded for the queried entity either.
I have simplified the data to show only the current issue, the real entity has more fields:
Entity:
use Doctrine\ORM\Mapping as ORM;
/**
* ContractSub
*
* #ORM\Table(name="contract_sub", indexes={#ORM\Index(name="contract_id", columns={"contract_id"}), #ORM\Index(name="list_pension_start", columns={"list_pension_start_id"}), #ORM\Index(name="list_lease_car_category", columns={"list_lease_car_category_id"}), #ORM\Index(name="created_by_id", columns={"created_by_id"})})
* #ORM\Entity(repositoryClass="Application\Repository\ContractSubRepository")
* #ORM\HasLifecycleCallbacks
*/
class ContractSub
{
/**
*
* #var integer #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
*
* #var \DateTime #ORM\Column(name="start_date", type="date", nullable=false)
*/
private $startDate;
/**
*
* #var \DateTime #ORM\Column(name="end_date", type="date", nullable=true)
*/
private $endDate;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set startDate
*
* #param \DateTime $startDate
*
* #return ContractSub
*/
public function setStartDate($startDate)
{
$this->startDate = $startDate;
return $this;
}
/**
* Get startDate
*
* #return \DateTime
*/
public function getStartDate()
{
return $this->startDate;
}
/**
* Set endDate
*
* #param \DateTime $endDate
*
* #return ContractSub
*/
public function setEndDate($endDate)
{
$this->endDate = $endDate;
return $this;
}
/**
* Get endDate
*
* #return \DateTime
*/
public function getEndDate()
{
return $this->endDate;
}
}
Repository:
use Doctrine\ORM\EntityRepository;
class ContractSubRepository extends EntityRepository
{
public function getPartialStuffForTest()
{
$oQuery = $this->_em->createQuery('SELECT PARTIAL ContractSub.{id, startDate}
FROM Application\Entity\ContractSub ContractSub');
return $oQuery->getResult();
}
}
Controller:
use Zend\Mvc\Controller\AbstractActionController;
class ContractController extends AbstractActionController
{
public function testAction()
{
$oEntityManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$aContracts = $oEntityManager->getRepository('Application\Entity\ContractSub')->getPartialStuffForTest();
$oContractSub = $oEntityManager->getRepository('Application\Entity\ContractSub')->find(38);
var_dump($oContractSub->getStartDate());
var_dump($oContractSub->getEndDate());
die();
}
}
This outputs:
object(DateTime)[479]
public 'date' => string '2015-06-01 00:00:00.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Amsterdam' (length=16)
null
Indicating that the endDate is not loaded, even though I do a find to retrieve the complete entity.
When I comment the line that executes the getPartialStuffForTest(), I do get the endDate as well.
So I was wondering if there is any way to force Doctrine to retrieve the full entity after it already has a cached version of the partial entity?
To fully load a partial you have to use $entityManager->refresh($object).
Your answer is in the first paragraph in the Doctrine2 documentation chapter 18. Partial objects.
Use of partial objects is tricky. Fields that are not retrieved from the database will not be updated by the UnitOfWork even if they get changed in your objects. You can only promote a partial object to a fully-loaded object by calling EntityManager#refresh() or a DQL query with the refresh flag.
I'm testing cascade persist between Job entity and Category Entity in Symfony2 and Doctrin2.
Here's my Category entity:
<?php
namespace Ibw\JobeetBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Ibw\JobeetBundle\Entity\Affiliate;
use Ibw\JobeetBundle\Entity\Job;
use Ibw\JobeetBundle\Utils\Jobeet;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="Ibw\JobeetBundle\Repository\CategoryRepository")
* #ORM\Table(name="categories")
* #ORM\HasLifecycleCallbacks
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=255, unique=true)
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="Job", mappedBy="category", cascade={"persist"})
*/
protected $jobs;
/**
* #ORM\ManyToMany(targetEntity="Affiliate", mappedBy="categories")
*/
protected $affiliates;
/**
* #ORM\Column(type="string", length=255, unique=true)
*/
protected $slug;
/**
* Constructor
*/
public function __construct()
{
$this->jobs = new ArrayCollection();
$this->affiliates = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Add jobs
*
* #param Job $jobs
* #return Category
*/
public function addJob(Job $jobs)
{
$this->jobs[] = $jobs;
return $this;
}
/**
* Remove jobs
*
* #param Job $jobs
*/
public function removeJob(Job $jobs)
{
$this->jobs->removeElement($jobs);
}
/**
* Get jobs
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getJobs()
{
return $this->jobs;
}
/**
* Add affiliates
*
* #param Affiliate $affiliates
* #return Category
*/
public function addAffiliate(Affiliate $affiliates)
{
$this->affiliates[] = $affiliates;
return $this;
}
/**
* Remove affiliates
*
* #param Affiliate $affiliates
*/
public function removeAffiliate(Affiliate $affiliates)
{
$this->affiliates->removeElement($affiliates);
}
/**
* Get affiliates
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getAffiliates()
{
return $this->affiliates;
}
/**
* Set slug
*
* #param string $slug
* #return Category
*/
public function setSlug($slug)
{
$this->slug = $slug;
return $this;
}
/**
* Get slug
*
* #return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function setSlugValue()
{
$this->setSlug(Jobeet::slugify($this->getName()));
}
}
Here's the cascade persist part in it:
/**
* #ORM\OneToMany(targetEntity="Job", mappedBy="category", cascade={"persist"})
*/
protected $jobs;
Now when i try to test it using this test :
public function testAddJobToCategory()
{
$job = new Job();
$job->setType('flexible-time');
$job->setCompany('Sensio Labs');
$job->setLogo('sensio-labs.gif');
$job->setUrl('http://www.sensiolabs.com/');
$job->setPosition('Web Developer');
$job->setLocation('Paris, France');
$job->setDescription('You\'ve already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available.');
$job->setHowToApply('Send your resume to fabien.potencier [at] sensio.com');
$job->setIsPublic(true);
$job->setIsActivated(true);
$job->setToken('job');
$job->setEmail('job#example.com');
$job->setExpiresAt(new \DateTime('+30 days'));
$category = $this->em->createQueryBuilder('c')
->select('c')
->from('IbwJobeetBundle:Category', 'c')
->where('c.id = 1')
->getQuery()
->getSingleResult();
$category->addJob($job);
$this->em->persist($category);
$this->em->flush();
$jobFromQuery = $this->em->createQueryBuilder('j')
->select('j')
->from('IbwJobeetBundle:Job', 'j')
->where('j.type = :type')
->setParameter('type', 'flexible-time')
->setMaxResults(1)
->setFirstResult(1)
->getQuery()
->getSingleResult();
$this->assertEquals(1, $jobFromQuery->getCategory()->getId());
}
When i run this test, I get this error :
PHP Fatal error: Call to a member function getId() on a non-object
So i think it's in the $jobFromQuery->getCategory()->getId() part. getCategory() being not set or something.
This doctrine doc http://docs.doctrine-project.org/en/latest/reference/unitofwork-associations.html says:
Doctrine will only check the owning side of an association for changes.
To fully understand this, remember how bidirectional associations are maintained in the object world. There are 2 references on each side of the association and these 2 references both represent the same association but can change independently of one another. Of course, in a correct application the semantics of the bidirectional association are properly maintained by the application developer (that’s his responsibility). Doctrine needs to know which of these two in-memory references is the one that should be persisted and which not. This is what the owning/inverse concept is mainly used for.
Changes made only to the inverse side of an association are ignored. Make sure to update both sides of a bidirectional association (or at least the owning side, from Doctrine’s point of view)
The owning side of a bidirectional association is the side Doctrine “looks at” when determining the state of the association, and consequently whether there is anything to do to update the association in the database
On your code you just do $category->addJob($job); so you're setting the association on the inverse side. As the docs that's ignored so you should at least do $job->setCategory($category) first.