I'm working with the Workflow component in symfony 4.3. For my entity I have a marking property and a transition_contexts property. The transition_contexts is of JSON type like
[{"time": ..., "context":[all info I want], "new_marking" : "some_place", {etc...}]
for every transition made.
I want to set a transition_context also for the initial place called creation. For now, when I create a new object my transition_contexts looks like :
[{"time": ..., "context":[], "new_marking" : "creation"}]
For all other transitions I use the apply() function where I can set the context array, but for the initialisation with the initial place, I don't know how to do it.
I've read about listeners, so I tried
class FicheSyntheseTransitionListener implements EventSubscriberInterface
{
private $security;
private $connection;
public function __construct(Security $security, ManagerRegistry $em)
{
$this->security = $security;
//ManagerRegistry au lieu de EntityManagerInterface car on a plusieurs entityManager
$this->connection = $em->getManager('fichesynthese');
}
public function addRedacteur(Event $event)
{
$context = $event->getContext();
$user = $this->tokenStorage->getToken()->getUser();
if ($user instanceof UserInterface) {
$context['user'] = $user->getUsername();
}
$event->setContext($context);
$this->get('doctrine')->getManager('fichesynthese')->flush();
}
public static function getSubscribedEvents()
{
return [
'workflow.fiche.entered.creation' => ['addRedacteur'],
];
}
}
But it seems that it is not taken into account. Is there a way to achieve this ? A manual call to the function that set the initial marking ?
Thanks
Related
I have one question I don't seem to find an answer to.
I have my User entity with a "Status" field.
What I want to do is store in another table "StatusEvent" a new line each time the status of a user is changed to keep track of the history of statuses of my users.
I tried to work with the PreUpdate method but it doesn't allow the creation of new Entities in this step.
I was maybe thinking that it might be possible with other events (onFlush maybe?) but these do not have the methods of the LifecycleEventArgs from PreUpdate (which allows to know if a field has been changed).
Anyone has already came across a same pattern or has an idea on how I could implement it?
Thanks by advance,
This is a nice case to use a custom event and listener.
Create a class UserEvents to hold a constant with the event name like
class UserEvents
{
const STATUS_CHANGED = 'user.status.changed';
}
Create a UserStatusChangedEvent that extends Event and takes the user as a parameter.
class UserChangedEvent extends Event
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function getUser(): User
{
return $this->user;
}
}
And then create and register a listener to capture/handle that event and create the entry that you need using the data from the user object that was passed in the event when it was dispatched.
class UserListener
{
public function onStatusChanged(UserChangedEvent $event)
{
$user = $event->getUser();
//TODO: Create your new status change entry. If you need the entity manager, just inject it in the constructor, like with any other service
}
}
You then need to register you listener as a service and tag it
AppBundle\Event\Listener\UserListener:
tags:
- { name: kernel.event_listener, event: user.status.changed, method: onStatusChanged }
And now all you have to do is dispatch a new instance of the event every time the status changes, passing it the user that you just persisted.
$eventDispatcher->dispatch(
UserEvents::STATUS_CHANGED,
$user
);
Edit: To defend the manual dispatching of the custom event VS the automated dispatch of onFlush, the custom event code is far easier to read even from a newbie that has no knowledge of how/when doctrine lifecycle events are triggered or how the entity manager works internally. The cherry at the top is that the dispatching works as a nice reminder that you have a listener there, which will be useful when you revisit your code in a few months.
The solution by #Dimitris would work, but requires you to dispatch the event manually.
I would use the onFlush method like you mentioned. (If you are writing a library, you are better off with the custom event)
You can use UnitOfWork to get the change sets.
public function onFlush(OnFlushEventArgs $event)
{
$em = $event->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
$this->newEntities[] = $entity;
if ($entity instanceof User) {
$changeSet = $uow->getEntityChangeSet($entity);
// if the $changeSet contains the status, log the change
$log = new Log();
$em->persist($log);
$uow->computeChangeSet($em->getClassMetadata(Log::class), $log);
}
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
// same here, create a private method to avoid duplication
}
}
The trade off of this listener is it will only log things on flush.
If your entity changes state multiple times before flush, only the last state will be logged. Eg state1 -> 2 -> 3 will only be logged as state1 -> 3
If you plan on creating a complex status field with many states and transitions have a look at the workflow component and use the listeners from there. It is a bit more work, but well worth it.
So what I did following the advice of both Dimitris and Padam67.
Define a DoctrineListener that listens on the onFlush event and register it
Dispatch a custom event in the DoctrineListener
Define an EventSubscriber listening on my custom event
Define a handler to manage the logic
Call the handler from the EventSubscriber
I know it makes a lot of files, but I like to separate everything as much as possible for a cleaner and simpler code to read :)
Define a DoctrineListener that listens on the onFlush event:
config/services.yaml
App\EventListener\Doctrine\DoctrineListener:
tags:
- { name: doctrine.event_listener, event: onFlush }
App\EventListener\Doctrine\DoctrineListener.php
<?php
namespace App\EventListener\Doctrine;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\UnitOfWork;
use App\Event\TalentStatusChangedEvent;
use App\Entity\Talent;
use App\Event\Constants\TalentEvents;
class DoctrineListener
{
private $logger;
private $dispatcher;
public function __construct(
LoggerInterface $logger,
EventDispatcherInterface $dispatcher
) {
$this->logger = $logger;
$this->dispatcher = $dispatcher;
}
public function onFlush(OnFlushEventArgs $event)
{
$entityManager = $event->getEntityManager();
$uow = $entityManager->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof Talent) {
$this->createTalentStatusChangedEvent($entity, $uow);
}
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof Talent) {
$this->createTalentStatusChangedEvent($entity, $uow);
}
}
}
private function createTalentStatusChangedEvent(Talent $entity, UnitOfWork $uow)
{
$this->logger->info(self::class . ' - talentStatusChanged: ' . $entity . ' - start');
$changeSet = $uow->getEntityChangeSet($entity);
if (array_key_exists('status', $changeSet)) {
$talentStatusChangedEvent = new TalentStatusChangedEvent($entity, new \DateTime());
$this->dispatcher->dispatch(TalentEvents::STATUS_CHANGED, $talentStatusChangedEvent);
$this->logger->info(self::class . ' - talentStatusChanged: ' . $entity . ' - success');
} else {
$this->logger->info(self::class . ' - talentStatusChanged: ' . $entity . ' - fail');
}
}
}
Define a TalentStatusChangedEvent
App\Event\TalentStatusChangedEvent.php
<?php
namespace App\Event;
use App\Entity\Talent;
use Symfony\Component\EventDispatcher\Event;
class TalentStatusChangedEvent extends Event
{
private $talent;
private $statusChangedDate;
public function __construct(Talent $talent, \DateTime $date)
{
$this->talent = $talent;
$this->statusChangedDate = $date;
}
public function getTalent()
{
return $this->talent;
}
public function getStatus()
{
return $this->talent->getStatus();
}
public function getStatusChangedDate()
{
return $this->statusChangedDate;
}
}
Define an EventSubscriber for my event (defined a separate file containing all my events per type)
App\EventListener\Admin\User\TalentSubscriber.php
<?php
namespace App\EventListener\Admin\User;
use App\Domain\User\StatusChanged\StatusChangedHandler;
use App\Entity\Talent;
use App\Event\Constants\TalentEvents;
use App\Event\TalentStatusChangedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class TalentSubscriber implements EventSubscriberInterface
{
private $statusChangedHandler;
public function __construct(
StatusChangedHandler $statusChangedHandler
) {
$this->statusChangedHandler = $statusChangedHandler;
}
public static function getSubscribedEvents()
{
return array(
TalentEvents::STATUS_CHANGED => 'statusChanged',
);
}
public function statusChanged(TalentStatusChangedEvent $event) {
$this->statusChangedHandler->handle($event);
}
}
Define a handler to actually manage the creation of the linked entity
App\Domain\User\StatusChanged.php
<?php
namespace App\Domain\User\StatusChanged;
use App\Entity\Talent;
use App\Entity\TalentStatusEvent;
use App\Event\TalentStatusChangedEvent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
class StatusChangedHandler
{
private $entityManager;
private $logger;
public function __construct(
EntityManagerInterface $entityManager,
LoggerInterface $logger
) {
$this->entityManager = $entityManager;
$this->logger = $logger;
}
public function handle(TalentStatusChangedEvent $event)
{
$this->logger->info(self::class . ' - Talent ' . $event->getTalent() . ' - start');
$talentStatusEvent = new TalentStatusEvent();
$talentStatusEvent->setTalent($event->getTalent());
$talentStatusEvent->setStatus($event->getStatus());
$this->entityManager->persist($talentStatusEvent);
// Calling ComputeChangeSet and not flush because we are during the onFlush cycle
$this->entityManager->getUnitOfWork()->computeChangeSet(
$this->entityManager->getClassMetadata(TalentStatusEvent::class),
$talentStatusEvent
);
$this->logger->info(self::class . ' - Talent ' . $event->getTalent() . ' - success');
}
}
I decided, that it will be fine if I use data providers but when i try to generate code coverage whole tested class has 0% coverage.. Can someone tell me why?
Test class:
class AuthorDbManagerTest extends AbstractDbManagerTest
{
public function setUp()
{
parent::setUp();
}
/**
* #dataProvider instanceOfProvider
* #param bool $isInstanceOf
*/
public function testInstances(bool $isInstanceOf)
{
$this->assertTrue($isInstanceOf);
}
public function instanceOfProvider()
{
$manager = new AuthorDbManager($this->getEntityManagerMock());
return [
"create()" => [$manager->create() instanceof Author],
"save()" => [$manager->save(new Author()) instanceof AuthorDbManager],
"getRepository" => [$manager->getRepository() instanceof EntityRepository],
];
}
}
Tested class:
class AuthorDbManager implements ManagerInterface
{
protected $entityManager;
protected $repository;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
$this->repository = $entityManager->getRepository(Author::class);
}
public function create(array $data = [])
{
return new Author();
}
public function getRepository(): EntityRepository
{
return $this->repository;
}
public function save($object): ManagerInterface
{
$this->entityManager->persist($object);
$this->entityManager->flush();
return $this;
}
}
Why my code coverage is 0% on AuthorDbManager?
Screen
The data in the DataProvider is collected before the actual tests start - and there is nothing useful being tested within the testInstances() method.
If you passed the classname and expected class into testInstances($methodName, $expectedClass):
public function testInstances(callable $method, $expectedClass)
{
$this->assertInstanceOf($expectedClass, $method());
}
The dataprovider could return a callable, and the expected result:
"create()" => [[$manager,'create'], Author::class],
then you'd at least be running the code with in the actual test. You may also be better to just pass back a string methodname - 'create', and run that with a locally created $manager instance - $manager->$method() in the test.
In general, it's best to test something as specific as you can - not just letting it convert to a true/false condition.
Question: How to get the form_login.check_path by given firewall name?
We subscribe to Symfony\Component\Security\Http\SecurityEvent::INTERACTIVE_LOGIN in order to log successful logins inside an Application that has multiple firewalls.
One firewall uses JWT tokens via Guard authentication which has the negative effect that this event is triggered for every request with a valid token.
We have currently solved this by manually checking whether the current route matches the firewall's check-path and stopping the event-propagation together with an early return otherwise.
As we're adding more firewalls (with different tokens) I'd like to solve this more generally. Therefore I want to check whether the current route matches the current firewalls check-path without hardcoding any route or firewall-name.
There is a class to generate Logout URLs for the current firewall used by Twig logout_path() method which gets the logout route/path from the firewall listeners somehow. (Symfony\Component\Security\Http\Logout\LogoutUrlGenerator)
Before I hop into a long debugging session I thought maybe someone has solved this case before ;)
Any ideas?
Example code:
class UserEventSubscriber implements EventSubscriberInterface
{
/** #var LoggerInterface */
protected $logger;
/** #var FirewallMapInterface|FirewallMap */
protected $firewallMap;
public function __construct(LoggerInterface $logger, FirewallMapInterface $firewallMap)
{
$this->logger = $logger;
$this->firewallMap = $firewallMap;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
$firewallName = $this->firewallMap->getFirewallConfig($request)->getName();
$routeName = $request->get('_route');
if (('firewall_jwt' === $firewallName) && ('firewall_jwt_login_check' !== $routeName)) {
$event->stopPropagation();
return;
}
$this->logger->info(
'A User has logged in interactively.',
array(
'event' => SecurityEvents::INTERACTIVE_LOGIN,
'user' => $event->getAuthenticationToken()->getUser()->getUuid(),
));
The check_path option is only available from authentication factory/listener, so you could pass this configuration manually to the subscriber class while the container is building.
This solution take account that check_path could be a route name or path, that's why HttpUtils service is injected too:
namespace AppBundle\Subscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\SecurityEvents;
class UserEventSubscriber implements EventSubscriberInterface
{
private $logger;
private $httpUtils;
private $firewallMap;
private $checkPathsPerFirewall;
public function __construct(LoggerInterface $logger, HttpUtils $httpUtils, FirewallMapInterface $firewallMap, array $checkPathsPerFirewall)
{
$this->logger = $logger;
$this->httpUtils = $httpUtils;
$this->firewallMap = $firewallMap;
$this->checkPathsPerFirewall = $checkPathsPerFirewall;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
$firewallName = $this->firewallMap->getFirewallConfig($request)->getName();
$checkPath = $this->checkPathsPerFirewall[$firewallName];
if (!$this->httpUtils->checkRequestPath($request, $checkPath)) {
$event->stopPropagation();
return;
}
$this->logger->info('A User has logged in interactively.', array(
'event' => SecurityEvents::INTERACTIVE_LOGIN,
'user' => $event->getAuthenticationToken()->getUser()->getUsername(),
));
}
public static function getSubscribedEvents()
{
return [SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin'];
}
}
After regiter this subscriber as service (AppBundle\Subscriber\UserEventSubscriber) we need implement PrependExtensionInterface in your DI extension to be able to access the security configuration and complete the subscriber definition with the check paths per firewall:
namespace AppBundle\DependencyInjection;
use AppBundle\Subscriber\UserEventSubscriber;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
class AppExtension extends Extension implements PrependExtensionInterface
{
// ...
public function prepend(ContainerBuilder $container)
{
$checkPathsPerFirewall = [];
$securityConfig = $container->getExtensionConfig('security');
foreach ($securityConfig[0]['firewalls'] as $name => $config) {
if (isset($config['security']) && false === $config['security']) {
continue; // skip firewalls without security
}
$checkPathsPerFirewall[$name] = isset($config['form_login']['check_path'])
? $config['form_login']['check_path']
: '/login_check'; // default one in Symfony
}
$subscriber = $container->getDefinition(UserEventSubscriber::class);
$subscriber->setArgument(3, $checkPathsPerFirewall);
}
}
I hope it fits your need.
for PHP8
In __construct :
public function __construct(
private RequestStack $requestStack,
private FirewallMapInterface $firewallMap
)
{
}
use this :
$firewallName = $this->firewallMap->getFirewallConfig($this->requestStack->getCurrentRequest())->getName();
I'm in a symfony 2.8 project. Another developer wrote a Voter to check if a user has the permissions or not for a specific task. I already use this function in a controller without problems but now I'm writing a service to get a dynamic menu and I don't know how to access to the method isGranted:
Error: Attempted to call an undefined method named "isGranted" of
class.
namespace NameSpaceQuestion\Question\Service;
use Doctrine\ORM\EntityManager;
class MenuBuilder
{
private $areasTools;
public $menuItems = array();
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function createMenuCourse($course,$mode,$user)
{
$repository = $this->em->getRepository('eBundle:AreasTools');
$areas = $repository->findAll();
//Retrieve every area from the db
$itemsMenu = array();
foreach ($areas as $area) {
//If the user has permissions the area is included in the menu and proceed to check the tools that belong to the current area
if($this->isGranted( $mode,$area,$user ) ){
$itemsMenu[$area->getName()] = array();
$toolsPerCourse = $this->em->getRepository('eBundle:CourseTool')->findByAreaToolAndCourse($area, $course);
foreach ($toolsPerCourse as $toolCourse) {
//If the user has permissions the tool is included in the menu under the respective Area
if( ($this->isGranted( $mode,$toolCourse,$user ) )){
array_push($itemsMenu[$area->getName()], $toolCourse->getName());
}
}
}
}
return $itemsMenu;
}
}
You'll have to use Dependency Injection to get the AuthorizationChecker in your MenuBuilder class. You can read about it here: http://symfony.com/doc/current/cookbook/security/securing_services.html
Because it looks like you're already injecting the EntityManager, just add the AuthorizationChecker to your code:
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class MenuBuilder
{
protected $authorizationChecker;
public function __construct(EntityManager $em, AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
$this->em = $em;
}
function createMenuCourse()
{
if ( $this->authorizationChecker->isGranted('EDIT',$user) ) {
//build menu
}
}
}
First thing to understand is that the Symfony base controller has a number of helper functions such as isGranted implemented. But of course, if you are not inside of a controller then you don't have access to them. On the other hand, it's instructive to look at the base class and copy out the needed functionality.
The isGranted functionality relies on a authorization checker service which you will need to inject into your menu builder resulting in something like:
class MenuBuilder
{
private $em;
private $authorizationChecker;
public function __construct(
EntityManager $em,
$authorizationChecker // security.authorization_checker
) {
$this->em = $em;
$this->authorizationChecker = $authorizationChecker;
}
protected function isGranted($attributes, $object = null)
{
return $this->authorizationChecker->isGranted($attributes, $object);
}
Darn it. Stephan beat me by one minute. Must learn to type faster.
I use the sonata-admin bundle.
I have the relationship with the user (FOSUserBundle) in the PageEntity.
I want to save the current user which create or change a page.
My guess is get the user object in postUpdate and postPersist methods of the admin class and this object transmit in setUser method.
But how to realize this?
On the google's group I saw
public function setSecurityContext($securityContext) {
$this->securityContext = $securityContext;
}
public function getSecurityContext() {
return $this->securityContext;
}
public function prePersist($article) {
$user = $this->getSecurityContext()->getToken()->getUser();
$appunto->setOperatore($user->getUsername());
}
but this doesn't work
In the admin class you can get the current logged in user like this:
$this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser()
EDIT based on feedback
And you are doing it this? Because this should work.
/**
* {#inheritdoc}
*/
public function prePersist($object)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser();
$object->setUser($user);
}
/**
* {#inheritdoc}
*/
public function preUpdate($object)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser();
$object->setUser($user);
}
Starting with symfony 2.8, you should use security.token_storage instead of security.context to retrieve the user. Use constructor injection to get it in your admin:
public function __construct(
$code,
$class,
$baseControllerName,
TokenStorageInterface $tokenStorage
) {
parent::__construct($code, $class, $baseControllerName);
$this->tokenStorage = $tokenStorage;
}
admin.yml :
arguments:
- ~
- Your\Entity
- ~
- '#security.token_storage'
then use $this->tokenStorage->getToken()->getUser() to get the current user.
I was dealing with this issue on the version 5.3.10 of symfony and 4.2 of sonata. The answer from greg0ire was really helpful, also this info from symfony docs, here is my approach:
In my case I was trying to set a custom query based on a property from User.
// ...
use Symfony\Component\Security\Core\Security;
final class YourClassAdmin extends from AbstractAdmin {
// ...
private $security;
public function __construct($code, $class, $baseControllerName, Security $security)
{
parent::__construct($code, $class, $baseControllerName);
// Avoid calling getUser() in the constructor: auth may not
// be complete yet. Instead, store the entire Security object.
$this->security = $security;
}
// customize the query used to generate the list
protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
{
$query = parent::configureQuery($query);
$rootAlias = current($query->getRootAliases());
// ..
$user = $this->security->getUser();
// ...
return $query;
}
}