User as a service in Symfony2

4 minute read

To get the current logged in user in Symfony2 is kind of complicated. You have to do a lot for such simple task. First you have to get the TokenStorage to retrieve the token. The token may or may not exist. If the token exist you can get an user object or a string ‘anon.’. It all looks like this:

public function getUser()
{
    if (null === $token = $this->container->get('security.token_storage')->getToken()) {
        // no authentication information is available
        return;
    }


    if (!is_object($user = $token->getUser())) {
        // e.g. anonymous authentication
        return;
    }


    return $user;
}

The function above is provided by the FramworkBundle base controller. But what if you want to use the current user in a service? You have to inject the security.token_storage and do this yourself. Wouldn’t it be great if we could do something like this in our service declaration:

services:
  my_servce:
    class: Acme\Service\MyService
    arguments: [@current_user]

And then have the constructor of MyService like…

public function __construct(User $user = null) {}

Use factories

We know that we can use a factory to create a service. What if we can use a factory to fetch the current user for us?

Lets create a factory class and register the current_user service.

class CurrentUserFactory
{
    private $tokenStorage;


    public function __construct(TokenStorageInterface $tokenStorage)
    {
        $this->tokenStorage = $tokenStorage;
    }


    /**
     * Get the logged in user or null.
     *
     * @return User
     */
    public function getUser()
    {
        if (null === $token = $this->tokenStorage->getToken()) {
            return;
        }


        if (!is_object($user = $token->getUser())) {
            return;
        }


        return $user;
    }
}
services:
  current_user.factory:
    class: Acme\UserBundle\Factory\CurrentUserFactory
    arguments: [@security.token_storage]
  current_user:
    class:   Acme\UserBundle\Entity\User
    factory: ["@current_user.factory", getUser]

This works and it is excellent. We only encounter a problem when we consider the dependency injection scopes (which we always should). By default we register services in the container scope. That means that the services are created once and lives in memory until after kernel.terminate event. So if we got the following chain of events:

  1. The user submits the login form
  2. We instantiate the current_user service (which is null)
  3. We authenticate the user and updates the TokenStorage with a new token
  4. We try to use the current_user service

The current_user service will still be null in step 4. So let’s change the scope of current_user to prototype. Now the service will be created every time it is used. This is exactly what we want! But when you declare current_user in scope prototype you will get this nasty exception:

You may remember back in Symfony 2.3 when you tried to inject the @request into a service, then you got a similar exception. So how did Symfony solve this it? They introduced the RequestStack in 2.4. This is just a service that you could push and pop Requests on. Yes, like a normal stack.

My recommended solution

To avoid the problems highlighted you should not use the user object as a service. The main reason is because it is not a service… It is a model. But what you do want to do is making it easier for yourself to retrieve a user. I suggest you create a CurrentUserProvider that has a function getUser. Then inject that CurrentUserProvider into your services. The implementation of CurrentUserProvider is very similar to the CurrentUserFactory showed above.

I have made a PR for a new security.current_user_provider service that hopefully will be merged into 2.8. Follow it here: https://github.com/symfony/symfony/pull/14407

Edit 2015-05-05: When discussing this post with friends in the community and on the PR, I’ve been told that it is bad design if you need to inject the user into a service. You should pass the user object as an parameter from your controller. I will try to avoid the CurrentUserProvider when I write new features.

Categories:

Updated:

Leave a Comment