I am working on an application using Symfony 3 and Twig templates. I have created forms using symfony formBuilder. I need to run a php script every time a row is inserted in database. Is there anyway that I can do this ?
yes of course, you can use the Events and Event Listeners https://symfony.com/doc/current/event_dispatcher.html or Doctrine Event Listeners and Subscribers https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html
First, to run a script, you can use the Process component of Symfony.
Here is an example of usage:
$phpBinaryFinder = new PhpExecutableFinder();
$phpBinaryPath = $phpBinaryFinder->find();
$process = new Process("{$phpBinaryPath} worker.php");
$process->run();
You should read the related doc for more insights.
Then you want to hook after the flush of doctrine, then use an event listener. It's a class with a specific method that you register as a service.
You need to define a class:
namespace App\EventListener;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
class YourListener
{
private $persisted = [];
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof YourRecord) {
return;
}
$this->persisted[] = $entity;
}
public function postFlush(PostFlushEventArgs $args)
{
foreach ($persisted as $row) {
// Execute your action for the given row
}
}
}
Then you need to register it as service:
# services.yaml
services:
App\EventListener\YourListener:
tags:
- { name: doctrine.event_listener, event: postPersist }
- { name: doctrine.event_listener, event: postFlush }
Check the related documentation: https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html
Related
Trying to register a Doctrine EventSubscriber but nothing is ever actually fired.
I have, on the Entity, in question, set the #ORM\HasLifeCycleCallbacks annotation.
Here's the Subscriber:
<?php
namespace App\Subscriber;
use App\Entity\User;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserPasswordChangedSubscriber implements EventSubscriber
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function getSubscribedEvents()
{
return [Events::prePersist, Events::preUpdate, Events::postLoad];
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
private function updateUserPassword(User $user)
{
$plainPassword = $user->getPlainPassword();
if (!empty($plainPassword)) {
$encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword);
$user->setPassword($encodedPassword);
$user->eraseCredentials();
}
}
}
The part that is making this particuarly frustrating is that this same code and configuration was fine in Symfony 3 whe autowiring was turned off and I manually coded all my services.
However, now, even if I manually code up a service entry for this, in the usual way, still nothing happens.
EDIT:
Here is my services.yaml after trying what suggested Domagoj from the Symfony docs:
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
It didn't work. Interestingly, If I un-implement the EventSubscriber interface, Symfony throws an exception (rightly). Yet my break points in the code are completely ignored.
I've considered an EntityListener, but it cannot have a constructor with arguments, doesn't have access to the Container and I shouldn't have to; this ought to work :/
I ended up figuring this out. The field that I was specifically updating was transient, and therefore Doctrine didn't consider this an Entity change (rightly).
To fix this, I put
// Set the updatedAt time to trigger the PreUpdate event
$this->updatedAt = new DateTimeImmutable();
In the Entity field's set method and this forced an update.
I also did need to manually register the Subscriber in the services.yaml using the following code. symfony 4 autowiring wasn't auto enough for a Doctrine Event Subscriber.
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
For your first problem, doctrine event subscribers are not autoconfigured/auto-tagged. For the reasons and solutions, you have some responses here.
Personnaly, I just have one Doctrine ORM mapper, so I put this in my services.yaml file :
services:
_instanceof:
Doctrine\Common\EventSubscriber:
tags: ['doctrine.event_subscriber']
You have to register your Event Listener as a service and tag it as doctrine.event_listener
https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html#configuring-the-listener-subscriber
I use symfony and easy admin and I want to listen event easy_admin.pre_persist.
In easy admin controller it calls this event like this:
$this->dispatch(EasyAdminEvents::PRE_PERSIST, array('entity' => $entity));
and this consts:
/** #Event("Symfony\Component\EventDispatcher\GenericEvent") */
const PRE_PERSIST = 'easy_admin.pre_persist';
If I in the same controller add listener for this event like this:
$ed = $this->get('event_dispatcher');
$ed->addListener('easy_admin.pre_persist', function($e) {
echo 'it works!';
die();
});
...it works.
But I want to add this listener in something other place. I think services.yml will be good place for it. I've read in Sf documentation i should add service this way:
# app/config/services.yml
services:
app.exception_listener:
class: AppBundle\EventListener\ExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception }
but there is 2 properies - name and event. I know only event name, easy_admin.pre_persist.
How it works? What is name for this event and what is event?
If I can add listener for easy_admin.pre_persist how can I add this listener to services.yml?
I'll give you a very simple example but you need to adapt it to yours. It should be fairly simple. It will just give you a fair idea, not a real life example.
For more examples: http://www.inanzzz.com/index.php/posts/symfony
UserController.php
class UserController
{
....
public function createAction()
{
$user = new User();
$user->setUsername('username');
$user->setPassword('password');
$this->entityManager->persist($user);
$this->entityManager->flush();
}
....
}
Services.yml
services:
application_backend.listener.user_entity:
class: Application\BackendBundle\Listener\UserEntityListener
tags:
- { name: doctrine.event_listener, event: prePersist }
UserControllerListener.php
namespace Application\BackendBundle\Listener;
use Application\BackendBundle\Entity\User;
use Doctrine\ORM\Event\LifecycleEventArgs;
class UserEntityListener
{
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof User) {
// Do whatever you want
}
}
}
You need to know the class of the listener and the name of the method to call. Both are depending on easy admin (sorry, never used it). So look into easy admin's code
You can see in the event definition you posted that use symfony generic events, so you should listen to the kernel and the tag name property should be kernel.event_listener
# app/config/services.yml
services:
app.exception_listener:
class: YourBundle\EventListener\EasydminPrePersitListener
tags:
- { name: kernel.event_listener, event: easy_admin.pre_persist, method: onPrePersist }
And in the Listener class you mus define a method onPrePersist()
I have this event listener below, but it is not working:
<?php
namespace Project\BackendBundle\EventListener;
//src/Project/BackendBundle/EventListener/ClippedImagesManager.php
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Project\BackendBundle\Entity\Subitem;
class ClippedImagesManager
{
public function preUpdate(LifecycleEventArgs $args)
{
die("Event listener!!!");
}
//src/Project/BackendBundle/Resources/config/services.yml
services:
project.clipped_images_manager:
class: Project\BackendBundle\EventListener\ClippedImagesManager
tags:
- { name: doctrine.event_listener, event: preUpdate }
I expected "Event listener!!" was fired when updating any entity inside BackendBundle.
I had a similar issue before.
Stripped off example below is same as yours but to see the full working example visit the post please. The trick is, persisting after preUpdate() within postFlush() event.
Note: Although this might not be the best solution, it could be done with an Event Subscriber or simple onFlush() -> $uow->getScheduledEntityUpdates() in an Event Listener.
Service.yml
services:
entity.event_listener.user_update:
class: Site\FrontBundle\EventListener\Entity\UserUpdateListener
tags:
- { name: doctrine.event_listener, event: preUpdate }
Event Listener
<?php
namespace Site\FrontBundle\EventListener\Entity;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Site\FrontBundle\Entity\User;
class UserUpdateListener
{
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// False check is compulsory otherwise duplication occurs
if (($entity instanceof User) === false) {
// Do something
}
}
}
I have a Doctrine filter in Symfony2 project. I am trying to set filter's parameter to some value (taken from session) on every request.
The problem is that filter object is created after Symfony's onKernelRequest event, so I can't set it from there. If I try to set it in Doctrine's postConnect event circular dependency is detected:
ServiceCircularReferenceException: Circular reference detected for service "doctrine.orm.private_entity_manager", path: "routing.loader -> assetic.asset_manager -> twig -> translator.default -> doctrine.orm.private_entity_manager -> doctrine.dbal.private_connection -> year_visibility.parameter_setter".
The question is, where (or rather how) should I set filter's parameter?
You can try to define filters manually and pass required parameters at the same time.
services:
app.filter_manager:
class: App\Bundle\AppBundle\Filter\FilterManager
arguments: [#doctrine.orm.entity_manager, #session]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
And in the filter manager class:
// ...
public function __construct(EntityManager $em, SessionInterface $session)
{
$this->em = $em;
$this->session = $session;
}
// ...
public function onKernelRequest()
{
$this->em->getConfiguration()->addFilter('filter_name', 'Filter/Class/Name/With/Ns');
$filter = $this->em->getFilters()->enable('filter_name');
$filter->setParameter('param_name', $this->session->get('param_name'));
}
As seen here: https://stackoverflow.com/a/14650403/244058 ,
you can have an instance of your Filter class at kernel boot.
So, your instance would be available very early.
<?php
class MyBundle extends Bundle
{
public function boot()
{
$em = $this->container->get('doctrine.orm.default_entity_manager');
$conf = $em->getConfiguration();
$conf->addFilter(
'filter_name',
'Doctrine\Filter\TestFilter'
);
// either enable it here, or later in the event listener
$em->getFilters()->enable('filter_name');
}
}
After that, just add a kernel.event_listener that listens on kernel.request and set a filter parameter (something like this):
<?php
class DoctrineSqlFilterConfigurator
{
private $em; // inject the entity manager somehow (ctor is a good idea)
public function onKernelRequest(GetResponseEvent $event)
{
$filter = $this->em->getFilters()->enable('filter_name');
$filter->setParameter('param_name', $event->getRequest()->getSession()->get('param_name'));
}
}
I want add new Feed item on entity persist and update. I write this event listener (postUpdate is same):
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$em = $args->getEntityManager();
if ($entity instanceof FeedItemInterface) {
$feed = new FeedEntity();
$feed->setTitle($entity->getFeedTitle());
$feed->setEntity($entity->getFeedEntityId());
$feed->setType($entity->getFeedType());
if($entity->isFeedTranslatable()) {
$feed->getEnTranslation()->setTitle($entity->getFeedTitle('en'));
}
$em->persist($feed);
$em->flush();
}
}
But I got
Integrity constraint violation: 1062 Duplicate entry '30-2' for key
'PRIMARY'
and in log a have two insertations:
INSERT INTO interview_scientificdirection (interview_id,
scientificdirection_id) VALUES (?, ?) ([30,2]) INSERT INTO
interview_scientificdirection (interview_id, scientificdirection_id)
VALUES (?, ?) ([30,2])
scientificdirection is Many to Many relationship table for entity what we want to persist.
In frontend application everything work fine, but in Sonata Admin I got this problem :(
If you need to persist additional objects, the postPersist or postUpdate handler in Doctrine is, sadly, not the right place to go. I struggled with the same problem today, as I needed to generate some message entries in that handler.
The problem at this point is that the postPersist handler is called during the flush event, and not after. So you can't persist additional objects here, as they are not getting flushed afterwards. Additionally, you can't call flush during an postPersist handler, as this might lead to ducplicate entries (as you have experienced).
One way to go is using the onFlush handler from doctrine, documented here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/events.html#onflush
This is just problematic if you need the inserted ids of the database object, as the entity hasn't yet been written to the database in that handler. If you don't need those ids, you are fine with the onFlush event in doctrine.
For me, the solution was a little different. I'm currently working on a symfony2 project, and needed the ids of the inserted database objects (for callbacks and updates later on).
I created a new service in symfony2, which basically just acts like a queue for my messages. During the postPersist update, I just fill the entries in the queue. I have another handler registered on kernel.response, which then takes those entries and persists them to the database. (Something along the line of this: http://symfony.com/doc/current/cookbook/service_container/event_listener.html)
I hope I don't digress too much from the topic here, but as it is something I really struggled with, I hope that some people might find this useful.
The service entries for this are:
amq_messages_chain:
class: Acme\StoreBundle\Listener\AmqMessagesChain
amqflush:
class: Acme\StoreBundle\Listener\AmqFlush
arguments: [ #doctrine.orm.entity_manager, #amq_messages_chain, #logger ]
tags:
- { name: kernel.event_listener, event: kernel.response, method: onResponse, priority: 5 }
doctrine.listener:
class: Acme\StoreBundle\Listener\AmqListener
arguments: [ #logger, #amq_messages_chain ]
tags:
- { name: doctrine.event_listener, event: postPersist }
- { name: doctrine.event_listener, event: postUpdate }
- { name: doctrine.event_listener, event: prePersist }
You can't use the doctrine.listener for this, as this leads to a circular dependency (as you need the entity manager for the service, but the entity manager needs the service....)
That worked like a charm. If you need more info on that, don't hesitate to ask, I'm glad to add some examples to this.
The answer from Francesc is wrong, as the changesets in the postFlush event are already empty.
The second answer of jhoffrichter could work, but is overkill.
The right way to go is to persist the entity in the postPersist event and to call flush again in the postFlush event. But you have to do this only if you changed something in the postPersist event, otherwise you create an endless loop.
public function postPersist(LifecycleEventArgs $args) {
$entity = $args->getEntity();
$em = $args->getEntityManager();
if($entity instanceof FeedItemInterface) {
$feed = new FeedEntity();
$feed->setTitle($entity->getFeedTitle());
$feed->setEntity($entity->getFeedEntityId());
$feed->setType($entity->getFeedType());
if($entity->isFeedTranslatable()) {
$feed->getEnTranslation()->setTitle($entity->getFeedTitle('en'));
}
$em->persist($feed);
$this->needsFlush = true;
}
}
public function postFlush(PostFlushEventArgs $eventArgs)
{
if ($this->needsFlush) {
$this->needsFlush = false;
$eventArgs->getEntityManager()->flush();
}
}
The solution from jhoffrichter is working very well. If you use Console Commands you should add a tag for the event command.terminate. Otherwise it is not working inside Console Commands. See https://stackoverflow.com/a/19737608/1526162
config.yml
amq_messages_chain:
class: Acme\StoreBundle\Listener\AmqMessagesChain
amqflush:
class: Acme\StoreBundle\Listener\AmqFlush
arguments: [ #doctrine.orm.entity_manager, #amq_messages_chain, #logger ]
tags:
- { name: kernel.event_listener, event: kernel.response, method: onResponse, priority: 5 }
- { name: kernel.event_listener, event: command.terminate, method: onResponse }
doctrine.listener:
class: Acme\StoreBundle\Listener\AmqListener
arguments: [ #logger, #amq_messages_chain ]
tags:
- { name: doctrine.event_listener, event: postPersist }
- { name: doctrine.event_listener, event: postUpdate }
- { name: doctrine.event_listener, event: prePersist }
Well, heres how i have done in SF 2.0 and 2.2:
Listener class:
<?php
namespace YourNamespace\EventListener;
use Doctrine\ORM\Mapping\PostPersist;
/*
* ORMListener class
*
* #author: Marco Aurélio Simão
* #description: Listener para realizar operações em qualquer objeto manipulado pelo Doctrine 2.2
*/
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\Common\EventArgs;
use Doctrine\ORM\Mapping\PrePersist;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Mapping\PostUpdate;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Enova\EntitiesBundle\Entity\Entidades;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Enova\EntitiesBundle\Entity\Tagged;
use Enova\EntitiesBundle\Entity\Tags;
class ORMListener
{
protected $extra_update;
public function __construct($container)
{
$this->container = $container;
$this->extra_update = false;
}
public function onFlush(OnFlushEventArgs $args)
{
$securityContext = $this->container->get('security.context');
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$cmf = $em->getMetadataFactory();
foreach ($uow->getScheduledEntityInsertions() AS $entity)
{
$meta = $cmf->getMetadataFor(get_class($entity));
$this->updateTagged($em, $entity);
}
foreach ($uow->getScheduledEntityUpdates() as $entity)
{
$meta = $cmf->getMetadataFor(get_class($entity));
$this->updateTagged($em, $entity);
}
}
public function updateTagged($em, $entity)
{
$entityTags = $entity->getTags();
$a = array_shift($entityTags);
//in my case, i have already sent the object from the form, but you could just replace this part for new Object() etc
$uow = $em->getUnitOfWork();
$cmf = $em->getMetadataFactory();
$meta = $cmf->getMetadataFor(get_class($a));
$em->persist($a);
$uow->computeChangeSet($meta, $a);
}
}
Config.yml:
services:
updated_by.listener:
class: YourNamespace\EventListener\ORMListener
arguments: [#service_container]
tags:
- { name: doctrine.event_listener, event: onFlush, method: onFlush }
Hope it helps ;)