In a Symfony2 project I'm using the Loggable Doctrine Extension.
I saw that there is a LoggableListener.
Is there indeed an event that gets fired when a (loggable) field in a loggable entity changes? If it is so, is there a way to get the list of fields that triggered it?
I'm imagining the case of an entity with, let's say 10 fields of which 3 loggable. For each of the 3 I want to perform some actions if they change value, so 3 actions will be performed if the 3 of them change.
Any idea?
Thank you!
EDIT
After reading the comment below and reading the docs on doctrine's events I understood have 3 options:
1) using lifecycle callbacks directly at the entity level even with arguments if I'm using doctrine >2.4
2) I can listen and subscribe to Lifecycle Events, but in this case the docs say that "Lifecycle events are triggered for all entities. It is the responsibility of the listeners and subscribers to check if the entity is of a type it wants to handle."
3) doing what you suggest, which is using an Entity listener, where you can define at the entity level which is the listener that is going to be "attached" to the class.
Even if the first solution seems easier, I read that "You could also use this listener to implement validation of all the fields that have changed. This is more efficient than using a lifecycle callback when there are expensive validations to call". What's considered an "expensive validation?".
In my case what I have to perform is something like "if field X of entity Y changed than add a notification on the notification table saying "user Z changed the value of X(Y) from A to B"
Which would be the most suitable approach, considering that I have around 1000 fields like those?
EDIT2
To solve my problem I'm trying to inject the service_container service inside the listener, so that I can have access to the dispatcher to dispatch a new event which can perform the persist of new entity I need. But how can I do that?
I tried the usual way, I add the following to the service.yml
app_bundle.project_tolereances_listener:
class: AppBundle\EventListener\ProjectTolerancesListener
arguments: [#service_container]
and of course I added the following to the listener:
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
but I get the following:
Catchable Fatal Error: Argument 1 passed to AppBundle\ProjectEntityListener\ProjectTolerancesListener::__construct() must be an instance of AppBundle\ProjectEntityListener\ContainerInterface, none given, called in D:\provarepos\user\vendor\doctrine\orm\lib\Doctrine\ORM\Mapping\DefaultEntityListenerResolver.php on line 73 and defined
Any idea?
The Loggable listener only saves the changesvalue for the watched properties of your entities over time.
It does not fire an event, it listens to the onFlush and postPersist doctrine events.
I think you are looking for Doctrine listeners on preUpdate and prePersist events where you can manipulate the changeset before a flush.
see: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html
If you are using Doctrine 2.4+ you can add them easily to your entity:
Simple entity class:
namespace Your\Namespace\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\EntityListeners({"Your\Namespace\Listener\DogListener"})
*/
class Dog
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="integer")
*/
private $age;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* #return int
*/
public function getAge()
{
return $this->age;
}
/**
* #param int $age
*/
public function setAge($age)
{
$this->age = $age;
}
}
Then in Your\Namespace\Listener you create the ListenerClass DogListener:
namespace Your\Namespace\Listener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Your\Namespace\Entity\Dog;
class DogListener
{
public function preUpdate(Dog $dog, PreUpdateEventArgs $event)
{
if ($event->hasChangedField('name')) {
$updatedName = $event->getNewValue('name'). ' the dog';
$dog->setName($updatedName);
}
if ($event->hasChangedField('age')) {
$updatedAge = $event->getNewValue('age') % 2;
$dog->setAge($updatedAge);
}
}
public function prePersist(Dog $dog, LifecycleEventArgs $event)
{
//
}
}
Clear the cache and the listener should be called when flushing.
Update
You are right about recomputeSingleEntityChangeSet which was not needed in this case. I updated the code of the listener.
The problem with the first choice (in-entity methods) is that you can't inject other services in the method.
If you only need the EntityManager then yes, it is the easiest way code-wise.
With an external Listener class, you can do so.
If those 1000 fields are in several separate entities, the second type of Listener would be the most suited. You could create a NotifyOnXUpdateListener that would contain all your watch/notification logic.
Update 2
To inject services in an EntityListener declare the Listener as a service tagged with doctrine.orm.entity_listener and inject what you need.
<service id="app.entity_listener.your_service" class="Your\Namespace\Listener\SomeEntityListener">
<argument type="service" id="logger" />
<argument type="service" id="event_dispatcher" />
<tag name="doctrine.orm.entity_listener" />
</service>
and the listener will look like:
class SomeEntityListener
{
private $logger;
private $dispatcher;
public function __construct(LoggerInterface $logger, EventDispatcherInterface $dispatcher)
{
$this->logger = $logger;
$this->dispatcher = $dispatcher;
}
public function preUpdate(Block $block, PreUpdateEventArgs $event)
{
//
}
}
According to: How to use Doctrine Entity Listener with Symfony 2.4? it requires DoctrineBundle 1.3+
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
I'm trying to record every change in quantity of a given item. For that purpose, I listen for a change of an Item entity and wish to create a new Transaction instance with details about the action. So I'm creating an entity inside a listener.
I've set up everything according to the documentation and created the listener based on this example.
The code (I believe) is relevant for my problem is following.
ItemListener
// ...
private $log;
/** #ORM\PreUpdate */
public function preUpdateHandler (Item $item, PreUpdateEventArgs $args)
{
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($item)['quantity'];
$quantityChange = $changeSet[1] - $changeSet[0];
$transaction = new Transaction();
$transaction->setItem($item);
$transaction->setQuantityChange($quantityChange);
$this->log = $transaction;
}
/** #ORM\PostUpdate */
public function postUpdateHandler(Item $item, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$em->persist($this->log);
$em->flush();
}
This works perfectly. However, the problem is when I add another field to the transaction entity. The user field inside Transaction entity has ManyToOne relation. Now when I try to set the user inside the preUpdateHandler, it leads to and undefined index error inside the UnitOfWork function of the Entity Manager.
Notice: Undefined index: 000000003495bf92000000001108e474
The listener is now like this. I retreive the user based on the token that was sent with the request. Therefore, I inject the request stack and my custom user provider in the listener's constructor. I do not think this is the source of the problem. However, if necessary, I'll edit the post and add all the remaining code (rest of the listener, services.yaml and user provider).
ItemListener
// ...
private $log;
/** #ORM\PreUpdate */
public function preUpdateHandler (Item $item, PreUpdateEventArgs $args)
{
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($item)['quantity'];
$quantityChange = $changeSet[1] - $changeSet[0];
$transaction = new Transaction();
$transaction->setItem($item);
$transaction->setQuantityChange($quantityChange);
$request = $this->requestStack->getCurrentRequest();
$company = $this->userProvider->getUserByRequest($request);
$this->log = $transaction;
}
/** #ORM\PostUpdate */
public function postUpdateHandler(Item $item, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$em->persist($this->log);
$em->flush();
}
I do not understand why retreiving the flush with retrieval of another entity leads to that error. When searching for an answer I found that that many recommend not to use flush() inside the postUpdate cycle but rather in postFlush. However, this method is not defined for Entity listeners according to the documentation and if possible, I'd like to stick to such a listener and not an event listener.
Thank you for any help. I also include the transaction entity code just in case.
Transaction Entity
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\DoctrineUtils\MagicAccessors;
use App\Entity\T\TIdentifier;
/**
* #ORM\Entity
* #ORM\Table(name="transaction")
*/
class Transaction
{
use TIdentifier;
use MagicAccessors;
/**
* #ORM\ManyToOne(targetEntity="Item")
* #ORM\JoinColumn(name="item_id", referencedColumnName="id", nullable=false)
*/
public $item;
/**
* #ORM\Column(type="decimal", length=14, precision=4, nullable=false)
*/
public $quantityChange;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $createdTime;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
public function __construct()
{
$this->createdTime = new \DateTime();
}
/**
* #param mixed $quantityChange
*/
public function setQuantityChange(int $quantityChange): void
{
$this->quantityChange = $quantityChange;
}
/**
* #param mixed $createdTime
*/
public function setCreatedTime($createdTime): void
{
$this->createdTime = $createdTime;
}
/** #ORM\PrePersist **/
public function onCreate() : void
{
$this->setCreatedTime(new \DateTime('now'));
}
public function setUser(?User $user): self
{
$this->user= $user;
return $this;
}
}
I found out that the problem was that another instance of the entity manager was instantiated in the getUserByRequest() function, where I log that the user's token was used. Apart others, I created inside it a new manager, persisted the entry and flushed the result. However, the new entity manager does not know about the unit of work inside the other entity manager inside the listener. Hence the undefined index error.
I tried to omit the persist and the flush part inside the user getter function, but that was not enough. In the end I solved the problem by passing the given instance entity manager from inside the listener to the getter function. So basically, I ended up calling this from the preUpdateHandler function inside the listener.
$em = $args->getEntityManager();
$company = $this->userProvider->getUserByRequest($request, $em);
Hope this helps if you find yourself in a similar pickle.
I'm tying to create one to many relations
A have class
class Interview {
/**
* #OneToMany(targetEntity="Question", mappedBy="question")
*/
private $questions;
public function __construct() {
$this->questions = new ArrayCollection();
}
public function __toString() {
return $this->id;
}
/**
* #return Collection|Question[]
*/
public function getQuestions() {
return $this->questions;
}
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
......
}
another
class Question {
/**
* #ManyToOne(targetEntity="Interview", inversedBy="interview")
* #JoinColumn(name="interview_id", referencedColumnName="id")
*/
private $interview;
public function getInterview() {
return $this->interview;
}
public function setInterview(Interview $interview) {
$this->interview = $interview;
return $this;
}
/**
* #ORM\Column(type="integer")
* #ORM\Id
*/
private $interview_id;
......
}
and Controller for all this
if ($form->isSubmitted() && $form->isValid()) {
$interview = new Interview();
$question = new Question();
$em->persist($interview);
$question->setInterview($interview);
$question->setTitle($request->get('title'));
$em->persist($question);
$em->flush();
return $this->redirectToRoute('homepage');
}
i'm receiving an error:
Entity of type AppBundle\Entity\Question is missing an assigned ID for
field 'interview_id'. The identifier generation strategy for this
entity requires the ID field to be populated before
EntityManager#persist() is called. If you want automatically generated
identifiers instead you need to adjust the metadata mapping
accordingly.
Don't understand what the problem and how to fix it.
To enforce loading objects from the database again instead of serving them from the identity map. You can call $em->clear(); after you did $em->persist($interview);, i.e.
$interview = new Interview();
$em->persist($interview);
$em->clear();
It seems like your project config have an error in doctrine mapped part.
If you want automatically generated identifiers instead you need to
adjust the metadata mapping accordingly.
Try to see full doctrine config and do some manipulation with
auto_mapping: false
to true as example or something else...
Also go this , maybe it will be useful.
I am sure, its too late to answer but maybe someone else will get this error :-D
You get this error when your linked entity (here, the Interview entity) is null.
Of course, you have already instantiate a new instance of Interview.But, as this entity contains only one field (id), before this entity is persited, its id is equal to NULL. As there is no other field, so doctrine think that this entity is NULL. You can solve it by calling flush() before linking this entity to another entity
I have a symfony entity that has a not mapped calculated field
namespace AppBundle\Entity;
class Page
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* Page count. Non-mapped
*
* #var integer
*/
protected $pageCount;
}
The $pageCount value is obtainable by consuming a remote service that will provide the value for use in the application.
I figured the best way is to use the postLoad event to handle this.
class PageListener
{
/**
* #ORM\PostLoad
*/
public function postLoad(LifecycleEventArgs $eventArgs)
{
// ...
}
}
I need to retrieve this value when loading values.
public function indexAction()
{
// I want to fetch the pageHits here
$pagesListing = $this->getDoctrine()
->getRepository('AppBundle:Pages')
->findAll();
// I don't want to fetch the pageHits here
$pagesListing2 = $this->getDoctrine()
->getRepository('AppBundle:Pages')
->findAll();
}
However, this will ALWAYS result in a call to a remote service.
There may be cases where I do not want the service to be invoked, so that it reduced a performance load on the application.
How can I fetch the remote values automatically, but only when I want to.
Your "problem" is pretty common and one of the reasons I never use Doctrine repositories directly.
Solution I would recommend
Always make custom repository services and inject Doctrine into them.
That way, if you want to merge some data from some other data source (eg. Redis, filesystem, some remote API), you have complete control over it and process is encapsulated.
Example:
class PageRepository
{
private $em;
private $api;
public function __construct(EntityManagerInterface $em, MyAwesomeApi $api)
{
$this->em = $em;
$this->api = $api;
}
public function find($id)
{
return $em->getRepository(Page::class)->find($id);
}
public function findAll()
{
return $em->getRepository(Page::class)->findAll();
}
public function findWithCount($id)
{
$page = $this->find($id);
$count = $this->myAwesomeApi->getPageCount($id);
return new PageWithCount($page, $count);
}
}
Solution I wouldn't recommend, but works :)
If you don't want to change your code structure and want to keep it as it is, you could make a really simple change that will make your pageCount be loaded only when it is necessary:
Move code from Page::postLoad method into Page::getPageCount()
Example:
public function getPageCount()
{
if (null === $this->pageCount) {
$this->pageCount = MyAwesomeApi::getPageCount($this->id);
}
return $this->pageCount;
}
This way, pageCount will only be loaded if something tries to access it.
I have setup some event listeners for my Doctrine entities which are working fine, but I'd like some automated tests on these for peace of mind. Ideally these tests wouldn't hit the database to maintain some level of performance when testing.
Here is my code (simplified) for my user entity which makes sure the password is encrypted.
// UserBundle/Entity/User.php
/**
* #ORM\EntityListeners({"UserBundle\EventListener\UserListener"})
*/
class User implements UserInterface, \Serializable
{
// ...
}
-
// UserBundle/EventListener/UserListener.php
/**
* #Service
* #Tag("doctrine.orm.entity_listener")
*/
class UserListener
{
/**
* #var Container
*/
protected $container;
/**
* #DI\InjectParams({
* "container" = #DI\Inject("service_container")
* })
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* #ORM\PreUpdate
*/
public function preUpdateHandler(User $user, PreUpdateEventArgs $args)
{
$this->getUserManager()->updatePassword($user);
}
/**
* #return UserManager
*/
public function getUserManager()
{
return $this->container->get('user_manager');
}
}
All I'm aiming to test here is that Doctrine is firing the event on update and that the method updatePassword is actually called on my user manager. This is my test so far but I can't work out how to fire the event without carrying out any database queries.
// UserBundle/Tests/EventListener/UserListenerTest.php
class UserListenerTest extends KernelAwareTest
{
public function testPreUpdate()
{
$user = new User();
$userManager = $this->getMockBuilder('UserBundle\Service\UserManager')
->disableOriginalConstructor()
->getMock();
$userManager->expects($this->once())
->method('updatePassword')
->with($this->equalTo($user));
$this->container->set('user_manager', $userManager);
// TODO: how to test?
}
If I understand your issue the right way, what you want to achieve is to test that your listener is correctly called when Doctrine fires the update event and not to test that the event is effectively fired.
As veNuker said, you must trust that Doctrine will call your listener when the event is fired because it is its responsibility.
However if you want to test the behavior of your listener, you can try https://stackoverflow.com/a/22830909/2721918