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.
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;
}
I use Symfony 4 with messenger and I use a worker who consumes my messages as a long-running process.
I have a bug with doctrine if I delete my post and recreate a new one and I dispatch my message. $post have the old data and not the new one.
I have tried a lot of things and nothing work, it works when I restart my worker.
class ChannelMessageHandler implements MessageHandlerInterface
{
private $channel;
private $bus;
public function __construct(ChannelService $channel, MessageBusInterface $commandBus)
{
$this->channel = $channel;
$this->bus = $commandBus;
}
public function __invoke(ChannelMessage $channelMessage)
{
$error = $this->channel->handleChannel($channelMessage->getUser(), $channelMessage->getService());
if ($error) {
throw new Exception($error[0]);
}
$this->bus->dispatch(new FeedMessage($channelMessage->getUser(), $channelMessage->getService()));
}
}
}
My MessageHandler call a service :
class ChannelService implements ContainerAwareInterface
{
use ContainerTrait;
protected $em;
protected $logger;
public function __construct(EntityManagerInterface $entityManager, LoggerInterface $logger)
{
$this->em = $entityManager;
$this->logger = $logger;
}
public function handleChannel($userId, $serviceName)
{
$user = $this->em->getRepository('App:User\Authentication\User')->findOneById($userId);
$post = $user->getPost();
return $this->getUserAnalyticBy($post, $serviceName);
}
thanks a lot
I'm trying to log some informations with the logger service within an ExceptionListener class but I don't understand how to access / create a logger object...
Here is my piece of code :
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
...
$exception = $event->getException();
if ($exception instanceof HttpExceptionInterface) {
// HTTP Exception (400, 401, 404, ...)
$response = new JsonResponse(...)
}
...
$event->setResponse($response);
}
}
The listener works perfectly but in this case nothing is logged by default into the dev.log file (of course logging is enabled and functional).
I tried to had an LoggerInterface parameter to the onKernelException function (autowiring ?) but without success.
Should I had some additional configuration in the service.yaml file ?
App\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
Autowiring being by default on SF4, you should be able to inject the LoggerInterface into your listener without further configuration, like so (then call $this->logger when desired) :
use Psr\Log\LoggerInterface;
class ExceptionListener
{
protected $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
...
$exception = $event->getException();
if ($exception instanceof HttpExceptionInterface) {
// HTTP Exception (400, 401, 404, ...)
$response = new JsonResponse(...)
}
...
$event->setResponse($response);
}
}
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.
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();