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
Related
I want to use Listener in my project with postLoad method but I got an error
[TypeError] App\Company\Infrastructure\Persistence\ORM\EventListener\LoadLicensesListener::postLoad(): Argument #1 ($args) must be of type Do
ctrine\ORM\Event\LifecycleEventArgs, App\Company\Domain\Entity\Company given, called in D:\OpenServer\domains\project\vendor\doctrine\orm\lib\Doc
trine\ORM\Event\ListenersInvoker.php on line 108
My Listener
use Doctrine\ORM\Event\LifecycleEventArgs;
final class LoadLicensesListener
{
/**
* #param LifecycleEventArgs $args
*/
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
if (!$entity instanceof Copmany) {
// Something to do
$licenses = $entity->relatedLicenses;
$entity->initializeObject($licenses);
}
}
}
And I registered it in Company.orm.xml
<entity-listeners>
<entity-listener class="App\Company\Infrastructure\Persistence\ORM\EventListener\LoadLicensesListener">
<lifecycle-callback type="postLoad" method="postLoad"/>
</entity-listener>
</entity-listeners>
services.yml
App\Company\Infrastructure\Persistence\ORM\EventListener\LoadLicensesListener:
tags:
- { name: doctrine.event_listener, event: postLoad, connection: default }
Where did I go wrong? Maybe I misunderstood the documentation - Symfony Events or Doctrine Events
Or I should do something in services.yml because I've changed a folder with EventListeners?
"doctrine/orm": "2.8.4"
Doctrine provide different type of listeners, "Default" event listener and Entity Listener, here your registered an entity listener in your file Company.orm.xml and also for the same class a "default" event listener.
Choose which type of listener you want and register it according to the documentation.
If you choose a Entity Listener then the first argument will be the Entity itself, that's why you get this error.
I would say it looks like you've configured it wrong.
try to implement postLoad method inside your Campany.php (Note! Without any params) and see what it outputs.
class Company {
// ...
public function postLoad() {
dump(__METHOD__);
}
}
also take a look at this https://symfony.com/doc/4.1/doctrine/event_listeners_subscribers.html and this one https://symfony.com/doc/current/bundles/DoctrineBundle/entity-listeners.html
I am unfortunately not familiar with xml-configs, so I can't spot anything suspicious.
As always, there are several ways to get it done:
simple EntityLifeCycles (docs) - useful for basic stuff and if you don't rely on additional services for this particular task. Logic applies only for that specific Entity.
an Doctrine\Common\EventSubscriber with getSubscribedEvents - more advanced and flexible. One logic could be applied for several entities
an EventListener.
So here are examples for symfony 4.4 and doctrine 2.7:
Entity LifeCylcles:
/**
* #ORM\Entity()
* #ORM\Table(name="company")
* #ORM\HasLifecycleCallbacks
*/
class Company {
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
// ... props and methods
/**
* #ORM\PostLoad()
*/
public function doStuffAfterLoading(): void
{
// yor logic
// you can work with $this as usual
// no-return values!
// dump(__METHOD__);
}
}
with these annotations no extra entries in services.yml|xml necessary
Subscriber - to apply same logic for one or several Entities
use App\Entity\Company;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
final class PostLoadSubscriber implements EventSubscriber {
public functuin __construct()
{
// you can inject some additional services if you need to
// e.g. EntityManager
}
public function getSubscribedEvents()
{
return [
Events::postLoad,
];
}
public function postLoad(LifecycleEventArgs $args)
{
// check if it's right entity and do your stuff
$entity = $args->getObject();
if ($entity instanceof Company) {
// magic...
}
}
}
You need to register this PostLoadSubscriber as a service in services.yaml|xml
sry if something is not so accurate, but im less experienced with Symfony
I have the following orm mapping:
src/app/ExampleBundle/Resources/config/doctrine/Base.orm.yml
app\ExampleBundle\Entity\Base:
type: mappedSuperclass
fields:
createdAt:
type: datetime
nullable: true
options:
default: null
updatedAt:
type: datetime
nullable: true
options:
default: null
This creates a entity Base which i modified to be abstract
src/app/ExampleBundle/Entity/Base.php
abstract class Base {
...
}
I have some other entities they extend this abstract class e.g.
src/app/ExampleBundle/Entity/Category.php
class Category extends Base
{
...
}
Now i tried to add a listener that sets the createdAt/updatedAt datetime on every persist for every entity that extends the Base Entity
src/app/ExampleBundle/EventListener/BaseListener.php
namespace app\ExampleBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\User\UserInterface;
use app\ExampleBundle\Entity\Base;
class BaseListener
{
protected $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function prePersist(Base $base, LifecycleEventArgs $event)
{
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof UserInterface) {
$user = null;
}
if ($base->getCreatedAt() === null) {
$base->setCreated($user, new \DateTime());
} else {
$base->setUpdated($user, new \DateTime());
}
}
}
And added it to the bundles services.yml
src/app/ExampleBundle/Resources/config
services:
app\ExampleBundle\EventListener\BaseListener:
arguments: ['#security.token_storage']
tags:
- { name: doctrine.orm.entity_listener, entity: app\ExampleBundle\Entity\Base, event: prePersist }
Symfony throws no Exception, but the defined event seems also not triggered.
I tried to change the entity param in services to the "real" entity Category, but still no error, nor the event triggered.
I think, i did everything as it is decribed in the documentation. But it still not working.
The command
debug:event-dispatcher
does also not show the event
So, the question is: What did i wrong?
Here the documentation I follow https://symfony.com/doc/3.4/doctrine/event_listeners_subscribers.html
The prePersist method is called for all the entities so you must exclude non instance of app\ExampleBundle\Entity\Base. The first argument is LifecycleEventArgs.
public function prePersist(LifecycleEventArgs $event)
{
$base = $event->getObject();
if (!$base instanceof Base) {
return;
}
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof UserInterface) {
$user = null;
}
if ($base->getCreatedAt() === null) {
$base->setCreated($user, new \DateTime());
} else {
$base->setUpdated($user, new \DateTime());
}
}
I can recommend you StofDoctrineExtensionsBundle (Timestampable) that does exactly what you want. It based on DoctrineExtensions.
There is even a trait that works like a charm.
After some research, many more tests, diving into the EntityManager and the UnitOfWork. Nothing seems to work fine. I get it so far to work on doctrine:fixtures:load, but for any reason they still not working if i use the entity manager in the Controllers. So, i decided to try another way with a subscriber.
tags:
- { name: doctrine.event_subscriber }
class ... implements EventSubscriber
So i still dont know why the Listener did not work as expected, but with the subscribers i found a solution that does.
Thanks to all of you for support :)
For Symfony 5 and anybody who will struggle with issue when Events::loadClassMetadata is not fired in subscriber.
Class should implement "EventSubscriberInterface"
class DiscriminatorSubscriber implements EventSubscriberInterface
{
public function getSubscribedEvents()
{
return [Events::loadClassMetadata];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $event): void
{
}
Than you dont have to do anythng more if you have autoconfigure and autowire on TRUE in your services.yml
Doctrine will handle your subscriber registration (check DoctrineExtension.php)
$container->registerForAutoconfiguration(EventSubscriberInterface::class)
->addTag('doctrine.event_subscriber');
I use FOSUserEvents after submit form but the subscriber call twice.
In this way my captcha is valid the first time and not valid the second
this is my code
<?php
namespace AppBundle\EventListener;
class CaptchaSubscriber implements EventSubscriberInterface
{
private $router;
private $requestStack;
private $templating;
/**
* RedirectAfterRegistrationSubscriber constructor.
*/
public function __construct(RouterInterface $router, RequestStack $requestStack, \Twig_Environment $templating)
{
$this->router = $router;
$this->requestStack = $requestStack;
$this->templating = $templating;
}
public function onRegistrationInit(GetResponseUserEvent $event)
{
if ($this->requestStack->getMasterRequest()->isMethod('post')) {
...handle captcha...
}
}
public static function getSubscribedEvents()
{
return [
FOSUserEvents::REGISTRATION_INITIALIZE => 'onRegistrationInit'
];
}
}
my symfony is 3.3
UPDATE
I added
$event->stopPropagation();
with this snippet the code works, but i don't know if it is the best practice
In my case of symfony 4.2 it depends on the service definition if it occures or not.
My Subscriber gets registered twice if I define the service like this:
# oauth process listener
app.subscriber.oauth:
class: App\EventListenerSubscriber\OauthSubscriber
arguments: ['#session', '#router', '#security.token_storage', '#event_dispatcher', '#app.entity_manager.user', '#app.fos_user.mailer.twig_swift']
tags:
- { name: kernel.event_subscriber }
But it gets registerd only once if I chenge the definition to this:
# oauth process listener
App\EventListenerSubscriber\OauthSubscriber:
arguments: ['#session', '#router', '#security.token_storage', '#event_dispatcher', '#app.entity_manager.user', '#app.fos_user.mailer.twig_swift']
tags:
- { name: kernel.event_subscriber }
I posted a bug report on github and got immediately an answer, that in newer symfony versions event listeners and subscribers get registered automatically with their class name as key (under some default conditions - must read on that topic).
So there is no need to register them explicitely as services.
I we do this anyway, but using an arbitrary key instead of class name, there will be two services.
If you are using Autowiring/Autoconfiguration, it's possible that you've added the subscriber service you show above, twice. I've done it myself when I first added the autowiring, but I also had the subscriber listed explicitly in the configuration as well.
You can see what events are registered (and check if any are registered more than once to perform the same service/action) with:
bin/console debug:event-dispatcher
I'm building a multitenancy backend using Symfony 2.7.9 with FOSRestBundle and JMSSerializerBundle.
When returning objects over the API, I'd like to hash all the id's of the returned objects, so instead of returning { id: 5 } it should become something like { id: 6uPQF1bVzPA } so I can work with the hashed id's in the frontend (maybe by using http://hashids.org)
I was thinking about configuring JMSSerializer to set a virtual property (e.g. '_id') on my entities with a custom getter-method that calculates the hash for the id, but I don't have access to the container / to any service.
How could I properly handle this?
You could use a Doctrine postLoad listener to generate a hash and set a hashId property in your class. Then you could call expose the property in the serializer but set the serialized_name as id (or you could just leave it at hash_id).
Due to the hashing taking place int the postLoad you would need to refresh your object if you have just created it using $manager->refresh($entity) for it take effect.
AppBundle\Doctrine\Listener\HashIdListener
class HashIdListsner
{
private $hashIdService;
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$reflectionClass = new \ReflectionClass($entity);
// Only hash the id if the class has a "hashId" property
if (!$reflectionClass->hasProperty('hashId')) {
return;
}
// Hash the id
$hashId = $this->hashIdService->encode($entity->getId());
// Set the property through reflection so no need for a setter
// that could be used incorrectly in future
$property = $reflectionClass->getProperty('hashId');
$property->setAccessible(true);
$property->setValue($entity, $hashId);
}
}
services.yml
services:
app.doctrine_listsner.hash_id:
class: AppBundle\Doctrine\Listener\HashIdListener
arguments:
# assuming your are using cayetanosoriano/hashids-bundle
- "#hashids"
tags:
- { name: doctrine.event_listener, event: postLoad }
AppBundle\Resources\config\serializer\Entity.User.yml
AppBundle\Entity\User:
exclusion_policy: ALL
properties:
# ...
hashId:
expose: true
serialized_name: id
# ...
Thanks a lot for your detailed answer qooplmao.
However, I don't particularly like this approach because I don't intend to store the hashed in the entity. I now ended up subscribing to the serializer's onPostSerialize event in which I can add the hashed id as follows:
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MySubscriber implements EventSubscriberInterface
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public static function getSubscribedEvents()
{
return array(
array('event' => 'serializer.post_serialize', 'method' => 'onPostSerialize'),
);
}
/**
* #param ObjectEvent $event
*/
public function onPostSerialize(ObjectEvent $event)
{
$service = $this->container->get('myservice');
$event->getVisitor()->addData('_id', $service->hash($event->getObject()->getId()));
}
}
What is the best way to access configuration values inside an entity in a symfony 2 application?
I've searched about this and i've found two solutions:
Define the entity as a service and inject the service container to access configuration values
And this approach which defines a class in the same bundle of the entity with static methods that allows to get the parameter value
Is there any other solution? What's the best workaround?
Your entity shouldn't really access anything else, apart from associated entities. It shouldn't really have any connection outwardly to the outside world.
One way of doing what you want would be to use a subscriber or listener to listen to the entity load event and then pass that value in to the entity using the usual setter.
For example....
Your Entity
namespace Your\Bundle\Entity;
class YourClass
{
private $parameter;
public function setParameter($parameter)
{
$this->parameter = $parameter;
return $this;
}
public function getParameter()
{
return $this->parameter;
}
...
}
Your Listener
namespace Your\Bundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Your\Bundle\Entity\YourEntity;
class SetParameterSubscriber implements EventSubscriber
{
protected $parameter;
public function __construct($parameter)
{
$this->parameter = $parameter;
}
public function getSubscribedEvents()
{
return array(
'postLoad',
);
}
public function postLoad(LifecycleEventArgs $args)
{
/** #var YourEntity $entity */
$entity = $args->getEntity();
// so it only does it to your YourEntity entity
if ($entity instanceof YourEntity) {
$entity->setParameter($this->parameter);
}
}
}
Your services file.
parameters:
your_bundle.subscriber.set_parameter.class:
Your\Bundle\EventListener\SetParameterSubscriber
// Should all be on one line but split for readability
services:
your_bundle.subscriber.set_parameter:
class: %your_bundle.subscriber.set_parameter.class%
arguments:
- %THE PARAMETER YOU WANT TO SET%
tags:
- { name: doctrine.event_subscriber }
You shouldn't need a configuration in your entity.
For example you have File entity and you need to save a file represented by this entity to a disk. You need some parameter, let say "upload_dir". You can pass somehow this parameter to the entity and define a method inside this entity which saves a file to upload dir. But better way would be create a service which would be responsible for saving files. Then you can inject configurtion into it and in save method pass entity object as an argument.