when I register a new Plasmid Entity, I want give him an automatic name (like: p0001, p0002, p0003), to do this, I need to select in the database the last Plasmid entity for a specific User, get its autoName, and use this previous name to define the new one.
But, when I inject the token_storage in my listener, the token is null... In the controller, I can have the user, it's work.
The service.yml
app.event_listener.plasmid:
class: AppBundle\EventListener\PlasmidListener
arguments: ["#security.token_storage"]
tags:
- { name: doctrine.event_listener, event: prePersist }
And, the PlasmidListener
class PlasmidListener
{
private $user;
public function __construct(TokenStorage $tokenStorage)
{
$this->user = $tokenStorage->getToken()->getUser();
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// If the entity is not a Plasmid, return
if (!$entity instanceof Plasmid) {
return;
}
// Else, we have a Plasmid, get the entity manager
$em = $args->getEntityManager();
// Get the last plasmid Name
$lastPlasmid = $em->getRepository('AppBundle:Plasmid')->findLastPlasmid($this->user);
// Do something with the last plasmid in the database
}
}
If someone know why I can get the actual user in the Doctrine Listener ?
Thanks
I think that you should store pointer to tokenStorage class in your service instead of user object:
class PlasmidListener
{
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function prePersist(LifecycleEventArgs $args)
{
$user = $this->tokenStorage->getToken()->getUser();
//...
}
}
To avoid error in Symfony4 and above, use TokenStorageInterface instead of TokenStorage
For example
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
And in your constructor :
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
To get the user and its details in prePersist :
$user = $this->tokenStorage->getToken()->getUser();
Related
When the user logs in to the system, I need to fill a class variable (Login-> testInfo) with information, but in the controller the variable always returns null.
Here is a generic example.
The Login class
class Login extends UserInterface
{
private $testInfo = null;
public function setTestInfo(string $testInfo)
{
$this->testInfo = $testInfo;
}
public function getTestInfo() : ?string
{
return $this->testInfo;
}
}
The Authenticator:
class FormAuthenticator extends AbstractFormLoginAuthenticator
{
...
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = $this->entityManager->getRepository(Login::class)->findByUsername(credentials['username']);
if (!$user)
{
throw new CustomUserMessageAuthenticationException('Username could not be found.');
}
//this prints NULL
dd($user->getTestInfo());
$user->setTestInfo('testing the string');
//this prints 'testing the string'
dd($user->getTestInfo());
return $user;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
//this prints 'testing the string'
dd($token->getUser()->getTestInfo());
}
...
}
The Controller Class:
class MyController extends AbstractController
{
private $login = null;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->login = $tokenStorage->getToken() ? $tokenStorage->getToken()->getUser() : null;
}
public function home()
{
//this prints null
dd($this->login->getTestInfo());
}
}
If $user goes to the tokenStorage with the new value ('testing the string'), why, when I try to use it on the controller, does the variable always return null? what am I doing wrong?
Is testInfo a transient variable? Because you gotta know that there is UserProvider that tries to refresh user from token (maybe it could be "changed" somehow between requests). I'm pretty sure you're losing those infos right in this process.
Are you sure your controller constructor isn't being executed too soon, prior to the authentication success event writing the token to the token storage service? I'd dd() the token in the constructor to verify if the token and Login instance are present at that point.
You may need to use setContainer() instead of __construct() in your controller to retrieve the authenticated token, which would look something like this:
private $tokenStorage = null;
private $login = null;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param ContainerInterface $container Symfony service container interface
* #return ContainerInterface|null
*/
public function setContainer(\Psr\Container\ContainerInterface $container): ?\Psr\Container\ContainerInterface
{
if ($this->tokenStorage instanceof TokenStorageInterface && $this->tokenStorage->getToken() instanceof TokenInterface && $this->tokenStorage->getToken()->getUser() instanceof Login) {
$this->login = $this->tokenStorage->getToken()->getUser();
}
return $container;
}
Changes to my Entities are logged using an EventSubscriber for Doctrine lifecycle events. I want to log a request id alognside the entity change log entries to see what's happened in one single user action.
Adding the request id is as easy as this:
class RequestIdSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [KernelEvents::REQUEST => 'addRequestId'];
}
public function addRequestId(GetResponseEvent $event): void
{
$request = $event->getRequest();
$request->attributes->set('RequestId', Uuid::uuid1()->toString());
}
}
The problem is that the request does not seem to be available in the Doctrine EventSubscribers in a reliable way:
class EntityEventSubscriber implements EventSubscriber
{
public function __construct(DelayedEventDispatcher $dispatcher, RequestStack $requestStack, LoggerInterface $logger)
{
$this->dispatcher = $dispatcher;
$this->inventory = new EntityInventory();
$this->requestStack = $requestStack;
$this->logger = $logger;
}
public function getSubscribedEvents(): array
{
return [
Events::postUpdate,
];
}
public function postUpdate(LifecycleEventArgs $args): void
{
// works
$this->logger->debug($this->requestStack->getCurrentRequest()->get('RequestId'));
$entity = $args->getEntity();
$changes = $this->inventory->getChangeSet($entity);
$event = new EntityUpdatedEvent($entity, $changes);
$this->triggerAuditLogEvent($event);
}
public function triggerAuditLogEvent(EntityEvent $event): void
{
// request.CRITICAL: Uncaught PHP Exception Error: "Call to a member function get() on null"
$event->setRequestId($this->requestStack->getCurrentRequest()->get('RequestId'));
$this->dispatcher->dispatch(FhrEvents::GENERIC_ENTITY_EVENT, $event);
}
}
So what really bothers me is that the request seems to be available in one method and if I call the next one, it's already gone.
I have a website made with Symfony 3.4 and within my actions I must check if the current user can edit the target product, something like this:
/**
* #Route("/products/{id}/edit")
*/
public function editAction(Request $request, Product $product)
{
// security
$user = $this->getUser();
if ($user != $product->getUser()) {
throw $this->createAccessDeniedException();
}
// ...
}
How can I avoid making the same check on every action (bonus points if using annotations and expressions)?
I am already using security.yml with access_control to deny access based on roles.
You can use Voters for this exact purpose. No magic involved. After creating and registering the Voter authentication will be done automatically in the security layer.
You just have to create the Voter class and then register it as a service. But if you're using the default services.yaml configuration, registering it as a service is done automatically for you!
Here is an example you can use. You may have to change a few items but this is basically it.
To read more visit: https://symfony.com/doc/current/security/voters.html
<?php
namespace AppBundle\Security;
use AppBundle\Entity\Product;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;
class ProductVoter extends Voter
{
const EDIT = 'EDIT_USER_PRODUCT';
protected function supports($attribute, $subject)
{
if($attribute !== self::EDIT) {
return false;
}
if(!$subject instanceof Product) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** #var Product $product */
$product= $subject;
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
return $this->belongsToUser($product, $user);
}
private function belongsToUser(Product $product, User $user)
{
return $user->getId() === $product->getUser()->getId();
}
}
You could try with a listener:
Check the action name,for example, if it is "edit_product", them continue.
Get the current logged User.
Get the user of the product entity.
Check if current user is different to Product user, if it is true, throw CreateAccessDeniedException.
services.yml
app.user.listener:
class: AppBundle\EventListener\ValidateUserListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
arguments: ["#service_container", "#doctrine.orm.entity_manager"]
Edit Action:
Added name "edit_product" to the action.
/**
*
* #Route("/products/{id}/edit",name="edit_product")
*/
public function editAction()
{
...
src\AppBundle\EventListener\ValidateUserListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ValidateUserListener
{
private $container;
private $entityManager;
public function __construct($container, $entityManager)
{
$this->container = $container;
$this->entityManager = $entityManager;
}
public function onKernelRequest(GetResponseEvent $event)
{
$currentRoute = $event->getRequest()->attributes->get('_route');
if($currentRoute=='edit_product' || $currentRoute=='edit_item' )
{
$array_user = $this->getCurrentUser();
if($array_user['is_auth'])
{
$current_user = $array_user['current_user'];
$product = $this->entityManager->getRepository('AppBundle:User')->findOneByUsername($current_user);
$product_user = $product->getUsername();
if ($current_user !==$product_user)
{
throw $this->createAccessDeniedException();
}
}
}
}
private function getCurrentUser()
{
//Get the current logged User
$user = $this->container->get('security.token_storage')->getToken()->getUser();
if(null!=$user)
{
//If user is authenticated
$isauth = $this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY');
return array('is_auth'=>$isauth, 'current_user'=>$user);
}
return array('is_auth'=>false, 'current_user'=>$user);
}
}
Tested in Symfony 3.3
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 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;
}
}