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.
Related
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.
So i am sending an email when a certain value on an entity is changed. I only want the email to send after the update in case the update fails for what ever reason. so on the preUpdate I can do this
public function preUpdate(LifecycleEventArgs $args){
if ($args->hasChangedField('value') && is_null($args->getOldValue('value'))) {
$this->sendEmail();
}
}
but i need to do this on postUpdate and as these methods are not available on postUpdate i refactored it to look like this:
public function postUpdate(LifecycleEventArgs $args){
$entity = $args->getEntity();
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($entity);
if ($entity instanceof Entity && isset( $changeSet['value'] ) && empty( $changeSet['value'][0] )) {
$this->sendEmail();
}
}
However this returns an empty change set, but changes have been made and can be seen in preUpdate. Can anyone see what i am doing wrong? help would be much appreciated :)
On preUpdate event you get event object of class PreUpdateEventArgs where You have change set for entity.
On postUpdate you just get event object of class LifecycleEventArgs where you can ask only for Updated entity (and get latest state of it).
If you want to play with changeset then you need to do it before actual updating entity (preUpdate event).
A workaround could be to save change set somewhere by yourself and later retrieve it in postUpdate. It is a siplified exaple I've implement once:
<?php
namespace Awesome\AppBundle\EventListener;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
/**
* Store last entity change set in memory, so that it could be
* usable in postUpdate event.
*/
class EntityChangeSetStorageListener implements EventSubscriber
{
/**
* #var ArrayCache
*/
private $cache;
/**
* #param ArrayCache $cacheStorage
*/
public function __construct(ArrayCache $cacheStorage)
{
$this->cache = $cacheStorage;
}
/**
* Store last entity change set in memory.
*
* #param PreUpdateEventArgs $event
*/
public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
$this->cache->setNamespace(get_class($entity));
$this->cache->save($entity->getId(), $event->getEntityChangeSet());
}
/**
* Release the memory.
*/
public function onClear()
{
$this->clearCache();
}
/**
* Clear cache.
*/
private function clearCache()
{
$this->cache->flushAll();
}
/**
* {#inheritdoc}
*/
public function getSubscribedEvents()
{
return [
Events::preUpdate,
Events::onClear,
];
}
}
Later inject ChangeSetStorage service to the listener where it is necessary on postUpdate event.
I had a really annoying issue with the changeset data, sometimes I got the collection of changes and sometimes not.
I sorted out by adding this line $event->getEntityManager()->refresh($entity); in the prePersist and preUpdate events inside a doctrine.event_subscriber
After the refresh line, changesetdata was updated so the following line started to work:
/** #var array $changeSet */
$changeSet = $this->em->getUnitOfWork()->getEntityChangeSet($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]);
}
}
I've converted a PHP application over to Symfony2 and have yet another structural question...
In my old application I would have entity classes that might act upon other entity classes...for instance, I have a search class and a result class. A function like search->updateSearch() would operate upon the search class, and upon its child result class ($this->result->setFoo('bar'). This is just one example of an entity-related function that doesn't belong in Symfony2's entity class.
From what I can tell it seems like the most symfonyesque method would be to create a service, something along the lines of a searchHelper class, to which I could pass the entity manager, $search, and $result classes, and operate on them there.
Does that sound like the best course of action?
Thank you!
For this scenario I use Model Managers, it's intended to be a business layer ORM agnostic interface for operating with entities. Something like:
<?php
/**
* Group entity manager
*/
class GroupManager
{
/**
* Holds the Doctrine entity manager for database interaction
* #var EntityManager
*/
protected $em;
/**
* Holds the Symfony2 event dispatcher service
* #var EventDispatcherInterface
*/
protected $dispatcher;
/**
* Entity specific repository, useful for finding entities, for example
* #var EntityRepository
*/
protected $repository;
/**
* Constructor
*
* #param EventDispatcherInterface $dispatcher
* #param EntityManager $em
* #param string $class
*/
public function __construct(EventDispatcherInterface $dispatcher, EntityManager $em)
{
$this->dispatcher = $dispatcher;
$this->em = $em;
$this->repository = $em->getRepository($class);
}
/**
* #return Group
*/
public function findGroupBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
/**
* #return Group
*/
public function createGroup()
{
$group = new Group();
// Some initialization or creation logic
return $group;
}
/**
* Update a group object
*
* #param Group $group
* #param boolean $andFlush
*/
public function updateGroup(Group $group, $andFlush = true)
{
$this->em->persist($group);
if ($andFlush) {
$this->em->flush();
}
}
/**
* Add a user to a group
*
* #param User $user
* #param Group $group
* #return Membership
*/
public function addUserToGroup(User $user, Group $group)
{
$membership= $this->em->getRepository('GroupBundle:Membership')
->findOneBy(array(
'user' => $user->getId(),
'group' => $group->getId(),
));
if ($membership && $membership->isActive()) {
return null;
} elseif ($membership && !$membership->isActive()) {
$membership->setActive(true);
$this->em->persist($membership);
$this->em->flush();
} else {
$membership = new Membership();
$membership->setUser($user);
$membership->setGroup($group);
$this->em->persist($membership);
$this->em->flush();
}
$this->dispatcher->dispatch(
GroupEvents::USER_JOINED_GROUP, new MembershipEvent($user, $group)
);
return $membership;
}
And then the service definition:
<service id="app.model_manager.group" class="App\GroupBundle\Entity\GroupManager">
<argument type="service" id="event_dispatcher" />
<argument type="service" id="doctrine.orm.entity_manager" />
</service>
You can inject the logger, mailer, router, or whichever other service you could need.
Take a look to FOSUserBundle managers, to get examples and ideas about how to use them.
It sounds like you should be using doctrines custom repository classes. You can check them out here: http://symfony.com/doc/current/book/doctrine.html#custom-repository-classes
Basically they allow you to add custom logic above and beyond your basic entity. Also because they are basically an extension of the entity it makes it really easy to load them in and use their functions:
//Basic Entity File
/**
* #ORM\Entity(repositoryClass="Namespace\Bundle\Repository\ProductRepo")
*/
class Product
{
//...
}
Then the repo file for that entity:
//Basic Repo File
use Doctrine\ORM\EntityRepository;
class ProductRepo extends EntityRepository
{
public function updateSearch($passedParam)
{
// Custom query goes here
}
}
Then from your controller you can load the repo and use the function:
//Controller file
class ProductController extends Controller
{
public function updateSearchAction()
{
$productRepo = $this->getDoctrine()->getManager()->getRepository('Namespace\Bundle\Entity\Product');
// Set $passedParam to what ever it needs to be
$productRepo->updateSearch($passedParam);
}
}
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