JMS Serializer does not apply formatting to newly created objects - php

When getting an object from an API, I receive a properly serialized Course object.
"startDate": "2018-05-21",
But when I create a new object and try to return it, the formatting is not applied.
"startDate": "2019-02-01T02:37:02+00:00",
Even if I use the repository to get a new Course object, if it is the same object I have just created then it is still not serialized with formatting. Maybe because it is already loaded in memory by that point?
If I use the repository to get a different course from the database then the serialization formatting is applied.
I expected formatting to be applied when I return a Course object regardless of whether it has just been created or not. Any ideas?
Course class
/**
* #ORM\Entity(repositoryClass="App\Repository\CourseRepository")
*
* #HasLifecycleCallbacks
*/
class Course
{
/**
* #var string
*
* #ORM\Column(type="date")
*
* #Assert\Date
* #Assert\NotNull
*
* #JMS\Type("DateTime<'Y-m-d'>")
* #JMS\Groups({"courses-list", "course-details"})
*/
private $startDate;
/**
* #return string
*/
public function getStartDate(): string
{
return $this->startDate;
}
}
Course API Controller Class
public function getCourse($id)
{
$em = $this->getDoctrine()->getManager();
$repo = $em->getRepository('App:Course');
$course = $repo->find($id);
if(!$course) {
throw new NotFoundHttpException('Course not found', null, 2001);
}
return $course;
}
public function addCourse(Request $request) {
$course = new Course();
$course->setStartDate($startDate);
$validator = $this->get('validator');
$em->persist($course);
$em->flush();
return $course;
}

Turns out that you shouldn't use Carbon objects with JMS Serializer.
As soon as I set DateTime objects on the Course object instead of Carbon objects, it worked fine.
Strange behaviour considering that they both implement DateTimeInterface.

Related

Multiple dimensional map using objects as keys

