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
Related
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();
}
}
}
As an EntityListener is registered as a service, is it possible to register the same class multiple times with different argument and associate each of them with a particular entity ?
Considering the following entities :
/**
* Class EntityA
* #ORM\Entity
* #ORM\EntityListeners({"myBundle\EventListener\SharedListener"})
*/
class EntityA implements sharedBehaviourInterface
{
// stuff here
}
/**
* Class EntityB
* #ORM\Entity
* #ORM\EntityListeners({"myBundle\EventListener\SharedListener"})
*/
class EntityB implements sharedBehaviourInterface
{
// stuff here
}
I would like to register the following listener for both previous entities as this :
class SharedListener
{
private $usefulParameter;
public function __construct($usefulParameter)
{
$this->usefulParameter = $usefulParameter;
}
/**
* #PrePersist
*
*/
public function prePersist(sharedBehaviourInterface $dbFile, LifecycleEventArgs $event)
{
// code here
}
// more methods
}
Using :
mybundle.entitya.listener:
class: myBundle\EventListener\SharedListener
arguments:
- '%entitya.parameter%' # The important change goes here ...
tags:
- { name: doctrine.orm.entity_listener }
mybundle.entityb.listener:
class: myBundle\EventListener\SharedListener
arguments:
- '%entityb.parameter%' # ... and here
tags:
- { name: doctrine.orm.entity_listener }
It does not work, and I'm actually surprised that the EntityListener declaration in the Entity targets the Listener class and not the service. Is it possible to target a specific service instead ? Like :
#ORM\EntityListeners({"mybundle.entityb.listener"})
Or what I'm trying to do isn't even possible ?
You can inject other services into services with the #configured_service_id notation.This works for constructor arguments and setter injection.
Generally spoken: Do not try to find an abstraction where it isn't needed.
Most of the time a little code duplication is far easier in long term.
I would simply built two independent listeners for each purpose.
Do a simple check that jumps out of the handler if the Entity is NOT one of the two Entities that should be handled with the same listener:
<?php
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
class MyEventListener
{
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$entityManager = $args->getObjectManager();
if (!$entity instanceof EntityA && !$entity instanceof EntityB) {
return;
}
/* Your listener code */
}
}
Are you sure, this wouldn't do the trick:
public function prePersist(sharedBehaviourInterface $dbFile, LifecycleEventArgs $event)
{
$entity = $event->getObject();
if ($entity instanceof ClassA) {
// Do something
} elseif ($entity instanceof ClassB) {
// Something else
} else {
// Nah, none of the above...
return;
}
}
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');
}
}
I have an DB table with a UNIQUE column that should contain a unique 8 character alphanumeric string.
I've (finally) making the move from my own MVC framework to symfony. Up until now I would have had a private method in the model that is called on CREATE. A loop in the method would generate a random hash, and perform a READ on the table to see if it is unique: if so, the hash would be returned and injected into the CREATE request.
The problem as I see it is that in symfony I have no access to the repository from within the entity class, so I can't use a lifecycle callback. I understand the reasoning behind this. On the other hand, the hash generation has nothing to do with the controller – for me it is internal logic that belongs in the model. If I later change the data structure, I need to edit the controller.
My question is: architecture-wise, where should I put the hash generation method?
You can use a listener. You were right that the lifecycle callbacks are not the correct solution since you need access to the repository. But you can define a Listener that listens to the same event as the lifecycle callback, but is an service and therefore can have the repository as dependency.
Answering my own question:
I created a custom repository, which has access to the doctrine entity manager.
The repository has a createNewHash method:
class HashRepository extends EntityRepository
{
public function createNewHash()
{
$hash = new Hash();
$hash->setHash($this->_getUniqueHash());
$em = $this->getEntityManager();
$em->persist($hash);
$em->flush();
return $hash;
}
private function _getUniqueHash()
{
$hash = null;
$hashexists = true;
while ($hashexists) {
$hash = $this->_generateRandomAlphaNumericString();
if (!$hashobject = $this->findOneByHash($hash)) {
$hashexists = false;
}
}
return $hash;
}
private function _generateRandomAlphaNumericString( $length=8 )
{
$bits = $length / 2;
return bin2hex(openssl_random_pseudo_bytes($bits));
}
}
The createNewHash() method can then be called from the Controller, and the Controller does not have to concern itself with hash creation.
EDIT: Listeners are another way of doing it.
In your entity constructor, i can add this:
<?php
namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* MyEntity
*
* #ORM\Table(name="my_entity")
*/
class MyEntity
{
/**
* #ORM\Column(type="string", length=8, unique=true, nullable=false)
* #var string
*/
private $uniqId;
public function __construct()
{
$this->uniqId = hash('crc32b', uniqid());
}
// ...
}
Hope this helps
Im seraching over and cannot find answer.
I have database role model in my application. User can have a role but this role must be stored into database.
But then user needs to have default role added from database. So i created a service:
<?php
namespace Alef\UserBundle\Service;
use Alef\UserBundle\Entity\Role;
/**
* Description of RoleService
*
* #author oracle
*/
class RoleService {
const ENTITY_NAME = 'AlefUserBundle:Role';
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function findAll()
{
return $this->em->getRepository(self::ENTITY_NAME)->findAll();
}
public function create(User $user)
{
// possibly validation here
$this->em->persist($user);
$this->em->flush($user);
}
public function addRole($name, $role) {
if (($newrole = findRoleByRole($role)) != null)
return $newrole;
if (($newrole = findRoleByName($name)) != null)
return $newrole;
//there is no existing role
$newrole = new Role();
$newrole->setName($name);
$newrole->setRole($role);
$em->persist($newrole);
$em->flush();
return $newrole;
}
public function getRoleByName($name) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('name' => $name));
}
public function getRoleByRole($role) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('role' => $role));
}
}
my services.yml is:
alef.role_service:
class: Alef\UserBundle\Service\RoleService
arguments: [%doctrine.orm.entity_manager%]
And now I want to use it in two places:
UserController and User entity. How can i get them inside entity?
As for controller i think i just need to:
$this->get('alef.role_service');
But how to get service inside entity?
You don't. This is a very common question. Entities should only know about other entities and not about the entity manager or other high level services. It can be a bit of a challenge to make the transition to this way of developing but it's usually worth it.
What you want to do is to load the role when you load the user. Typically you will end up with a UserProvider which does this sort of thing. Have you read through the sections on security? That should be your starting point:
http://symfony.com/doc/current/book/security.html
The reason why it's so difficult to get services into entities in the first place is that Symfony was explicitly designed with the intent that services should never be used inside entities. Therefore, the best practice answer is to redesign your application to not need to use services in entities.
However, I have found there is a way to do it that does not involve messing with the global kernel.
Doctrine entities have lifeCycle events which you can hook an event listener to, see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events For the sake of the example, I'll use postLoad, which triggers soon after the Entity is created.
EventListeners can be made as services which you inject other services into.
Add to app/config/config.yml:
services:
example.listener:
class: Alef\UserBundle\EventListener\ExampleListener
arguments:
- '#alef.role_service'
tags:
- { name: doctrine.event_listener, event: postLoad }
Add to your Entity:
use Alef\UserBundle\Service\RoleService;
private $roleService;
public function setRoleService(RoleService $roleService) {
$this->roleService = $roleService;
}
And add the new EventListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Service\RoleService;
class ExampleListener
{
private $roleService;
public function __construct(RoleService $roleService) {
$this->roleService = $roleService;
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(method_exists($entity, 'setRoleService')) {
$entity->setRoleService($this->roleService);
}
}
}
Just keep in mind this solution comes with the caveat that this is still the quick and dirty way, and really you should consider redesigning your application the proper way.
Thanks to Kai's answer above which answer to the question, but it's not compatible with symfony 5.x .
It's good to precise it's a bad practice, but required in some special case like legacy code or a bad DB design (as a temporary solution before schema migration)
As in my case, I use this code with a mailer and translator, which introduce an issue with the private property if Symfony >= 5.3 , so here the solution for recent version of symfony:
in config/services.yaml:
services:
Alef\UserBundle\EventListener\ExampleListener:
tags:
- { name: doctrine.event_listener, event: postLoad }
ExampleListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Entity\Role;
class ExampleListener
{
public function postLoad(LifecycleEventArgs $postLoad): void
{
$entity = $postLoad->getEntity();
if ($entity instanceof User) {
$repository = ;
$entity->roleRepository(
$postLoad->getEntityManager()->getRepository(Role::class)
);
}
}
}
And in your Entity (or in a trait if you use it in more than one entity):
use Alef\UserBundle\Service\RoleService;
/** #internal bridge for legacy schema */
public function roleRepository(?RoleRepository $repository = null) {
static $roleRepository;
if (null !== $repository) {
$roleRepository = $repository;
}
return $roleRepository;
}
public function getRoleByName($name) {
return $this->roleRepository()->findBy(array('name' => $name));
}