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).
Related
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.
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'm looking for a solution to automatically translate the entities of my Symfony application. I'm stuck with a legacy database where translations are stored in the same table as extra fields:
id | name | name_de | name_fr
1 | cat | Katze | chat
2 | dog | Hund | chien
My entity is mapped accordinggly:
class Animal
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #var integer
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=64, nullable=false)
*
* #var string
*/
private $name;
/**
* #ORM\Column(name="name_de", type="string", length=64, nullable=false)
*
* #var string
*/
private $nameDe;
/**
* #ORM\Column(name="name_fr", type="string", length=64, nullable=false)
*
* #var string
*/
private $nameFr;
/* Followed by getters and setters */
}
I've already looking into the Translatable extension, but that cannot match my database schema. I also started with a custom annotation hooking into the postLoad event, but then I was stopped by the simple issue that postLoad may be triggered in the proxy state of an entity.
Next I'd look into a custom query walker (basically a modified approach of the Translatable extension), but I'd hope there's a less complex solution out there.
Cheers
Matthias
There are quite a few solutions here and I guess I haven't looked at half of them.
Anyhow, the least complex and at least a little bit clean solution I came up with so far is using a static class for translating. This could look something like this:
class Translation
{
/**
* #var string
*/
private static $language;
/**
* tries to get a translated property by prepending the
* configured language to the given entities getter
*
* #param object $entity
* #param string $getter
* #return mixed
*/
public static function getTranslated($entity, $getter) {
$language = self::getLanguage();
$translatedGetter = $getter.$language;
if(method_exists($entity, $translatedGetter)) {
return $entity->$translatedGetter();
} else {
return $entity->$getter;
}
}
/**
* #return string
*/
public static function getLanguage()
{
return self::$language;
}
/**
* #param string $language
*/
public static function setLanguage($language)
{
self::$language = ucfirst(strtolower($language));
}
}
You then set the language whenever your application starts and implement the translations either in your entities:
/**
* #return string
*/
public function getName()
{
return Translation::getTranslated($this, __FUNCTION__);
}
or call it from outside:
Translation::getTranslated($animal, "getName");
So with the first method this code:
Translation::setLanguage("DE");
// far more code
/** #var Animal[] $animals */
$animals = $entityManager->getRepository(Animal::class)->findAll();
foreach ($animals as $animal) {
echo $animal->getName()."<br >";
}
would put out:
Katze
Hund
This is just one way to do it with a static class, of course.
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 have a question out of curiosity about the inner workings of Doctrine2. I as a User see a really clean and robust interface, but there must be some heavy magic at work under the hood.
When I generate a simple entity it looks something like this:
class SimpleEntity
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $title;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* #param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
}
As you will notice one thing is conspicuously absent, there is no way to set the id, but nevertheless doctrines factories return entities with set ids. How can that work? I have tried to look through the source, but lost the track somewhere.
How can it be possible to overwrite protected values without being in the class hierarchy?
The answer is Reflection. See http://www.doctrine-project.org/docs/orm/2.1/en/tutorials/getting-started-xml-edition.html#a-first-prototype
Without digging into the Doctrine source, I'd say it makes use of ReflectionProperty::setValue()