I have a set of objects (MainObject) which are uniquely defined by two objects (SubObject1, SubObject2) and a string (theString). I with to retrieve a MainObject from the set by returning an existing object based on the two subobjects and string should it exist, else creating a new one, adding it to the set, and returning that object.
The following pseudo code demonstrates this in the make believe world where a standard array can use objects as keys.
class SubObject1{}
class SubObject2{}
class MainObject{
private $subObject1, $subObject2, $theString;
public function __construct(SubObject1 $subObject1, SubObject2 $subObject2, string $theString):MainObject {
$this->subObject1=$subObject1;
$this->subObject2=$subObject2;
$this->theString=$theString;
}
}
class ObjectCollection
{
private $map=[];
public function getObject(SubObject1 $subObject1, SubObject2 $subObject2, string $theString):MainObject {
if(isset($this->map[$subObject1][$subObject2][$theString])) {
$mainObject=$this->map[$subObject1][$subObject2][$theString];
}
else {
$mainObject=new MainObject($subObject1, $subObject2, $theString);
$this->map[$subObject1][$subObject2][$theString]=$mainObject;
}
return $mainObject;
}
}
$objectCollection=new ObjectCollection();
$subObject1_1=new SubObject1();
$subObject1_2=new SubObject1();
$subObject2_1=new SubObject2();
$subObject2_1=new SubObject2();
$o=$objectCollection->getObject($subObject1_1, $subObject2_1, 'hello'); //returns a new object
$o=$objectCollection->getObject($subObject1_2, $subObject2_1, 'hello'); //returns a new object
$o=$objectCollection->getObject($subObject1_1, $subObject2_1, 'goodby'); //returns a new object
$o=$objectCollection->getObject($subObject1_1, $subObject2_1, 'hello'); //returns existing object
How should this be best implemented?
One possibility is something like the following untested code, however, it is a little verbose and am interested if there is a cleaner solution.
public function getObject(SubObject1 $subObject1, SubObject2 $subObject2, string $theString):MainObject {
if(isset($this->map[$theString])) {
if($this->map[$theString]->contains($subObject1)) {
$subObject1Storage=$this->map[$theString][$subObject1];
if($subObject1Storage->contains($subObject2)) {
$mainObject=$subObject1Storage[$subObject2];
}
else {
$mainObject=new MainObject($subObject1, $subObject2, $theString);
$subObject1Storage[$subObject2]=$mainObject;
}
}
else {
$subObject1Storage = new \SplObjectStorage();
$this->map[$theString][$subObject1]=$subObject1Storage;
$mainObject=new MainObject($subObject1, $subObject2, $theString);
$subObject1Storage[$subObject2]=$mainObject;
}
}
else {
$this->map[$theString] = new \SplObjectStorage();
$subObject1Storage = new \SplObjectStorage();
$this->map[$theString][$subObject1]=$subObject1Storage;
$mainObject=new MainObject($subObject1, $subObject2, $theString);
$subObject1Storage[$subObject2]=$mainObject;
}
return $mainObject;
}
The logic I had in mind was as follows:
A factory(or abstract factory in case of too many objects) will take care of creating the object itself.
A container will map unique identifiers with objects created by the factory.
And can retrieve objects based on those identifiers.
That's the easy part, the custom part should be even easier, you can add your own methods to do whatever magic you need with aliases and such.
namespace Example;
/**
* Class ObjectFactory
*
* #package Example
*/
class ObjectFactory {
/**
* This is obviosuly not ideal but it can work
* with a limited amount of objects. Otherwise use an
* abstract factory and let each instance take care of a few
* related objects
*
* #param string $objectAlias
*
* #throws \Exception
*/
public function make(string $objectAlias) {
switch($objectAlias) {
case 'object_unique_id_1':
try{
$instance = new $objectAlias;
}catch (\Exception $exception) {
// log or whatever and rethrow
throw new \Exception("Invalid class? maybe, I dunno");
}
// return $instance
// etc
}
}
}
You can also use Reflection here to recursively get the arguments for the object and dump new instances of the object in the current object based on the arguments in the construct esentially make your own little DI container.
But if you want to keep your sanity use something like Pimple.
Container:
<?php
namespace Example;
/**
* Class Container
*
* #package Example
*/
class Container {
/**
* #var array
*/
private $map = [];
/**
* #param $objectAlias
* #param $objectInstance
*
* #throws \Exception
*/
public function set($objectAlias, $objectInstance) {
// You can use a try catch here, I chose not to
if(isset($this->map[$objectAlias])) {
throw new \Exception("Already exists");
}
$this->map[$objectAlias] = $objectInstance;
}
/**
* #param $objectAlias
*
* #return bool|mixed
*/
public function get($objectAlias) {
if(isset($this->map[$objectAlias])) {
return $this->map[$objectAlias];
}
return false;
}
}
Specific container which will hold your own methods
<?php
namespace Example;
/**
* Class ContainerHashMapThingy
*
* #package Example
*/
class ContainerHashMapThingy extends Container {
// Your methods go here
}
And an example object:
<?php
namespace Example;
/**
* Class ExampleObject1
*
* #package Example
*/
class ExampleObject1 {
/**
* #return string
*/
public function alias() {
// This is just for example sake
// You can just as well have a config, another class to map them or not map them at all
return 'example_object_1';
}
}
And an actual example
<?php
$factory = new \Example\ObjectFactory();
$container = new \Example\Container();
$objectOne = $factory->make('example_object_1');
$container->set('first_object', $objectOne);
The idea here is to give you a clean slate for a container + factory.
If you extend the container you can implement your own methods, remove stuff from the map array, even rewrite the set method to suit your own needs.
While this is not a complete answer it's very hard to give one since, as I said, your needs may vary.
I do hope this gets you on the right track.

Can symfony serializer deserialize return nested entity of type child entity?

