Doctrine error with LifecycleEventArgs arguments - symfony

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

Related

EasyAdmin - How to show custom Entity property properly which use EntityRepository

I would like to show on EasyAdmin a custom property, here is an example :
class Book
{
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
public $id;
/**
* #ORM\Column(type="string")
*/
public $name;
/**
* #ORM\Column(type="float")
*/
public $price;
public function getBenefit(): float
{
// Here the method to retrieve the benefits
}
}
In this example, the custom parameter is benefit it's not a parameter of our Entity and if we configure EasyAdmin like that, it works !
easy_admin:
entities:
Book:
class: App\Entity\Book
list:
fields:
- { property: 'title', label: 'Title' }
- { property: 'benefit', label: 'Benefits' }
The problem is if the function is a bit complexe and need for example an EntityRepository, it becomes impossible to respect Controller > Repository > Entities.
Does anyone have a workaround, maybe by using the AdminController to show custom properties properly in EasyAdmin ?
You shouldn't put the logic to retrieve the benefits inside the Book entity, especially if it involves external dependencies like entityManager.
You could probably use the Doctrine events to achieve that. Retrieve the benefits after a Book entity has been loaded from the DB. Save the benefits before or after saving the Book entity in the DB.
You can find out more about it here https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html
class Book
{
...
public $benefits;
}
// src/EventListener/RetrieveBenefitListener.php
namespace App\EventListener;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use App\Entity\Book;
class RetrieveBenefitListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
// only act on some "Book" entity
if (!$entity instanceof Book) {
return;
}
// Your logic to retrieve the benefits
$entity->benefits = methodToGetTheBenefits();
}
}
// src/EventListener/SaveBenefitListener.php
namespace App\EventListener;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use App\Entity\Book;
class SaveBenefitListener
{
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getObject();
// only act on some "Book" entity
if (!$entity instanceof Book) {
return;
}
// Your logic to save the benefits
methodToSaveTheBenefits($entity->benefits);
}
}
// services.yml
services:
App\EventListener\RetrieveBenefitListener:
tags:
- { name: doctrine.event_listener, event: postLoad }
App\EventListener\SaveBenefitListener:
tags:
- { name: doctrine.event_listener, event: postUpdate }
This is just an example, I haven't tested the code. You will probably have to add the logic for the postPersist event if you create new Book objects.
Depending on the logic to retrieve the benefits (another DB call? loading from an external API?), you might want to to approach the problem differently (caching, loading them in your DB via a cron job, ...)

How to run a lookup query inside Doctrine2 Entity with Symfony3

I have a basic Doctrine2 entity, but one of the fields needs some formatting applied to it to turn it from a database primary key, into a user-visible "friendly ID".
I want to put the formatting logic in only one place, so that if it ever changes, it only has to be updated once.
Part of the formatting involves looking up a string from the database and using that as a prefix, as this value will be different for different installations. I am a bit stuck, because within the entity I can't (and probably shouldn't) look up the database to retrieve this prefix.
However I am not sure how else to go about this.
Here is some pseudocode illustrating what I am trying to do:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
// This is also an entity, annotations/getters/setters omitted for brevity.
class Lookup {
protected $key;
protected $value;
}
class Person {
/**
* Database primary key
*
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* Get the person's display ID.
*
* #Serializer\VirtualProperty
* #Serializer\SerializedName("friendlyId")
*/
protected function getFriendlyId()
{
if ($this->person === null) return null;
//$prefix = 'ABC';
// The prefix should be loaded from the DB, somehow
$lookup = $this->getDoctrine()->getRepository('AppBundle:Lookup')->find('USER_PREFIX');
$prefix = $lookup->getValue();
return $prefix . $this->person->getId();
}
}
You could use event listeners using symfony and doctrine and listen to postLoad event by registering the service
services:
person.postload.listener:
class: AppBundle\EventListener\PersonPostLoadListener
tags:
- { name: doctrine.event_listener, event: postLoad }
Now in your listener you will have an access to entity manager
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\Person;
class PersonPostLoadListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof Person) {
return;
}
$entityManager = $args->getEntityManager();
$lookup =$entityManager->getRepository('AppBundle\Entity\Lookup')->findOneBy(array(
'key'=> 'USER_PREFIX'
));
$entity->setFriendlyId($entity->getId().$lookup->getValue());
//echo "<pre>";dump($entity);echo "</pre>";die('Call')
}
}
And in your person entity you need to define an un mapped property for your id and its getter and setter method like
class Person
{
private $friendlyId;
public function getFriendlyId()
{
return $this->friendlyId;
}
public function setFriendlyId($friendlyId)
{
return $this->friendlyId = $friendlyId;
}
}

Symfony2/Doctrine: Get the field(s) that changed after "Loggable" entity changed

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+

Symfony2 - Set default value in entity constructor

