Custom Repository User Provider in Symfony

Last week we had a basic overview of OAuth 2. Before we can start implementing an OAuth 2 server or any other form of authentication. We need to provide a User to the Symfony Security bundle. This can be done by creating a User entity and then providing it through a User provider.

Actually implementing the Security Bundle and User authentication in Symfony is straightforward when following the documentation. But for any upcoming implementation, I will be using a custom User provider instead of the build in ones. Here I explain why.

What is a Provider?

Before we can continue, we need to talk about providers.

A provider is something that provides you with data. But a provider itself is not a real design pattern. It is actually a variation on the strategy pattern.

The idea behind the strategy pattern is that we should be able to easily switch between different strategies. In the case of the User provider, there are multiple strategies to provide a user to the security bundle.

The strategy pattern, and thus Providers makes sure our code is open for extension and closed for modification. We can easily add and load multiple strategies for getting user data. A user is could be provided through a database or maybe some external service. All these are different strategies to provide data.

Why create a custom User Provider?

The Symfony Doctrine Bridge includes an EntityProvider that implements the UserProviderInterface. For basic use case of using an anemic entity, this is probably sufficient.

But it does not easily allow me to use my custom repository interface. And the flexibility to easily make changes in how I want Users to be loaded.

I want to create a repository that is not Doctrine specific. That is provided through an interface and can have multiple implementations. Each of these implementations should work without requiring us to change the User Provider or to create new ones for each.

So if you want to move away from the Doctrine ORM, you can without needing to rewrite a lot of code. You just have to create new repository implementations, and then be done with it.

You should always strive to have the least amount of coupling as possible to your infrastructure. This means that only your repositories should know of Doctrine. Your entities and providers should be free from these implementation details.

Creating the User entity

Before we can authenticate anyone, we need a User entity. Because we will be using the Security Component,  we need to implement the UserInterface on our User entity. This is the only single rule we have to follow when working with the security bundle.

Except for that, we are free in how we create and use our entity.

class User implements UserInterface
{
    /**
     * @var string
     */
    private $id;

    /**
     * @var string
     */
    private $email;

    /**
     * @var string
     */
    private $name;

    /**
     * @var array
     */
    private $roles;

    /**
     * @var string
     */
    private $password;

    /**
     * @var bool
     */
    private $active;

    private function __construct(UserId $userId, string $email, string $name)
    {
        $this->id = $userId->toString();
        $this->email = $email;
        $this->name = $name;
    }

    public static function create(string $email, string $name): User
    {
        $userId = UserId::fromString(Uuid::uuid4()->toString());
        return new self($userId, $email, $name);
    }

    public function getId(): UserId
    {
        return UserId::fromString($this->id);
    }

    ... getters and setters ...
}

I prefer to use Ids as value objects. But you can also use a simple integer id.

final class UserId
{
    /**
     * @var UuidInterface
     */
    private $uuid;

    public function __construct(UuidInterface $uuid)
    {
        Assertion::uuid($uuid);
        $this->uuid = $uuid;
    }

    public static function fromString(string $userId): UserId
    {
        return new self(Uuid::fromString($userId));
    }

    public function uuid(): UuidInterface
    {
        return $this->uuid;
    }

    public function toString(): string
    {
        return $this->uuid->toString();
    }

    public function equals($other): bool
    {
        return $other instanceof self && $this->uuid->equals($other->uuid);
    }
}

And then doctrine mapping XML so we can persist users in Doctrine.

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
    <entity name="App\Domain\Model\User">
        <id name="id" column="id" type="string"/>
        <field name="name" type="string"/>
        <field name="password" type="string"/>
        <field name="email" type="string"/>
        <field name="roles" type="json_array"/>
        <field name="active" type="boolean"/>
    </entity>
</doctrine-mapping>

I’m using XML mapping instead of Annotations here because this also allows the least amount of coupling. By using annotations in your entities, you have infrastructure details inside your domain. You are again coupling your infrastructure to your domain.

Creating the User Repository

Now that we have created the User entity. We also need a repository. First, create the interface and then a simple implementation in our application layer.

interface UserRepositoryInterface
{
    public function find(UuidInterface $id): ?User;

    public function findOneByEmail(string $username): ?User;

    public function save(User $user): void;

    public function remove(User $user): void;
}
final class UserRepository implements UserRepositoryInterface
{
    private const ENTITY = User::class;

    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var ObjectRepository
     */
    private $objectRepository;


    public function __construct(
       EntityManagerInterface $entityManager
    ) {
        $this->entityManager = $entityManager;
        $this->objectRepository = $this->entityManager->getRepository(self::ENTITY);
    }

    public function find(UuidInterface $id): ?User
    {
        $this->entityManager->find(self::ENTITY, $id->toString());
    }

    public function findOneByEmail(string $username): ?User
    {
        return $this->objectRepository->findOneBy(['email' => $username]);
    }

    public function save(User $user): void
    {
        $this->entityManager->persist($user);
        $this->entityManager->flush();
    }

    public function remove(User $user): void
    {
        $this->entityManager->remove($user);
        $this->entityManager->flush();
    }
}

The repository implementation is a very basic one. You can use the one you prefer.

Creating the User Provider

Now that we have created a User entity. It is time to we create the User provider using our custom Repository. I call this the UserProvider class. We are thus creating a custom repository User provider. This provider will work for any implementation of our repository interface.

namespace App\Application\Provider;

use App\Domain\Model\User;
use App\Domain\Repository\UserRepositoryInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

final class UserProvider implements UserProviderInterface
{
    /**
     * @var UserRepositoryInterface
     */
    private $userRepository;
    
    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }
    
    public function loadUserByUsername($username): UserInterface
    {
        return $this->findUsername($username);
    }
    
    private function findUsername(string $username): User
    {
        $user = $this->userRepository->findOneByEmail($username);
        if ($user !== null) {
            return $user;
        }

        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $username)
        );
    }
    
    public function refreshUser(UserInterface $user): UserInterface
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', \get_class($user))
            );
        }

        $username = $user->getUsername();
        return $this->findUsername($username);
    }
    
    public function supportsClass($class): bool
    {
        return User::class === $class;
    }
}

As you can see I inject the UserRepositoryInterface and then implement all the methods to load or refresh a User. In my use case, the email address is the username. By using a custom User Provider you know what is happening and you can control the actual implementation in detail.

And then in the security.yml file, we need to set this provider to be used.

security:
    providers:
        database:
            id: App\Application\Provider\UserProvider
    encoders:
        App\Domain\Model\User: argon21

Conclusion

Today we went over the basic setup of using a custom User Provider with a repository. This is, in my opinion, the preferred way to implement a User entity with the Symfony Security bundle.

Only your repositories should be aware of your database implementations. Don’t rely on easy to use bridges and provider to make you write less code. In this, you lose readability and clarity. You will be getting to much coupling which will bite you later on. It will cost you a bit more upfront time, but save you a lot more in the future.

From here on you can use this for your OAuth 2 server, or any authentication implementation.