Symfony onFlush Doctrine Listener - php

Hi I have an onFlush listener:
<?php
namespace FM\AppBundle\EventListener;
use FM\AdminBundle\Entity\Address\DeliveryAddress;
use Doctrine\ORM\Event\OnFlushEventArgs;
class DeliveryAddressListener
{
/**
* #param OnFlushEventArgs $args
*/
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof DeliveryAddress) {
$this->addPostalToUser($entity, $args);
}
}
}
/**
* #param DeliveryAddress $deliveryAddress
* #param OnFlushEventArgs $args
*/
public function addPostalToUser(DeliveryAddress $deliveryAddress, OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$user = $deliveryAddress->getOwner();
$user->setPostalCode($deliveryAddress->getZipCode());
}
}
service.yml:
delivery_address.listener:
class: FM\AppBundle\EventListener\DeliveryAddressListener
tags:
- { name: doctrine.event_listener, event: onFlush }
I'm trying to set the new zipCode to the User. But it does not seem to work.
Even when I'm adding a $em->persist($user).
I'm looking throught this doc: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#onflush
But I don't understand how I can make it works with this explanation:
If you create and persist a new entity in onFlush, then calling EntityManager#persist() is not enough. You have to execute an additional call to $unitOfWork->computeChangeSet($classMetadata, $entity).

When manipulating fields, they should be done in the preUpdaet/prePersist.
AppBundle/EventSubscriber/EntitySubscriber.php
namespace AppBundle\EventSubscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
class EntitySubscriber implements EventSubscriber
{
private $now;
public function __construct()
{
$this->now = \DateTime::createFromFormat('Y-m-d h:i:s', date('Y-m-d h:i:s'));
}
public function getSubscribedEvents()
{
return [
'prePersist',
'preUpdate'
];
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if (method_exists($entity, 'setCreatedAt')) {
$entity->setUpdatedAt($this->now);
}
if (method_exists($entity, 'setUpdatedAt')) {
$entity->setUpdatedAt($this->now);
}
}
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if (method_exists($entity, 'setUpdatedAt')) {
$entity->setUpdatedAt($this->now);
}
}
}
services.yml
app.entity_subscriber:
class: AppBundle\EventSubscriber\EntitySubscriber
tags:
- { name: doctrine.event_subscriber, connection: default }

If you need to create an object, persist it and flush it in your listener, then tlorens' answer won't work, as Doctrine docs mention that this must be done with an onFlush Event.
Initial question was how to make it work following docs advice:
If you create and persist a new entity in onFlush, then calling EntityManager#persist() is not enough. You have to execute an additional call to $unitOfWork->computeChangeSet($classMetadata, $entity).
And this is a way to achieve this:
/**
* #param OnFlushEventArgs $eventArgs
*/
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof User) {
$uow->computeChangeSets();
$changeSet = $uow->getEntityChangeSet($entity);
// In this exemple, User has a boolean property 'enabled' and a log will be created if it is passed to 'false'
if ($changeSet && isset($changeSet['enabled']) && $changeSet['enabled'][1] === false) {
$log = new Log();
$em->persist($log);
$uow->computeChangeSet($em->getClassMetadata(get_class($log)), $log);
}
}
}

Well it works when I use that:
// Remove event, if we call $this->em->flush() now there is no infinite recursion loop!
$eventManager->removeEventListener('onFlush', $this);
My Listener
namespace FM\AppBundle\EventListener;
use FM\AdminBundle\Entity\Address\DeliveryAddress;
use Doctrine\ORM\Event\OnFlushEventArgs;
class DeliveryAddressListener
{
/**
* #param OnFlushEventArgs $args
*/
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$eventManager = $em->getEventManager();
// Remove event, if we call $this->em->flush() now there is no infinite recursion loop!
$eventManager->removeEventListener('onFlush', $this);
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof DeliveryAddress) {
$this->addPostalToUser($entity, $args);
}
}
}
/**
* #param DeliveryAddress $deliveryAddress
* #param OnFlushEventArgs $args
*/
public function addPostalToUser(DeliveryAddress $deliveryAddress, OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$user = $deliveryAddress->getOwner();
$user->setPostalCode($deliveryAddress->getZipCode());
$em->flush();
}
}

