Codeception/module-symfony

User from Security service is not available in Doctrine Listeners

Opened this issue ยท 10 comments

Hello,

When trying to test a part of our code with an Event Listener like:

<?php
namespace App\EventListener\Doctrine;

use Symfony\Component\Security\Core\Security;

class Listener
{
    private Security $security;
    public function __construct(Security $security)
    {
        $this->security = $security;
    }

    public function prePersist(Entity $entity)
    {
        $user = $this->security->getUser();
        $entity->setUser($user);
    }
}

$user is always null

I found this SO question with the exact same problem:
https://stackoverflow.com/questions/57609821/make-security-available-for-doctrine-onflush-within-functional-test

Do you recommend using the workaround given in the SO answers or something else ?

Hey, @BastienLedon
Hmm, in my applications i have a code very similar to yours, but it works perfectly for me.

I don't think what those answers suggest is necessary.

However, there are several things that should be verified, the best way i could help you is in a real application.
Can you create a demo of your problem based on this repo?

You can choose a branch for the version of symfony you are using, and from there we can see what the real problem is.

@TavoNiievez
Of course, I think you can reproduce here, https://github.com/BastienLedon/codeception-symfony-tests

I've added one other user and just added the amLoggedAs inside those test. (And created a fake Listener)

You can see that inside the controller the user of Security is available but not inside the Listener.

Yep, i can confirm that there is unexpected behavior here.

The Security object requires the symfony container as a parameter.
For some reason in the tests the 'real' container: 'service_container' is injected instead of the test container: 'test.service_container'.

In fact, if you manually inject the test container everything works correctly:

    public function prePersist(LifecycleEventArgs $entity)
    {
        $user = $entity->getObject();
        if($user instanceof User) {
            /** @var ContainerInterface $testContainer */
            $testContainer = $this->container->get('test.service_container');
            $security = new Security($testContainer);
            $user->setUser($security->getUser());
        }
    }

I'm still not sure if it's Codeception behavior or Symfony itself.

But at least there should be documentation on this.

Lastly, I added an Entity Listener to the test project to be able to test this easily in the future.

In theory, a workaround would be to inject the correct service into the env-specific services config file: services_test.yaml.

Any news on this?
I have faced the same issue, spent whole day debugging my tests and I give up ๐Ÿ˜ž

Ok.
Problem is in wrong container being used for doctrine subscribers (Maybe for all persistent services? Didn't check)

Let's say there is a service.

<?php

namespace App\Service;

use App\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

class LocatorConsumerService implements ServiceSubscriberInterface
{
    /**
     * @var ContainerInterface
     */
    private $locator;

    public function __construct(ContainerInterface $locator)
    {
        $this->locator = $locator;
    }

    public function getUserFromLocator(): ?User
    {
        $token = $this->locator->get('security.token_storage')->getToken();
        $user = null;
        if($token) {
            $user = $token->getUser();
        }

        return $user;
    }

    public static function getSubscribedServices(): array
    {
        return [
            'security.token_storage' => TokenStorageInterface::class
        ];
    }

}

If service is injected into controller, user is logged in:

    public function my(LocatorConsumerService $service): Response
    {
// ... 
        $user = $service->getUserFromLocator(); // $user is not null
// ... 
    }

If service injected into any doctrine subscriber - $user is null

<?php

namespace App\EventSubscriber;

use App\Service\LocatorConsumerService;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;

class MySubscriber implements EventSubscriber
{
    /**
     * @var LocatorConsumerService
     */
    private $service;

    public function __construct(LocatorConsumerService $service) {
        $this->service = $service;
    }

    public function postLoad(LifecycleEventArgs $lifecycleEventArgs): void
    {
// ... 
        $user = $this->service->getUserFromLocator(); // $user is null here
// ... 
    }

    public function getSubscribedEvents(): array
    {
        return [
            Events::postLoad
        ];
    }
}

However, if I configure LocatorConsumerService manually in services_test.yaml like this:

    App\Service\LocatorConsumerService:
        arguments:
            - '@test.service_container'

$user is not null in both cases.

I can't figure out which service I have to re-declare this way though. And I'm not sure if such workaround can help at all

@request_stack also empty from doctrine subscriber. Frustration.

same problem. no solution?

Hello
My simple solution:

services_test.yaml

services:
    security.helper:
        class: Symfony\Component\Security\Core\Security
        arguments:
            - '@test.service_container'