When I deserialize my doctrine entity, the initial object is constructed/initiated correctly, however all child relations are trying to be called as arrays.
The root level object's addChild(ChildEntity $entity) method is being called, but Symfony is throwing an error that addChild is receiving an array and not an instance of ChildEntity.
Does Symfony's own serializer have a way to deserialize nested arrays (child entities) to the entity type?
JMS Serializer handles this by specifying a #Type("ArrayCollection<ChildEntity>") annotation on the property.
I believe the Symfony serializer attempts to be minimal compared to the JMS Serializer, so you might have to implement your own denormalizer for the class. You can see how the section on adding normalizers.
There may be an easier way, but so far with Symfony I am using Discriminator interface annotation and type property for array of Objects. It can also handle multiple types in one array (MongoDB):
namespace App\Model;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* #DiscriminatorMap(typeProperty="type", mapping={
* "text"="App\Model\BlogContentTextModel",
* "code"="App\Model\BlogContentCodeModel"
* })
*/
interface BlogContentInterface
{
/**
* #return string
*/
public function getType(): string;
}
and parent object will need to define property as interface and get, add, remove methods:
/**
* #var BlogContentInterface[]
*/
protected $contents = [];
/**
* #return BlogContentInterface[]
*/
public function getContents(): array
{
return $this->contents;
}
/**
* #param BlogContentInterface[] $contents
*/
public function setContents($contents): void
{
$this->contents = $contents;
}
/**
* #param BlogContentInterface $content
*/
public function addContent(BlogContentInterface $content): void
{
$this->contents[] = $content;
}
/**
* #param BlogContentInterface $content
*/
public function removeContent(BlogContentInterface $content): void
{
$index = array_search($content, $this->contents);
if ($index !== false) {
unset($this->contents[$index]);
}
}

Synfony custom validator alter object value within validation