Related

Symfony 5 PreUpdate change value for only one true

Hello and thank you for your concern.
i'am student and it's my first time creating an eventSubriber.
I would like to change all my attribute "display" (Boolean) in my Entity "Menu" when i try to update one of my entity "Menu" for have only one Attribute "display" true in all my entities.
I use EasyAdmin 3 in my project Symfony if you need to know.
My probleme start with $this->entityManager->flush(); in UniqueBoolTureEvent.php i think.
thank you in advance.
Service.yaml
App\EventListener\UniqueBoolTrueEvent:
tags:
- { name: doctrine.event_listener, event: preUpdate, Lazy: true }
UniqueBoolTrueEvent.php
<?php
namespace App\EventListener;
use App\Entity\Menu;
use Doctrine\Common\EventSubscriber;
// Entity to listen
use Doctrine\Persistence\ObjectManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
class UniqueBoolTrueEvent implements EventSubscriber {
private $entityManager;
/**
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function getSubscribedEvents() {
return array('preUpdate');//Event to listen
}
public function preUpdate( PreUpdateEventArgs $eventArgs ) {
if ($eventArgs->getEntity() instanceof Menu) {
if ($eventArgs->hasChangedField('display') && $eventArgs->getNewValue('display') == 'true') {
//get the id of entity change for true
$entityId = $eventArgs->getEntity()->getId();
// search the entity already on true exept entity change now
$displayTrue = $this->entityManager->getRepository(Menu::class)->findByDisplay($entityId);
//Change the value for entity already on true exept entity change now
foreach ($displayTrue as $display) {
$display->setDisplay(false);
$this->entityManager->persist($display);
}
$this->entityManager->flush();
}
}
}
}
MenuRepository.php
<?php
namespace App\Repository;
use App\Entity\Menu;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* #method Menu|null find($id, $lockMode = null, $lockVersion = null)
* #method Menu|null findOneBy(array $criteria, array $orderBy = null)
* #method Menu[] findAll()
* #method Menu[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class MenuRepository extends ServiceEntityRepository
{
/**
* #param ManagerRegistry $registry
*/
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Menu::class);
}
/**
* #return Menu[] Returns an array of Menu objects
*/
public function findByDisplay($entityId)
{
return $this->createQueryBuilder('m')
->andWhere('m.display = true')
->andWhere('m.id != :entityId')
->setParameter('entityId', $entityId)
->orderBy('m.id', 'ASC')
->getQuery()
->getResult()
;
}
}
after that i have only one error in my ajax request and cannot see where is the probleme.
Thank you in advance.
My Solution No 2 Solutions lol
First solution with EventSubscriberInterface and BeforeEntityUpdatedEvent. With this solution you don't need to add config in service.yaml
class UniqueBoolTrueEvent implements EventSubscriberInterface
{
private $entityManager;
/**
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public static function getSubscribedEvents()
{
return [
BeforeEntityUpdatedEvent::class => ['setDisplayTrue'],
];
}
public function setDisplayTrue(BeforeEntityUpdatedEvent $event)
{
$entity = $event->getEntityInstance();
$entityId = $entity->getId();
if ($entity instanceof Menu) {
$displayTrue = $this->entityManager->getRepository(Menu::class)->findByDisplay($entityId);
foreach ($displayTrue as $display) {
$display->setDisplay(false);
$this->entityManager->persist($display);
}
$this->entityManager->flush();
}
}
}
And my second solution with onFlush()
service.yaml
App\EventListener\UniqueBoolTrueEvent:
tags:
- { name: doctrine.event_listener, event: onFlush}
UniqueBoolTrueEvent.php
class UniqueBoolTrueEvent
{
private $entityManager;
/**
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$entities = $uow->getScheduledEntityUpdates();
foreach ($entities as $entity) {
//continue only if the object to be updated is a Ticket
if ($entity instanceof Menu) {
$entityId = $entity->getId();
$entityDisplay = $entity->getDisplay();
// //get all the changed properties of the Menu object
// $changes_set = $uow->getEntityChangeSet($entity);
//Check if the changed value of "display" is True
if ($entityDisplay == true) {
//find the entity with "display" value true exept the entity update
$displayTrue = $this->entityManager->getRepository(Menu::class)->findByDisplay($entityId);
// Change the value of "display" to false for each entity on true exept the entity update
foreach ($displayTrue as $display) {
$display->setDisplay(false);
$em->persist($display);
// for flush
$classMetadata = $em->getClassMetadata(get_class($entity));
// dd($classMetadata);
$uow->computeChangeSet($classMetadata, $display);
}
}
}
}
}
}

Custom annotation in symfony 3 controller

So the question is pretty straightforward. I have a code in my controller that has became redundant, and i've decided to make the annotation for it.
if (!$request->getContentType() === 'json' ) {
return new JsonResponse(array('success' => false));
}
$content = $request->getContent();
if(empty($content)){
throw new BadRequestHttpException("Content is empty");
}
$data = json_decode($content, true);
if(empty($data) || !array_key_exists('type', $data)) {
return new JsonResponse(array('success' => false));
}
How do i make custom annotation #CheckRequest in which I can use the $request object as a parameter?
You need to make a custom annotation and then a listener that injects the annotation reader and handles the kernel.controller event:
Annotation
/**
* #Annotation
*/
class CheckRequest
{
}
Service Definition
services:
controller_check_request:
class: AppBundle\EventListener\ControllerCheckRequestListener
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController}
arguments:
- "#annotation_reader"
Listener:
namespace AppBundle\EventListener;
use AppBundle\Annotation\CheckRequest;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class ControllerCheckRequestListener
{
/** #var Reader */
private $reader;
/**
* #param Reader $reader
*/
public function __construct(Reader $reader)
{
$this->reader = $reader;
}
/**
* {#inheritdoc}
*/
public function onKernelController(FilterControllerEvent $event)
{
if (!is_array($controllers = $event->getController())) {
return;
}
$request = $event->getRequest();
$content = $request->getContent();
list($controller, $methodName) = $controllers;
$reflectionClass = new \ReflectionClass($controller);
$classAnnotation = $this->reader
->getClassAnnotation($reflectionClass, CheckRequest::class);
$reflectionObject = new \ReflectionObject($controller);
$reflectionMethod = $reflectionObject->getMethod($methodName);
$methodAnnotation = $this->reader
->getMethodAnnotation($reflectionMethod, CheckRequest::class);
if (!($classAnnotation || $methodAnnotation)) {
return;
}
if ($request->getContentType() !== 'json' ) {
return $event->setController(
function() {
return new JsonResponse(['success' => false]);
}
);
}
if (empty($content)) {
throw new BadRequestHttpException('Content is empty');
}
$data = json_decode($content, true);
if ($request->getContentType() !== 'json' ) {
return $event->setController(
function() {
return new JsonResponse(['success' => false]);
}
);
}
}
}
Notice that instead of returning the response, you set the entire controller with $event->setController();, and you also must return when making that call.
Then in your controller you can set it on the entire class:
use AppBundle\Annotation\CheckRequest;
/**
* #CheckRequest
*/
class YourController extends Controller
{
}
or individual methods/actions:
use AppBundle\Annotation\CheckRequest;
class TestController extends Controller
{
/**
* #Route("/", name="index")
* #CheckRequest
*/
public function indexAction(Request $request)
{
// ...
}
}
For Symfony 3.4.*
public function onKernelController(FilterControllerEvent $event){
if (!is_array($controllers = $event->getController())) {
return;
}
list($controller, $methodName) = $controllers;
$reflectionClass = new \ReflectionClass($controller);
// Controller
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
$classAnnotation = $reader->getClassAnnotation($reflectionClass, AnnotationClass::class);
// Method
$reflectionMethod = $reflectionClass->getMethod($methodName);
$methodAnnotation = $reader->getMethodAnnotation($reflectionMethod, AnnotationClass::class);
if(!($classAnnotation || $methodAnnotation)){
return;
}
/** TODO CODE HERE **/
}

