symfonysymfony-http-foundation

When does Symfony intialize the session, and can you force it to be initialized sooner?


I'm using Symfony 7.2 and EasyAdmin 4. I'm trying to execute code in a trait that recovers the currently logged User and the current Session, and uses them to initialize an attribute of the class it is used in. I need this initialization to be executed when the object is instantiated or immediately thereafter, before any of its methods is called. To achieve this, I'm using an init() method with the "Required" attribute, which in theory gets executed immediately after the constructor.

trait ConfigTrait
{
    protected ?Azienda $azienda = null;

    #[Required]
    public function init(Security $security, ManagerRegistry $doctrine, RequestStack $requestStack)
    {
        /** @var User */
        $user = $security->getUser();

        /** @var EntityManager */
        $em = $doctrine->getManager();

        if ($user) {
            if ($user->getAzienda()) {
                $this->azienda = $user->getAzienda();
            } else {
                try {
                    $sessionAziendaId = $requestStack->getSession()->get('aziendaId');
                    $this->azienda = $sessionAziendaId ? $em->getReference(Azienda::class, $sessionAziendaId) : null;
                } catch (SessionNotFoundException $ex) {
                }
            }
        }
    }

    //[... more methods ...]
}

When this trait is used in a Controller, it looks like the init() method is called when the session has not been loaded yet (no session and Security->getUser() returns null, but the PHPSESSID cookie is there).

When exactly does Symfony initialize the session (i.e. calls the open() and read() methods of its session save handler)? RequestStack is initialized before the controllers, but not the session... is there anything I can do to make sure it is initialized before the trait's init() method is called in a Controller?

I tried to add the code above in a kernel.controller event subscriber, thinking that the session and the authenticated user would be available at that point, but it still does not initialize the "azienda" property sometimes. I noticed that, at least in DEV environment, if I modify the controller source file (like by adding a space somewhere), save it and refresh the page, the "azienda" property doesn't get initialized. On the next refresh it does, and on subsequent ones too. Maybe there's something about symfony's cache at play here?


Solution

  • What you are trying to do isn't exactly the right way, but I think I understand what you are trying to do.

    The #[Required] attribute only works on compile time. This only happens once and is store in cache.

    So what you can do it the following.

    trait ConfigTrait
    {
        private Security $security;
        private ManagerRegistry $doctrine;
        private RequestStack $requestStack;
    
        #[Required]
        public function init(Security $security, ManagerRegistry $doctrine, RequestStack $requestStack)
        {
            $this->security = $security;
            $this->doctrine = $doctrine;
            $this->requestStack = $requestStack;
        }
    
        public function getAzienda(): ?Azienda
        {
            /** @var User */
            $user = $security->getUser();
    
            /** @var EntityManager */
            $em = $doctrine->getManager();
    
            if (!$user) {
                return null;
            }
            if ($user->getAzienda()) {
                return $user->getAzienda();
            } 
            try {
                $sessionAziendaId = $requestStack->getSession()->get('aziendaId');
                return $sessionAziendaId ? $em->getReference(Azienda::class, $sessionAziendaId) : null;
            } catch (SessionNotFoundException $ex) {
                return null;
            }
        }
    
    }
    

    In your controllers you can then call $this->getAzienda() that will either return the Azienda instance, or null.

    It's a bad practice to store user based information in private properties of a controller. Or any singleton/service for that matter.

    If for any reason you change your application to a daemon (ReactLoop), that singleton is shared across all your users.

    You could use a Cache that's only used in the current user's session. You can also store the Azienda object in the session instead of the id, so it doesn't require multiple database calls. But that's up to you.