I can set a simple default value such as a string or boolean, but I can't find how to set the defualt for an entity.
In my User.php Entity:
/**
* #ORM\ManyToOne(targetEntity="Acme\DemoBundle\Entity\Foo")
*/
protected $foo;
In the constructor I need to set a default for $foo:
public function __construct()
{
parent::__construct();
$this->foo = 1; // set id to 1
}
A Foo object is expected and this passes an integer.
What is the proper way to set a default entity id?
I think you're better to set it inside a PrePersist event.
In User.php:
use Doctrine\ORM\Mapping as ORM;
/**
* ..
* #ORM\HasLifecycleCallbacks
*/
class User
{
/**
* #ORM\PrePersist()
*/
public function setInitialFoo()
{
//Setting initial $foo value
}
}
But setting a relation value is not carried out by setting an integer id, rather it's carried out by adding an instance of Foo. And this can be done inside an event listener better than the entity's LifecycleCallback events (Because you'll have to call Foo entity's repository).
First, Register the event in your bundle services.yml file:
services:
user.listener:
class: Tsk\TestBundle\EventListener\FooSetter
tags:
- { name: doctrine.event_listener, event: prePersist }
And the FooSetter class:
namespace Tsk\TestBundle\EventListener\FooSetter;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Tsk\TestBundle\Entity\User;
class FooSetter
{
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof User) {
$foo = $entityManager->getRepository("TskTestBundle:Foo")->find(1);
$entity->addFoo($foo);
}
}
}
I would stay well away from listeners in this simple example, and also passing the EntityManager into an entity.
A much cleaner approach is to pass the entity you require into the new entity:
class User
{
public function __construct(YourEntity $entity)
{
parent::__construct();
$this->setFoo($entity);
}
Then elsewhere when you create a new entity, you will need to find and pass the correct entity in:
$foo = [find from entity manager]
new User($foo);
--Extra--
If you wanted to go further then the creation of the entity could be in a service:
$user = $this->get('UserCreation')->newUser();
which could be:
function newUser()
{
$foo = [find from entity manager]
new User($foo);
}
This would be my preferred way
You can't just pass the id of the relationship with 'Foo'. You need to retrieve the Foo entity first, and then set the foo property. For this to work, you will need an instance of the Doctrine Entity Manager. But then, you make your entity rely on the EntityManager, this is something you don't want.
Example:
// .../User.php
public function __construct(EntityManager $em) {
$this->em = $em;
$this->foo = $this->em->getRepository('Foo')->find(1);
}

How to assign roles on successful registration?

I'm using fos user bundle and pugx multi user bundle.
I've read all the documentation and I'm new to Symfony.
In the pugx multi user bundle there's a sample on every point but one: sucessful registration.
Samples of overriding controllers for generating forms => ok
Samples of overriding templates for generating forms => ok
Samples of overriding successful registration sample => nothing.
Here's my code:
class RegistrationController extends BaseController
{
public function registerAction(Request $request)
{
$response = parent::registerAction($request);
return $response;
}
public function registerTeacherAction()
{
return $this->container
->get('pugx_multi_user.registration_manager')
->register('MyBundle\Entity\PersonTeacher');
}
public function registerStudentAction()
{
return $this->container
->get('pugx_multi_user.registration_manager')
->register('MyBundle\Entity\PersonStudent');
}
}
The problem is with ->get('pugx_multi_user.registration_manager') which returns a manager. In the fos user overring controllers help, they get either a form or a form.handler. I'm having hard times to "link" those with the pugx_multi_user manager.
What code should I put in the registerTeacherAction() to set roles for teacher, and in registerStudentAction() to set roles for student on a successful registration?
Solution 1 (Doctrine Listener/Subscriber)
You can easily add a doctrine prePersist listener/subscriber that adds the roles/groups to your entities depending on their type before persisting.
The listener
namespace Acme\YourBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\YourBundle\Entity\Student;
class RoleListener
{
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
// check for students, teachers, whatever ...
if ($entity instanceof Student) {
$entity->addRole('ROLE_WHATEVER');
// or
$entity->addGroup('students');
// ...
}
// ...
}
}
The service configuration
# app/config/config.yml or load inside a bundle extension
services:
your.role_listener:
class: Acme\YourBundle\EventListener\RoleListener
tags:
- { name: doctrine.event_listener, event: prePersist }
Solution 2 (Doctrine LifeCycle Callbacks):
Using lifecycle callbacks you can integrate the role-/group-operations directly into your entity.
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Student
{
/**
* #ORM\PrePersist
*/
public function setCreatedAtValue()
{
$this->addRole('ROLE_WHATEVER');
$this->addGroup('students');
}
Solution 3 (Event Dispatcher):
Register an event listener/subscriber for the "fos_user.registration.success" event.
How to create an event listener / The EventDispatcher component.

Resources