I'm writing a REST API, my problem is when I want to deserialize an request in an entity that has a one to many relation to other entity, because when I want to persist the object instead of alocationg the existing "child", Doctrine creates a new one and alocates him to the object.
Here is an post example:
{"category": {"id": 1}, {"theme": {"id": 1} }
What I expect to happen, is to add a new channel with category:1 and theme:1, but instead doctrine creates a new category/theme.
What I wanted to do is to change the category/theme object created by deserializing with JMS Serializer in an Doctrine object within the custom validator,
class Channel
{
/**
* #ChoiceEntity(targetEntity="Bundle\ChannelBundle\Entity\ChannelCategory", allowNull=true)
* #Type("Bundle\ChannelBundle\Entity\ChannelCategory")
*/
public $category;
/**
* #ChoiceEntity(targetEntity="Bundle\ChannelBundle\Entity\Theme", allowNull=true)
* #Type("Bundle\ChannelBundle\Entity\Theme")
*/
public $theme;
}
And here the custom validator:
class ChoiceEntityValidator extends ConstraintValidator
{
/**
*
* #var type
*/
private $entityManager;
/**
*
* #param type $entityManager
*/
public function __construct($entityManager){
$this->entityManager = $entityManager;
}
/**
* #param FormEvent $event
*/
public function validate($object, Constraint $constraint)
{
if($constraint->getAllowNull() === TRUE && $object === NULL){//null allowed, and value is null
return;
}
if($object === NULL || !is_object($object)) {
return $this->context->addViolation($constraint->message);
}
if(!$this->entityManager->getRepository($constraint->getTargetEntity())->findOneById($object->getId())) {
$this->context->addViolation($constraint->message);
}
}
}
So is there a way of changing the $object from the custom validator with the value from repository result?
I don't think that editing the validated object within your custom validator is a good idea.
Keep in mind that the custom validator you added should be only used to check if your object is valid or not (depending on your validation rules).
If you want to edit the object, you should then do it before invoking the validation process. You'll probably need to use Data Transformers.

Zend Framework and Doctrine 2 - are my unit tests sufficient?

I'm quite new to Zend and unit testing in general. I have come up with a small application that uses Zend Framework 2 and Doctrine. It has only one model and controller and I want to run some unit tests on them.
Here's what I have so far:
Base doctrine 'entity' class, containing methods I want to use in all of my entities:
<?php
/**
* Base entity class containing some functionality that will be used by all
* entities
*/
namespace Perceptive\Database;
use Zend\Validator\ValidatorChain;
class Entity{
//An array of validators for various fields in this entity
protected $validators;
/**
* Returns the properties of this object as an array for ease of use. Will
* return only properties with the ORM\Column annotation as this way we know
* for sure that it is a column with data associated, and won't pick up any
* other properties.
* #return array
*/
public function toArray(){
//Create an annotation reader so we can read annotations
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
//Create a reflection class and retrieve the properties
$reflClass = new \ReflectionClass($this);
$properties = $reflClass->getProperties();
//Create an array in which to store the data
$array = array();
//Loop through each property. Get the annotations for each property
//and add to the array to return, ONLY if it contains an ORM\Column
//annotation.
foreach($properties as $property){
$annotations = $reader->getPropertyAnnotations($property);
foreach($annotations as $annotation){
if($annotation instanceof \Doctrine\ORM\Mapping\Column){
$array[$property->name] = $this->{$property->name};
}
}
}
//Finally, return the data array to the user
return $array;
}
/**
* Updates all of the values in this entity from an array. If any property
* does not exist a ReflectionException will be thrown.
* #param array $data
* #return \Perceptive\Database\Entity
*/
public function fromArray($data){
//Create an annotation reader so we can read annotations
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
//Create a reflection class and retrieve the properties
$reflClass = new \ReflectionClass($this);
//Loop through each element in the supplied array
foreach($data as $key=>$value){
//Attempt to get at the property - if the property doesn't exist an
//exception will be thrown here.
$property = $reflClass->getProperty($key);
//Access the property's annotations
$annotations = $reader->getPropertyAnnotations($property);
//Loop through all annotations to see if this is actually a valid column
//to update.
$isColumn = false;
foreach($annotations as $annotation){
if($annotation instanceof \Doctrine\ORM\Mapping\Column){
$isColumn = true;
}
}
//If it is a column then update it using it's setter function. Otherwise,
//throw an exception.
if($isColumn===true){
$func = 'set'.ucfirst($property->getName());
$this->$func($data[$property->getName()]);
}else{
throw new \Exception('You cannot update the value of a non-column using fromArray.');
}
}
//return this object to facilitate a 'fluent' interface.
return $this;
}
/**
* Validates a field against an array of validators. Returns true if the value is
* valid or an error string if not.
* #param string $fieldName The name of the field to validate. This is only used when constructing the error string
* #param mixed $value
* #param array $validators
* #return boolean|string
*/
protected function setField($fieldName, $value){
//Create a validator chain
$validatorChain = new ValidatorChain();
$validators = $this->getValidators();
//Try to retrieve the validators for this field
if(array_key_exists($fieldName, $this->validators)){
$validators = $this->validators[$fieldName];
}else{
$validators = array();
}
//Add all validators to the chain
foreach($validators as $validator){
$validatorChain->attach($validator);
}
//Check if the value is valid according to the validators. Return true if so,
//or an error string if not.
if($validatorChain->isValid($value)){
$this->{$fieldName} = $value;
return $this;
}else{
$err = 'The '.$fieldName.' field was not valid: '.implode(',',$validatorChain->getMessages());
throw new \Exception($err);
}
}
}
My 'config' entity, which represents a one-row table containing some configuration options:
<?php
/**
* #todo: add a base entity class which handles validation via annotations
* and includes toArray function. Also needs to get/set using __get and __set
* magic methods. Potentially add a fromArray method?
*/
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\Validator;
use Zend\I18n\Validator as I18nValidator;
use Perceptive\Database\Entity;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Config extends Entity{
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $minLengthUserId;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $minLengthUserName;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $minLengthUserPassword;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $daysPasswordReuse;
/**
* #ORM\Id
* #ORM\Column(type="boolean")
*/
protected $passwordLettersAndNumbers;
/**
* #ORM\Id
* #ORM\Column(type="boolean")
*/
protected $passwordUpperLower;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $maxFailedLogins;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $passwordValidity;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $passwordExpiryDays;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $timeout;
// getters/setters
/**
* Get the minimum length of the user ID
* #return int
*/
public function getMinLengthUserId(){
return $this->minLengthUserId;
}
/**
* Set the minmum length of the user ID
* #param int $minLengthUserId
* #return \Application\Entity\Config This object
*/
public function setMinLengthUserId($minLengthUserId){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('minLengthUserId', $minLengthUserId);
}
/**
* Get the minimum length of the user name
* #return int
*/
public function getminLengthUserName(){
return $this->minLengthUserName;
}
/**
* Set the minimum length of the user name
* #param int $minLengthUserName
* #return \Application\Entity\Config
*/
public function setMinLengthUserName($minLengthUserName){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('minLengthUserName', $minLengthUserName);
}
/**
* Get the minimum length of the user password
* #return int
*/
public function getMinLengthUserPassword(){
return $this->minLengthUserPassword;
}
/**
* Set the minimum length of the user password
* #param int $minLengthUserPassword
* #return \Application\Entity\Config
*/
public function setMinLengthUserPassword($minLengthUserPassword){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('minLengthUserPassword', $minLengthUserPassword);
}
/**
* Get the number of days before passwords can be reused
* #return int
*/
public function getDaysPasswordReuse(){
return $this->daysPasswordReuse;
}
/**
* Set the number of days before passwords can be reused
* #param int $daysPasswordReuse
* #return \Application\Entity\Config
*/
public function setDaysPasswordReuse($daysPasswordReuse){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('daysPasswordReuse', $daysPasswordReuse);
}
/**
* Get whether the passwords must contain letters and numbers
* #return boolean
*/
public function getPasswordLettersAndNumbers(){
return $this->passwordLettersAndNumbers;
}
/**
* Set whether passwords must contain letters and numbers
* #param int $passwordLettersAndNumbers
* #return \Application\Entity\Config
*/
public function setPasswordLettersAndNumbers($passwordLettersAndNumbers){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('passwordLettersAndNumbers', $passwordLettersAndNumbers);
}
/**
* Get whether password must contain upper and lower case characters
* #return type
*/
public function getPasswordUpperLower(){
return $this->passwordUpperLower;
}
/**
* Set whether password must contain upper and lower case characters
* #param type $passwordUpperLower
* #return \Application\Entity\Config
*/
public function setPasswordUpperLower($passwordUpperLower){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('passwordUpperLower', $passwordUpperLower);
}
/**
* Get the number of failed logins before user is locked out
* #return int
*/
public function getMaxFailedLogins(){
return $this->maxFailedLogins;
}
/**
* Set the number of failed logins before user is locked out
* #param int $maxFailedLogins
* #return \Application\Entity\Config
*/
public function setMaxFailedLogins($maxFailedLogins){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('maxFailedLogins', $maxFailedLogins);
}
/**
* Get the password validity period in days
* #return int
*/
public function getPasswordValidity(){
return $this->passwordValidity;
}
/**
* Set the password validity in days
* #param int $passwordValidity
* #return \Application\Entity\Config
*/
public function setPasswordValidity($passwordValidity){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('passwordValidity', $passwordValidity);
}
/**
* Get the number of days prior to expiry that the user starts getting
* warning messages
* #return int
*/
public function getPasswordExpiryDays(){
return $this->passwordExpiryDays;
}
/**
* Get the number of days prior to expiry that the user starts getting
* warning messages
* #param int $passwordExpiryDays
* #return \Application\Entity\Config
*/
public function setPasswordExpiryDays($passwordExpiryDays){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('passwordExpiryDays', $passwordExpiryDays);
}
/**
* Get the timeout period of the application
* #return int
*/
public function getTimeout(){
return $this->timeout;
}
/**
* Get the timeout period of the application
* #param int $timeout
* #return \Application\Entity\Config
*/
public function setTimeout($timeout){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('timeout', $timeout);
}
/**
* Returns a list of validators for each column. These validators are checked
* in the class' setField method, which is inherited from the Perceptive\Database\Entity class
* #return array
*/
public function getValidators(){
//If the validators array hasn't been initialised, initialise it
if(!isset($this->validators)){
$validators = array(
'minLengthUserId' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(1),
),
'minLengthUserName' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(2),
),
'minLengthUserPassword' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(3),
),
'daysPasswordReuse' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(-1),
),
'passwordLettersAndNumbers' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(-1),
new Validator\LessThan(2),
),
'passwordUpperLower' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(-1),
new Validator\LessThan(2),
),
'maxFailedLogins' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(0),
),
'passwordValidity' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(1),
),
'passwordExpiryDays' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(1),
),
'timeout' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(0),
)
);
$this->validators = $validators;
}
//Return the list of validators
return $this->validators;
}
/**
* #todo: add a lifecyle event which validates before persisting the entity.
* This way there is no chance of invalid values being saved to the database.
* This should probably be implemented in the parent class so all entities know
* to validate.
*/
}
And my controller, which can read from and write to the entity:
<?php
/**
* A restful controller that retrieves and updates configuration information
*/
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractRestfulController;
use Zend\View\Model\JsonModel;
class ConfigController extends AbstractRestfulController
{
/**
* The doctrine EntityManager for use with database operations
* #var \Doctrine\ORM\EntityManager
*/
protected $em;
/**
* Constructor function manages dependencies
* #param \Doctrine\ORM\EntityManager $em
*/
public function __construct(\Doctrine\ORM\EntityManager $em){
$this->em = $em;
}
/**
* Retrieves the configuration from the database
*/
public function getList(){
//locate the doctrine entity manager
$em = $this->em;
//there should only ever be one row in the configuration table, so I use findAll
$config = $em->getRepository("\Application\Entity\Config")->findAll();
//return a JsonModel to the user. I use my toArray function to convert the doctrine
//entity into an array - the JsonModel can't handle a doctrine entity itself.
return new JsonModel(array(
'data' => $config[0]->toArray(),
));
}
/**
* Updates the configuration
*/
public function replaceList($data){
//locate the doctrine entity manager
$em = $this->em;
//there should only ever be one row in the configuration table, so I use findAll
$config = $em->getRepository("\Application\Entity\Config")->findAll();
//use the entity's fromArray function to update the data
$config[0]->fromArray($data);
//save the entity to the database
$em->persist($config[0]);
$em->flush();
//return a JsonModel to the user. I use my toArray function to convert the doctrine
//entity into an array - the JsonModel can't handle a doctrine entity itself.
return new JsonModel(array(
'data' => $config[0]->toArray(),
));
}
}
Because of character limits on I was unable to paste in my unit tests, but here are links to my unit tests so far:
For the entity:
https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Entity/ConfigTest.php
For the controller:
https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Controller/ConfigControllerTest.php
Some questions:
Am I doing anything obviously wrong here?
In the tests for the entity, I am repeating the same tests for many different fields - is there a way to minimise this? Like have a standard battery of tests to run on integer columns for instance?
In the controller I am trying to 'mock up' doctrine's entity manager so that changes aren't really saved into the database - am I doing this properly?
Is there anything else in the controller which I should test?
Thanks in advance!
While your code appears to be solid enough, it presents a couple of design oversights.
First of all, Doctrine advise to treat entities like simple, dumb value objects, and states that the data they hold is always assumed to be valid.
This means that any business logic, like hydration, filtering and validation, should be moved outside entities to a separate layer.
Speaking of hydration, rather than implementing by yourself fromArray and toArray methods, you could use the supplied DoctrineModule\Stdlib\Hydrator\DoctrineObject hydrator, which can also blend flawlessly with Zend\InputFilter, to handle filtering and validation. This would make entity testing much much less verbose, and arguably not so needed, since you would test the filter separately.
Another important suggestion coming from Doctrine devs is to not inject an ObjectManager directly inside controllers. This is for encapsulation purposes: it is desirable to hide implementation details of your persistence layer to the Controller and, again, expose only an intermediate layer.
In your case, all this could be done by having a ConfigService class, designed by contract, which will only provide the methods you really need (i.e. findAll(), persist() and other handy proxies), and will hide the dependencies that are not strictly needed by the controller, like the EntityManager, input filters and the like. It will also contribute to easier mocking.
This way, if one day you would want to do some changes in your persistence layer, you would just have to change how your entity service implements its contract: think about adding a custom cache adapter, or using Doctrine's ODM rather than the ORM, or even not using Doctrine at all.
Other than that, your unit testing approach looks fine.
TL;DR
You should not embed business logic inside Doctrine entities.
You should use hydrators with input filters together.
You should not inject the EntityManager inside controllers.
An intermediate layer would help implementing these variations, preserving at the same time Model and Controller decoupling.
Your tests look very similar to ours, so there's nothing immediately obvious that you are doing incorrectly. :)
I agree that this "smells" a bit weird, but I don't have an answer for you on this one. Our standard is to make all of our models "dumb" and we do not test them. This is not something I recommend, but because I havent encountered your scenario before I don't want to just guess.
You seem to be testing pretty exhaustively, although I would really recommend checking out the mocking framework: Phake (http://phake.digitalsandwich.com/docs/html/) It really helps to seperate your assertions from your mocking, as well as provides a much more digestable syntax than the built in phpunit mocks.
good luck!

Doctrine doesn't load associations from session correctly

I'm having this behavior with Doctrine 2.1 where I'm looking for a nice 'workaround'. The problem is as follows:
I have a user Entity:
/**
* #Entity(repositoryClass="Application\Entity\Repository\UserRepository")
* #HasLifecycleCallbacks
*/
class User extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var \DateTime
* #Column(type="datetime",nullable=false)
*/
protected $insertDate;
/**
*
* #var string
* #Column(type="string", nullable=false)
*/
protected $username;
/**
*
* #ManyToOne(targetEntity="UserGroup", cascade={"merge"})
*/
protected $userGroup;
}
And a usergroup entity:
/**
* #Entity
*/
class UserGroup extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var string
* #Column(type="string",nullable=false)
*/
protected $name;
}
If I instantiate a user object (doing this with Zend_Auth) and Zend_Auth puts it automatically the session.
The problem is however, that is I pull it back from the session at a next page then the data in the user class is perfectly loaded but not in the userGroup association. If I add cascade={"merge"} into the annotation in the user object the userGroup object IS loaded but the data is empty. If you dump something like:
$user->userGroup->name
You will get NULL back. The problem is no data of the usergroup entity is accesed before the user object is saved in the session so a empty initialized object will be returned. If I do something like:
echo $user->userGroup->name;
Before I store the user object in the session all data of the assocication userGroup is succesfully saved and won't return NULL on the next page if I try to access the $user->userGroup->name variable.
Is there a simple way to fix this? Can I manually load the userGroup object/association with a lifecycle callback #onLoad in the user class maybe? Any suggestions?
Your problem is a combination of what mjh_ca answered and a problem with your AbstractEntity implementation.
Since you show that you access entity fields in this fashion:
$user->userGroup->name;
I assume your AbstractEntity base class is using __get() and __set() magic methods instead of proper getters and setters:
function getUserGroup()
{
return $this->userGroup;
}
function setUserGroup(UserGroup $userGroup)
{
$this->userGroup = $userGroup;
}
You are essentially breaking lazy loading:
"... whenever you access a public property of a proxy object that hasn’t been initialized yet the return value will be null. Doctrine cannot hook into this process and magically make the entity lazy load."
Source: Doctrine Best Practices: Don't Use Public Properties on Entities
You should instead be accessing fields this way:
$user->getUserGroup()->getName();
The second part of your problem is exactly as mjh_ca wrote - Zend_Auth detaches your entity from the entity manager when it serializes it for storage in the session. Setting cascade={"merge"} on your association will not work because it is the actual entity that is detached. You have to merge the deserialized User entity into the entity manager.
$detachedIdentity = Zend_Auth::getInstance()->getIdentity();
$identity = $em->merge($detachedIdentity);
The question, is how to do this cleanly. You could look into implementing a __wakeup() magic method for your User entity, but that is also against doctrine best practices...
Source: Implementing Wakeup or Clone
Since we are talking about Zend_Auth, you could extend Zend_Auth and override the getIdentity() function so that it is entity aware.
use Doctrine\ORM\EntityManager,
Doctrine\ORM\UnitOfWork;
class My_Auth extends \Zend_Auth
{
protected $_entityManager;
/**
* override otherwise self::$_instance
* will still create an instance of Zend_Auth
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new self();
}
return self::$_instance;
}
public function getEntityManager()
{
return $this->_entityManager;
}
public function setEntityManager(EntityManager $entityManager)
{
$this->_entityManager = $entityManager;
}
public function getIdentity()
{
$storage = $this->getStorage();
if ($storage->isEmpty()) {
return null;
}
$identity = $storage->read();
$em = $this->getEntityManager();
if(UnitOfWork::STATE_DETACHED === $em->getUnitOfWork()->getEntityState($identity))
{
$identity = $em->merge($identity);
}
return $identity;
}
}
And than add an _init function to your Bootstrap:
public function _initAuth()
{
$this->bootstrap('doctrine');
$em = $this->getResource('doctrine')->getEntityManager();
$auth = My_Auth::getInstance();
$auth->setEntityManager($em);
}
At this point calling $user->getUserGroup()->getName(); should work as intended.
When you store the entity to a session (via Zend_Auth or otherwise), the object is serialized and no longer maintained by Doctrine when subsequently retrieved and unserialized. Try merging the entity back into the EntityManager. See http://www.doctrine-project.org/docs/orm/2.1/en/reference/working-with-objects.html

Categories