Symfony Event Listener

Hi I'm tring to do a Symfony event listener following this documentation:
http://symfony.com/doc/2.8/cookbook/doctrine/event_listeners_subscribers.html
<?php
namespace FM\AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use FM\AdminBundle\Entity\Address\BillingAddress;
class BillingAdressListener
{
/**
* #param LifecycleEventArgs $args
*/
public function listenBillingAdress(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(!$entity instanceof BillingAddress){
return;
}
$this->postPersist($args);
}
/**
* #param LifecycleEventArgs $args
*/
public function postPersist(LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$billingAdress = $args->getEntity();
dump($billingAdress); die();
}
}
service.yml
billing_adress.listener:
class: FM\AppBundle\EventListener\BillingAdressListener
tags:
- { name: doctrine.event_listener, event: listenBillingAdress }
But nothing is happening when I'm submitting a form with the BillingAddress object.
Did I do something wrong?
It's called Entity Listeners in Doctrine. Here is documentation.
Don't forget to add your entity listener in mapping. For example xml mapping:
<entity name="AppBundle\Entity\AssignmentAttempt" table="app_assignment_attempt" >
<entity-listeners>
<entity-listener class="AppBundle\EntityListener\AssignmentAttemptListener">
<lifecycle-callback type="prePersist" method="prePersist"/>
<lifecycle-callback type="preUpdate" method="preUpdate"/>
</entity-listener>
</entity-listeners>
<!-- Rest mapping -->
</entity>
In config you can simply add
app.entity_listener.assignment_attempt:
class: AppBundle\EntityListener\AssignmentAttemptListener
tags:
- { name: doctrine.orm.entity_listener }
My bad, I was not doing a Persist but an Update.
It works with an Persist.
Here is my code now:
<?php
namespace FM\AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use FM\AdminBundle\Entity\Address\BillingAddress;
class BillingAddressListener
{
/**
* #param LifecycleEventArgs $args
*/
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(!$entity instanceof BillingAddress){
return;
}
$this->addNameToUser($args);
}
/**
* #param LifecycleEventArgs $args
*/
public function addNameToUser(LifecycleEventArgs $args)
{
/** #var BillingAddress $billingAdress */
$billingAdress = $args->getEntity();
$user = $billingAdress->getOwner();
if(empty($user->getFirstName())) $user->setFirstName($billingAdress->getFirstName());
if(empty($user->getLastName())) $user->setLastName($billingAdress->getLastName());
}
}
And I have to call the prePersist method.
I was doing the contrary.
billing_address.listener:
class: FM\AppBundle\EventListener\BillingAddressListener
tags:
- { name: doctrine.event_listener, event: prePersist }

