I have a doctrine-phpcr-odm document named article,I want to slugify a field before updating each article.
The event fires for doctrine-orm entities but dosn't fire for doctrine-phpcr-odm documents!
class ArticlePreUpdateListener
{
public function preUpdate(LifecycleEventArgs $args)
{
var_dump($args);
}
}
article.pre_update.listener:
class: AppBundle\EventListener\ArticlePreUpdateListener
tags:
- { name: doctrine.event_listener, event: preUpdate}
According to Docs, Doctrine-PHPCR-ODM events works the same way as for Doctrine ORM events. The only differences are:
use the tag name doctrine_phpcr.event_listener resp.
doctrine_phpcr.event_subscriber instead of doctrine.event_listener;
expect the argument to be of class
Doctrine\Common\Persistence\Event\LifecycleEventArgs.
`/**
* #Document
*/
class Article
{
[...]
/**
* #PreUpdate
* #PrePersist
*/
public function slugifiyField()
{
$this->yourField = yourSlugifyFunction($this->yourField);
}
}
Then, add a function with a preUpdate annotation (I've added PrePersist to slugify when article is created too)
Edit : According to your comment, I removed HasLifeCycleCallback annotation, but it looks you can use Pre/PostUpdate annotations directly within document entity.
Related
I have the following event class definition:
use Symfony\Contracts\EventDispatcher\Event;
class CaseEvent extends Event
{
public const NAME = 'case.event';
// ...
}
And I have created a subscriber as follow:
use App\Event\CaseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CaseEventListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [CaseEvent::NAME => 'publish'];
}
public function publish(CaseEvent $event): void
{
// do something
}
}
I have also defined the following at services.yaml:
App\EventSubscriber\CaseEventListener:
tags:
- { name: kernel.event_listener, event: case.event}
Why when I dispatch such event as follow the listener method publish() is never executed?
/**
* Added here for visibility but is initialized in the class constructor
*
* #var EventDispatcherInterface
*/
private $eventDispatcher;
$this->eventDispatcher->dispatch(new CaseEvent($args));
I suspect the problem is kernel.event_listener but not sure in how to subscribe the listener to the event properly.
Change your subscriber so getSubscribedEvents() reads like this:
public static function getSubscribedEvents(): array
{
return [CaseEvent::class => 'publish'];
}
This takes advantage of changes on 4.3; where you no longer need to specify the event name, and makes for the simpler dispatching you are using (dispatching the event object by itself, and omitting the event name).
You could have also left your subscriber as it was; and change the dispatch call to the “old style”:
$this->eventDispatcher->dispatch(new CaseEvent($args), CaseEvent::NAME);
Also, remove the event_listener tags from services.yaml. Since you are implementing EventSubscriberInterface, you do not need to add any other configuration.
I need to create changelog in the API for user actions on entities.
For example:
User updates entity Licensor I need to catch the changes and save them in the database in different table.
The first part I was able to do with Doctrine Event Listener
class ChangelogEventListener
{
public function preUpdate($obj, PreUpdateEventArgs $eventArgs)
{
if ($obj instanceof LoggableInterface) {
dump($eventArgs->getEntityChangeSet());
}
}
}
And with marking entity event listeners
/**
* #ORM\EntityListeners(value={"AppBundle\EventSubscriber\Changelogger\ChangelogEventListener"})
*/
class Licensor implements LoggableInterface
But I'm not sure if it's even possible and if it makes sense to access the ORM entity manager in a preUpdate event.
If it isn't then what's the proper way to do it?
I've tried with Symfony's EventListener instead of Doctrine's but then I don't have access to getEntityChangeSet().
Check out Doctrine events, and specifically the preUpdate event. This event is the most restrictive, but you do have access to all of the fields that have changed, and their old/new values. You can change the values here on the entity being updated, unless it's an associated entity.
Check out this answer, which suggests using an event subscriber, and then persisting to a logging entity.
There is also this blog post that uses the preUpdate event to save a bunch of changesets to the internal listener class, then postFlush it persists any entities that are being changed, and calls flush again. However, I would not recommend this, as the Doctrine documentation explicitly states:
postFlush is called at the end of EntityManager#flush().
EntityManager#flush() can NOT be called safely inside its listeners.
If you went the route of that blog post you'd be better off using the onFlush() event and then doing your computeChangeSets() call after your persist(), like the first answer I posted.
You can find a similar example here:
You are better off using an event listener for such thing. What you want is more like a database trigger to log changes. See example below (tested and works fine) which logs User entity changes in UserAudit entity. For demonstration purposes, it only watches username and password field but you can modify it as you wish.
Note: If you want an entity listener then look at this example.
services.yml
services:
application_backend.event_listener.user_entity_audit:
class: Application\BackendBundle\EventListener\UserEntityAuditListener
arguments: [ #security.context ]
tags:
- { name: doctrine.event_listener, event: preUpdate }
- { name: doctrine.event_listener, event: postFlush }
UserEntityAuditListener
namespace Application\BackendBundle\EventListener;
use Application\BackendBundle\Entity\User;
use Application\BackendBundle\Entity\UserAudit;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Symfony\Component\Security\Core\SecurityContextInterface;
class UserEntityAuditListener
{
private $securityContext;
private $fields = ['username', 'password'];
private $audit = [];
public function __construct(SecurityContextInterface $securityContextInterface)
{
$this->securityContext = $securityContextInterface;
}
public function preUpdate(PreUpdateEventArgs $args) // OR LifecycleEventArgs
{
$entity = $args->getEntity();
if ($entity instanceof User) {
foreach ($this->fields as $field) {
if ($args->getOldValue($field) != $args->getNewValue($field)) {
$audit = new UserAudit();
$audit->setField($field);
$audit->setOld($args->getOldValue($field));
$audit->setNew($args->getNewValue($field));
$audit->setUser($this->securityContext->getToken()->getUsername());
$this->audit[] = $audit;
}
}
}
}
public function postFlush(PostFlushEventArgs $args)
{
if (! empty($this->audit)) {
$em = $args->getEntityManager();
foreach ($this->audit as $audit) {
$em->persist($audit);
}
$this->audit = [];
$em->flush();
}
}
}
is it possible to create a relation to a generic table/class whith Doctrine?
Here is some code to make it easier to understand:
// class Log...
// TODO:
// It could be useful to have a reference to
// the element mentioned by the log, the issue is
// we don't know what kind of entity it is.
/**
* #ORM\ManyToOne(targetEntity="???")
*/
private $elementId
Maybe instead of using targetEntity I could just use an int that is the id of the element located in the unknow table.
There is no built-in possibility now.
Let me propose a work around using Doctrine Lifecycle Events :
Create 3 properties :
/*
* #ORM\Column(name="element_class", type="string")
*/
private $elementClass
/*
* #ORM\Column(name="element_id", type="integer")
*/
private $elementId
// Not mapped
private $element
public function setElement($element)
{
$this->element = $element;
$this->elementClass = get_class($element);
$this->elementId = $element->getId();
}
public function getElement()
{
return $this->element;
}
// You need these for the PostLoad event listener :
public function hydrateElementPostLoad($element)
{
$this->element = $element;
}
public function getElementClass()
{
return $this->elementClass;
}
public function getElementId()
{
return $this->elementId;
}
Then create a PostLoadListener able to hydrate the element property :
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\Log;
class PostLoadListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if($entity instanceOf Log){
$em = $args->getEntityManager();
$entity->hydrateElementPostLoad(
$this->em->getRepository($entity->getElementClass())->findOneById($entity->getElementId())
);
}
}
}
And register this event in your services.yml :
services:
places.listener:
class: AppBundle\EventListener\PostLoadListener
tags:
- { name: doctrine.event_listener, event: postLoad }
That's also how the most famous Bundle for logging works (The Gedmo DoctrineExtensions Logger)
To retrieve all logs for an entity, create a repository method for your Log entity :
getLogs($entity)
{
return $this->_em->findBy(array(
'element_id'=>$entity->getId(),
'element_class'=>get_class($entity)
));
}
You are trying to manage some abstraction of one or more of your entities in the database level which is a headache,
Doctrine already has proposed Somme solutions to manage this kind of abstractions by using Inheritance Mapping
A mapped superclass is an abstract or concrete class that provides persistent entity state and mapping information for its subclasses, but which is not itself an entity. Typically, the purpose of such a mapped superclass is to define state and mapping information that is common to multiple entity classes.
For more information check this
In a Symfony2 application, I have an entity that needs to be populated on pre-persist with various context properties (like user id, what page it was called from, etc.)
I figured that to do this, I need to add a doctrine event listener that has access to "service_container", and the best way to give such access is to pass "service_container" as an argument to this listener.
I have a specific entity that I want to listen to, and I do not want to trigger the listener to events with any other entity.
We can add an entity-specific listener, documentation is found here:
http://docs.doctrine-project.org/en/latest/reference/events.html#entity-listeners
- but this does not provide example of how to pass an argument (I use PHP annotations to declare the listener).
I also tried to use JMSDiExtraBundle annotations, like in the example below:
http://jmsyst.com/bundles/JMSDiExtraBundle/master/annotations#doctrinelistener-or-doctrinemongodblistener
- but this way requires to declare the listener as non-entity-specific
Is there any way to make a listener for one entity only, and have it have access to container?
One of the ways similar to doctrine docs through dependency injection:
<?php
namespace AppBundle\EntityListener;
use AppBundle\Entity\User;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\RouterInterface;
class UserListener {
/**
* #var LoggerInterface
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function postPersist(User $user, LifecycleEventArgs $args)
{
$logger = $this->logger;
$logger->info('Event triggered');
//Do something
}
}
services:
user.listener:
class: AppBundle\EntityListener\UserListener
arguments: [#logger]
tags:
- { name: doctrine.orm.entity_listener }
And dont forget add listener to entity mapping:
AppBundle\Entity\User:
type: entity
table: null
repositoryClass: AppBundle\Entity\UserRepository
entityListeners:
AppBundle\EntityListener\UserListener: ~
I would simply check entity type from the event. If you check type inside or outside the subscriber, it has the same performance cost. And simple type condition is fast enough.
namespace App\Modules\CoreModule\EventSubscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
class SetCountryToTaxSubscriber implements EventSubscriber
{
/**
* {#inheritdoc}
*/
public function getSubscribedEvents()
{
return [Events::prePersist];
}
public function prePersist(LifecycleEventArgs $lifecycleEventArgs)
{
$entity = $lifecycleEventArgs->getEntity();
if ( ! $entity instanceof Tax) {
return;
}
$entity->setCountry('myCountry');
}
}
For a Symfony 2.1 project, I'm trying to create a new annotation #Json() that will register a listener that will create the JsonResponse object automatically when I return an array. I've got it working, but for some reason the listener is always called, even on methods that don't have the #Json annotation. I'm assuming my approach works, since the Sensio extra bundle does this with the #Template annotation.
Here is my annotation code.
<?php
namespace Company\Bundle\Annotations;
/**
* #Annotation
*/
class Json extends \Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationAnnotation
{
public function getAliasName()
{
return 'json';
}
}
Here is my listener code.
<?php
namespace Company\Bundle\Listener\Response\Json;
class JsonListener
{
//..
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
$data = $event->getControllerResult();
if(is_array($data) || is_object($data)) {
if ($request->attributes->get('_json')) {
$event->setResponse(new JsonResponse($data));
}
}
}
}
This is my yaml definition for the listener.
json.listener:
class: Company\Bundle\Listener\Response\Json
arguments: [#service_container]
tags:
- { name: kernel.event_listener, event: kernel.view, method: onKernelView }
I'm obviously missing something here because its being registered as a kernel.view listener. How do I change this so that it is only called when a #Json() annotation is present on the controller action?
Not pretend to be the definitive answer.
I'm not sure why your are extending ConfigurationAnnotation: its constructor accepts an array, but you don't need any configuration for your annotation. Instead, implement ConfigurationInterface:
namespace Company\Bundle\Annotations;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface;
/**
* #Annotation
*/
class Json implements ConfigurationInterface
{
public function getAliasName()
{
return 'json';
}
public function allowArray()
{
return false;
}
}
Sensio ControllerListener from SensionFrameworkExtraBundle will read your annotation (merging class with methods annotations) and perform this check:
if ($configuration instanceof ConfigurationInterface) {
if ($configuration->allowArray()) {
$configurations['_'.$configuration->getAliasName()][] = $configuration;
} else {
$configurations['_'.$configuration->getAliasName()] = $configuration;
}
}
Setting a request attribute prefixed with _. You are correctly checking for _json, so it should work. Try dumping $request->attributes in your view event listener. Be sure that your json.listener service is correctly loaded too (dump them with php app/console container:debug >> container.txt).
If it doesn't work, try adding some debug and print statements here (find ControllerListener.php in your vendor folder):
var_dump(array_keys($configurations)); // Should contain _json
Remember to make a copy of it before edits, otherwise Composer will throw and error when updating dependencies.