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

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, ...)

Related

Doctrine error with LifecycleEventArgs arguments

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

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;
}
}

Injecting EntityManager-dependend service into Listener

I am trying to inject one of my services into an EntityListener in order to call some application specific behaviour when an entity gets updated.
My Logger service, used to store events in a LogEntry entity in my database:
class Logger
{
/**
* #var EntityManager $manager The doctrine2 manager
*/
protected $manager;
//...
}
The listener:
class EntityListener
{
public function __construct(Logger $logger)
{
$this->logger = $logger;
// ...
}
}
And the service definitions in my service.yml:
listener:
class: Namespace\EntityListener
arguments: [#logger]
tags:
- { name: doctrine.event_listener, event: preUpdate }
logger:
class: Namespace\Logger
arguments: [#doctrine.orm.entity_manager]
Unfortunately it results in a ServiceCircularReferenceException:
Circular reference detected for service "doctrine.orm.default_entity_manager", path: "doctrine.orm.default_entity_manager -> doctrine.dbal.default_connection -> listener -> logger".
The problem obviously is that I inject the doctrine into the my service while it is also automatically injected into my listener. How do I proceed? I found a very similar question but the accepted answer is to inject the container which is obviously not favourable.
Any suggestions on how to solve my issue would be appreciated.
Small side note: I would like to avoid a solution depending on lazy services if possible.
First of all I switched from an EventListener to an EventSubscriber. From the docs:
Doctrine defines two types of objects that can listen to Doctrine events: listeners and subscribers. Both are very similar, but listeners are a bit more straightforward.
It turns out one can access the ObjectManager via the passed $args-parameter like so:
/** #var Doctrine\Common\Persistence\ObjectManager $manager */
$manager = $args->getObjectManager();
So either use it directly in the callback:
public function postUpdate(LifecycleEventArgs $args)
{
$manager = $args->getObjectManager();
// ...
...or set it to an object field:
/** #var ObjectManager $manager */
private $manager;
public function postUpdate(LifecycleEventArgs $args)
{
$this->manager = $args->getObjectManager();
// ...
After struggling with the same problem, I found out that using lazy loading solved my issue.
listener:
class: AppBundle\EventListener\OrderDoctrineListener
tags:
- { name: doctrine.event_listener, event: postPersist, lazy: true }

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