Symfony, Doctrine update not working as expected

Hey All I have this wired error that I cannot overcome. I am using Symfony Framweork and Doctrine for my DB interaction. I am trying to develop a simple CRUD API to grasp some of the concepts.
The actual problem is when I try to update an item in my DB it only works if the ID of the item is inside the DB else I get this error:
Error: Call to a member function setTitle() on a non-object
Have a look at my Repository:
<?php
namespace BooksApi\BookBundle\Repositories;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query\QueryException;
class UpdateBookRepository
{
/**
* #var EntityManager
*/
public $em;
/**
* #param EntityManager $entityManager
*/
public function __construct(
EntityManager $entityManager
){
$this->em = $entityManager;
}
public function updateBook($id, $update)
{
try {
$book = $this->em->getRepository('BooksApiBookBundle:BooksEntity')
->find($id);
$book->setTitle($update);
$this->em->flush();
} catch (\Exception $em) {
throw new QueryException('003', 502);
}
return $book;
}
}
And My factory:
<?php
namespace BooksApi\BookBundle\Repositories;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query\QueryException;
class UpdateBookRepository
{
/**
* #var EntityManager
*/
public $em;
/**
* #param EntityManager $entityManager
*/
public function __construct(
EntityManager $entityManager
){
$this->em = $entityManager;
}
public function updateBook($id, $update)
{
$book = $this->em->getRepository('BooksApiBookBundle:BooksEntity')
->find($id);
if ($book)
{
try {
$book->setTitle($update);
$this->em->flush();
} catch (\Exception $em) {
throw new QueryException('003', 502);
}
return $book;
} else {
return false;
}
}
}
The factory handles the response true or false, So in event where I will try to update an Item thats ID is not in DB the factory should respond with false, 'Unable to Update Book' instead i get the above error, Any idea why guys..?
#Tomazi, you can avoid error by checking if your object exists before calling "setTitle" method.
public function updateBook($id, $update)
{
$book = $this->em->getRepository('BooksApiBookBundle:BooksEntity')->find($id);
if ($book) {
$book->setTitle($update);
$this->em->flush();
return $book;
}
return null;
}
Make sure that $book object is not empty before proceeding with the code. The error indicates that $book is null/empty.
Also, persist your object before using flush.
$this->em->persist($book);
$this->em->flush();

