How can I achieve this:
For example, I have an entity called Issue. I need to log changes of a field of this entity.
If a user changes the field "status" on the Issue entity I need to create a database record about it with the user, who changed the field, the previous status and the new status.
Using: Symfony2 + doctrine2.
You can use an event subscriber for that, and attach it to the ORM event listener (in symfony 2, there's docs about that):
namespace YourApp\Subscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use YourApp\Entity\Issue;
use YourApp\Entity\IssueLog;
class IssueUpdateSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $updated) {
if ($updated instanceof Issue) {
$em->persist(new IssueLog($updated));
}
}
$uow->computeChangeSets();
}
public function getSubscribedEvents()
{
return array(Events::onFlush);
}
}
You can eventually check the changeset as I've explained at Is there a built-in way to get all of the changed/updated fields in a Doctrine 2 entity.
I left the implementation of IssueLog out of the example, since that is up to your own requirements.
Related
Trying to register a Doctrine EventSubscriber but nothing is ever actually fired.
I have, on the Entity, in question, set the #ORM\HasLifeCycleCallbacks annotation.
Here's the Subscriber:
<?php
namespace App\Subscriber;
use App\Entity\User;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserPasswordChangedSubscriber implements EventSubscriber
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function getSubscribedEvents()
{
return [Events::prePersist, Events::preUpdate, Events::postLoad];
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
private function updateUserPassword(User $user)
{
$plainPassword = $user->getPlainPassword();
if (!empty($plainPassword)) {
$encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword);
$user->setPassword($encodedPassword);
$user->eraseCredentials();
}
}
}
The part that is making this particuarly frustrating is that this same code and configuration was fine in Symfony 3 whe autowiring was turned off and I manually coded all my services.
However, now, even if I manually code up a service entry for this, in the usual way, still nothing happens.
EDIT:
Here is my services.yaml after trying what suggested Domagoj from the Symfony docs:
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
It didn't work. Interestingly, If I un-implement the EventSubscriber interface, Symfony throws an exception (rightly). Yet my break points in the code are completely ignored.
I've considered an EntityListener, but it cannot have a constructor with arguments, doesn't have access to the Container and I shouldn't have to; this ought to work :/
I ended up figuring this out. The field that I was specifically updating was transient, and therefore Doctrine didn't consider this an Entity change (rightly).
To fix this, I put
// Set the updatedAt time to trigger the PreUpdate event
$this->updatedAt = new DateTimeImmutable();
In the Entity field's set method and this forced an update.
I also did need to manually register the Subscriber in the services.yaml using the following code. symfony 4 autowiring wasn't auto enough for a Doctrine Event Subscriber.
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
For your first problem, doctrine event subscribers are not autoconfigured/auto-tagged. For the reasons and solutions, you have some responses here.
Personnaly, I just have one Doctrine ORM mapper, so I put this in my services.yaml file :
services:
_instanceof:
Doctrine\Common\EventSubscriber:
tags: ['doctrine.event_subscriber']
You have to register your Event Listener as a service and tag it as doctrine.event_listener
https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html#configuring-the-listener-subscriber
Edited: Many to Many relationship instead of One To Many
Given entities: User & Item.
Item has a boolean property named: $mandatory.
User is related to Many-To-Many Items.
At the creation/construction of a new User, he must be related (initialization) to every Item that has ($mandatory) property set to true.
What is the best practice to ensure these requirements in Symfony3/Doctrine2 ?
Create an event subscriber like explained here:
http://symfony.com/doc/current/doctrine/event_listeners_subscribers.html#creating-the-subscriber-class
public function getSubscribedEvents()
{
return array(
'prePersist',
);
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getObject();
if ($entity instanceof User) {
$entityManager = $args->getEntityManager();
// ... find all Mandatody items and add them to User
}
}
add prePersist function (if you only want on creation) check if it is the User object, get all items from the database that are mandatory and add them to User Entity.
I came to this solution, inspired by #kunicmarko20 's hint above.
I had to subscribe to the preFlush() event, then, use the UnitOfWork object through the PreFlushEventArgs argument to get the entities scheduled for insertion.
If I encounter a User instance of such entities, I just add all mandatory items to it.
Here is the code:
<?php
// src/AppBundle/EventListener/UserInitializerSubscriber.php
namespace AppBundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\PreFlushEventArgs ;
use AppBundle\Entity\User;
use AppBundle\Entity\Item;
class UserInitializerSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
'preFlush',
);
}
public function preFlush (PreFlushEventArgs $args)
{
$em = $args ->getEntityManager();
$uow = $em ->getUnitOfWork();
// get only the entities scheduled to be inserted
$entities = $uow->getScheduledEntityInsertions();
// Loop over the entities scheduled to be inserted
foreach ($entities as $insertedEntity) {
if ($insertedEntity instanceof User) {
$mandatoryItems = $em->getRepository("AppBundle:Item")->findByMandatory(true);
// I've implemented an addItems() method to add several Item objects at once
$insertedEntity->addItems($mandatoryItems);
}
}
}
}
I hope this helps.
This is a Symfony 3 project.
In User entity, i need to implement the method getRoles(). I have a private member $roles that is an array and I added it into serialize and unserialize methods.
public function getRoles()
{
if (count($this->roles) == 0) {
$this->roles = { ... read from db ... };
}
return $this->roles;
}
A issue I'm facing is that in ... read from db ... part, I have to use some parameters from parameters.yml. Usually, $this->container->getParameter(...) does the job. Unfortunately, from an entity I have no access to the container.
My question is: How can I access parameters.yml from an Entity?
Can I somehow inject the required parameters?
Another question is: do I need to serialize $roles as well or should they be read on every request?
--- EDIT ---
That logic seems to me correctly placed.
getRoles() function is supposed to get user's role to Security bundle. It accomplishes it by querying private members and ORM relations. The only problem is that I need do identify certain groups, as they don't have similar names in all deployments. Thats why I need the parameters.yml.
Here is a fragment from User entity, which implements AdvancedUserInterface.
public function getRoles() {
$ADMIN_GRP = "ADMIN_GROUP"; // I need this from parameters.yml
$SUPPORT_GRP = "SUPPORT_GROUP"; // I need this from parameters.yml
$roles = ['ROLE_USER'];
foreach ($this->memberships as $m) {
if ($m->getGroupId() == $SUPPORT_GRP)
array_push($roles, "ROLE_SUPPORT");
if ($m->getGroupId()) == $ADMIN_GRP)
array_push($roles, "ROLE_ADMIN");
}
return $roles;
}
as malcolm said, you should not be touching the EntityManager, from inside your entity, that logic is NOT correctly placed.
also, you should not read parameters.yml from inside your entity
(you COULD)
use Symfony\Component\Yaml\Yaml;
$value = Yaml::parse(file_get_contents('/path/to/file.yml'));
but you really SHOULDNT use the above approach
(you could also add constants to the user entity ...)
Why not adding a group label to your Membership entity ? So you can do...
public function getRoles() {
$roles = ['ROLE_USER'];
foreach ($this->memberships as $m) {
if ($m->getGroupRole() == 'ROLE_SUPPORT')
array_push($roles, "ROLE_SUPPORT");
if ($m->getGroupRole()) == 'ROLE_ADMIN')
array_push($roles, "ROLE_ADMIN");
}
return $roles;
}
What is the best way to access configuration values inside an entity in a symfony 2 application?
I've searched about this and i've found two solutions:
Define the entity as a service and inject the service container to access configuration values
And this approach which defines a class in the same bundle of the entity with static methods that allows to get the parameter value
Is there any other solution? What's the best workaround?
Your entity shouldn't really access anything else, apart from associated entities. It shouldn't really have any connection outwardly to the outside world.
One way of doing what you want would be to use a subscriber or listener to listen to the entity load event and then pass that value in to the entity using the usual setter.
For example....
Your Entity
namespace Your\Bundle\Entity;
class YourClass
{
private $parameter;
public function setParameter($parameter)
{
$this->parameter = $parameter;
return $this;
}
public function getParameter()
{
return $this->parameter;
}
...
}
Your Listener
namespace Your\Bundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Your\Bundle\Entity\YourEntity;
class SetParameterSubscriber implements EventSubscriber
{
protected $parameter;
public function __construct($parameter)
{
$this->parameter = $parameter;
}
public function getSubscribedEvents()
{
return array(
'postLoad',
);
}
public function postLoad(LifecycleEventArgs $args)
{
/** #var YourEntity $entity */
$entity = $args->getEntity();
// so it only does it to your YourEntity entity
if ($entity instanceof YourEntity) {
$entity->setParameter($this->parameter);
}
}
}
Your services file.
parameters:
your_bundle.subscriber.set_parameter.class:
Your\Bundle\EventListener\SetParameterSubscriber
// Should all be on one line but split for readability
services:
your_bundle.subscriber.set_parameter:
class: %your_bundle.subscriber.set_parameter.class%
arguments:
- %THE PARAMETER YOU WANT TO SET%
tags:
- { name: doctrine.event_subscriber }
You shouldn't need a configuration in your entity.
For example you have File entity and you need to save a file represented by this entity to a disk. You need some parameter, let say "upload_dir". You can pass somehow this parameter to the entity and define a method inside this entity which saves a file to upload dir. But better way would be create a service which would be responsible for saving files. Then you can inject configurtion into it and in save method pass entity object as an argument.
Is it possible to run all doctrine queries through a walker of some sort so that I can modify the query based on the current user's credentials? Ideally, I wouldn't have to explicitly call a setHint for a custom walker on every query, as that would restrict my ability to pass the current SecurityContext into the walker.
Also, I'd prefer not to use a Doctrine Filter, as I can't modify join conditions with filters, and I'd be forced to use an "IN" clause, which would severely affect performance
Currently, I'm using a service that modifies the QueryBuilder based on a user's credentials, but this becomes tedious, as I need to call the service every time I create a new QueryBuilder, and is even more of a pain when Repositories come into play (as I'd need to inject the service into every repository that needs to modify the query.
Hopefully I've explained this clearly enough. Appreciate any feedback!
I think I have solved my own issue. If someone else has a more elegant way of doing achieving these results, feel free to explain. In order to modify all of my queries, I have created a custom EntityManager and custom EntityRepository.
In my custom EntityManager, I have overwritten 2 methods. create() and getRepository()
public static function create($conn, Configuration $config, EventManager $eventManager = null)
{
if ( ! $config->getMetadataDriverImpl()) {
throw ORMException::missingMappingDriverImpl();
}
switch (true) {
case (is_array($conn)):
$conn = \Doctrine\DBAL\DriverManager::getConnection(
$conn, $config, ($eventManager ?: new EventManager())
);
break;
case ($conn instanceof Connection):
if ($eventManager !== null && $conn->getEventManager() !== $eventManager) {
throw ORMException::mismatchedEventManager();
}
break;
default:
throw new \InvalidArgumentException("Invalid argument: " . $conn);
}
return new MyCustomEntityManager($conn, $config, $conn->getEventManager());
}
The only thing that is changed in this method is that I am returning my own EntityManger(MyCustomEntityManager). Then, I overlaid the getRepository method as follows:
public function getRepository($entityName)
{
$entityName = ltrim($entityName, '\\');
if (isset($this->repositories[$entityName])) {
return $this->repositories[$entityName];
}
$metadata = $this->getClassMetadata($entityName);
$repositoryClassName = $metadata->customRepositoryClassName;
if ($repositoryClassName === null) {
$repositoryClassName = "Acme\DemoBundle\Doctrine\ORM\MyCustomEntityRepository";
}
$repository = new $repositoryClassName($this, $metadata);
$this->repositories[$entityName] = $repository;
return $repository;
}
Here, I have only modified one line as well. Instead of relying on the DBAL Configuration to retreive the default $repositoryClassName, I have specified my own default repository Acme\DemoBundle\Doctrine\ORM\MyCustomEntityRepository.
Once you have created your own custom EntityRepository, the sky is the limit. You can inject services into the repository(I currently use JMS Di annotations, described below), or perform custom actions against a QueryBuilder in the createQueryBuilder method, like so:
use JMS\DiExtraBundle\Annotation as DI;
class MyCustomEntityRepository extends EntityRepository
{
private $myService;
public function createQueryBuilder($alias)
{
$queryBuilder = parent::createQueryBuilder($alias);
/** INSERT CUSTOM CODE HERE **/
return $queryBuilder;
}
/**
* #DI\InjectParams({
* "myService" = #DI\Inject("my_service_id")
* })
*/
public function setMyService(MyServiceInterface $myService)
{
$this->myService = $myService;
}
}
Once you have created your own EntityRepository, you should have all of your repositories that need this custom functionality extend MyCustomEntityRepository. You could even take it a step further and create your own QueryBuilder to further extend this.
You can write a custom AST Walker and setup your application to use this walker for all queries with defaultQueryHint (Doctrine 2.5 new feature) configuration option:
<?php
/** #var \Doctrine\ORM\EntityManager $em */
$em->getConfiguration()->setDefaultQueryHint(
Query::HINT_CUSTOM_TREE_WALKERS,
['YourWalkerFQClassName']
)