Symfony2 Event Listener preUpdate

I have configured in my service.yml some event listener
registerproduct.listener:
class: App\AppBundle\Listener\RegisterProductListener
arguments: [ #service_container ]
tags:
- { name: doctrine.event_listener, event: prePersist }
- { name: doctrine.event_listener, event: preUpdate }
My method decrementProductQuantity() works really well when I do a prePersist but not with the preUpdate.
During the update the product quantity decrement well, but there is no changes in the database.
Here is my code for the events:
public function prePersist(LifecycleEventArgs $args)
{
$this->decrementProductQuantity($args);
}
public function preUpdate(LifecycleEventArgs $args)
{
$this->decrementProductQuantity($args);
}
public function decrementProductQuantity(LifecycleEventArgs $args)
{
/** #var RegisterProduct $registerProduct */
$registerProduct = $args->getEntity();
if (!($registerProduct instanceof RegisterProduct))
return;
if ($registerProduct->getStatus() == 'inscription') {
$product = $registerProduct->getProduct();
$product->setQuantityStock($product->getQuantityStock() - $registerProduct->getQuantity());
}
}
Here is my update code in my controler:
/**
* #param Register $register
* #return \Symfony\Component\HttpFoundation\Response
*/
public function confirmAction(Request $request, Register $register)
{
$em = $this->getDoctrine()->getManager();
foreach($register->getRegisterProducts() as $registerProduct){
$registerProduct->setStatus('inscription');
$em->persist($registerProduct);
$em->flush();
}
return $this->redirectWithMode($this->generateUrl('asc_activity_register_show', array('register' => $register->getId())));
}
I have no idea why it works when I add something in the database, but not for the update.
Thanks for your help.
On your preUpdate listener you should to recompute changeset for product entity
public function decrementProductQuantity(LifecycleEventArgs $args)
{
/** #var RegisterProduct $registerProduct */
$registerProduct = $args->getEntity();
if (!($registerProduct instanceof RegisterProduct))
return;
if ($registerProduct->getStatus() == 'inscription') {
$product = $registerProduct->getProduct();
$product->setQuantityStock($product->getQuantityStock() - $registerProduct->getQuantity());
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$className = \Doctrine\Common\Util\ClassUtils::getClass($product);
$classMetadata = $em->getClassMetadata($className);
$uow->recomputeSingleEntityChangeSet($classMetadata, $product);
}
}
It happens because in your controller (or somewhere else) your code does not update a product entity. Before listener executing doctrine computes change sets for each entity. Hence in your preUpdate listener doctrine does not know anything about product (because it was not changed).
Try to change the signature of your preUpdate method:
use Doctrine\ORM\Event\PreUpdateEventArgs;
/.../
public function preUpdate(PreUpdateEventArgs $args)
{
$this->decrementProductQuantity($args);
}
/.../
Because I didn't find the awnser I finally choose to do it directly in the controller:
/**
* #param Register $register
* #return \Symfony\Component\HttpFoundation\Response
*/
public function confirmAction(Request $request, Register $register)
{
$mode = $this->getMode($request);
$em = $this->getDoctrine()->getManager();
foreach ($register->getRegisterProducts() as $registerProduct) {
if($registerProduct->getStatus() != 'inscription'){
$product = $registerProduct->getProduct();
$product->setQuantityStock($product->getQuantityStock() - $registerProduct->getQuantity());
}
$registerProduct->setStatus('inscription');
}
$em->flush();
return $this->redirectWithMode($this->generateUrl('asc_activity_register_show', array('register' => $register->getId())), $mode);
}
If someone find the anwser, please tell me.

Categories