Symfony: Pouvoir se loguer avec deux entités différentes

Publié le 20 avril 2020
#Symfony#security

Pour un side-project, j'ai eu besoin d'avoir une page de connexion qui permette de se connecter pour deux types d'utilisateurs complètement différents. Les professeurs et les élèves.

Ces utilisateurs ont des points communs :

  • email
  • mot de passe
  • ...

Et des différences :

  • un professeur a des classes.
  • un élève appartient à une seul classe.
  • ...

J'ai donc deux entités : Student & Teacher qui héritent de la classe User.

Comment gérer avec Symfony ces deux entités avec un seule formulaire de connexion ?

Setup

Class User

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity()
 */
abstract class User implements UserInterface
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=180, unique=true)
     */
    private $email;

    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];

    /**
     * @var string The hashed password
     * @ORM\Column(type="string")
     */
    private $password;

   // getters and setters
}

Class Student

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\StudentRepository")
 */
class Student extends User
{
    /**
     * @ORM\Column(type="string", length=255)
     */
    private $studentField;

    // getters and setters
}

Class Teacher

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\TeacherRepository")
 */
class Teacher extends User
{
    /**
     * @ORM\Column(type="string", length=255)
     */
    private $teacherField;

    // getters and setters
}

Inheritance Mapping

Afin de modéliser nos Student et nos Teacher avec Doctrine nous allons utiliser l'inheritance Mapping. Cela permet pour les classes filles (ici Student et Teacher) de persister les propriétés de la classe mère (ici User).

Plusieurs stratégies existent. Nous allons utiliser la stratégie : Class Table Inheritance.

Au niveau de la base de données, cela se traduira par 3 tables : user, student et teacher, chacune possédant les informations de leurs classes respective. Les tables student et teacher seront liées à la table user via une clé étrangère (en général, cela sera la clé primaire id qui sert de clé étrangère.).

Implémentation

Implémentons cette stratégie. La class User doit être abstraite et avoir les annotations suivantes :

/**
 * @ORM\Entity()
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="type", type="string")
 * @ORM\DiscriminatorMap({"student"="Student", "teacher"="Teacher"})
 */
abstract class User implements UserInterface
{
    //...
}

Nous indiquons :

  • le type d'héritage : JOINED, cela indique à Doctrine la stratégie à utiliser.
  • la colonne qui identifie le type d'utilisateur : type
  • les valeurs possibles du type d'utilisateur : Student ou Teacher. Comme on est dans le même namespace, nous n'avons pas besoin d'indiquer le chemin complet de la classe (App\Entity\Student, App\Entity\Teacher)

Un petit coup de :

php bin/console doctrine:schema:update --force

Et voici nos entités modélisées dans la base de données.

Relations entre la table student et user
Relations entre la table student et user

Connexion

Voici la partie sur laquelle je suis resté bloqué le plus longtemps. Écrire la configuration nécessaire pour indiquer à Symfony que nous avons deux types d'utilisateur ( Student et Teacher) , mais qu'ils utilisent le même type d'authentification.

C'est dans le config/packages/security.yaml que cela se passe :

security:
    encoders:
        App\Entity\User:
            algorithm: auto

    providers:
        chain_provider:
            chain:
                providers: [app_student_provider, app_teacher_provider]
        app_student_provider:
            entity:
                class: App\Entity\Student
                property: email
        app_teacher_provider:
            entity:
                class: App\Entity\Teacher
                property: email

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: lazy
            provider: chain_provider

    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: IS_AUTHENTICATED_FULLY }

Le secret est de définir deux providers différents : app_student_provider et app_teacher_provider puis en décrire un troisième provider : chain_provider qui chaîne les deux précédent. Le but de ce dernier est, lors d'une connexion, de tester les deux providers.

Pour ce qui est de la définition de l'algorithme d'encodage, utiliser la class User suffi.

Avec ce chain_provider, nous pouvons gérer deux types d'utilisateur, et ainsi avoir un seul et même formulaire de connexion pour les deux types d'utilisateurs.

Exemple

Il ne manque plus qu'à gérer l'authentification. Pour cela, je vous conseille la commande :

php bin/console make:auth

Voici un repo github qui fournit un exemple concret et fonctionnel : https://github.com/gkueny/symfony-two-user-entities

En savoir plus

Voici les liens qui m'ont été utiles lors de mes recherches :

  • https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/inheritance-mapping.html#inheritance-mapping
  • https://symfony.com/doc/4.0/security/multiple_user_providers.html
gkueny

À propos de l'auteur - gkueny @ZEBet

Développeur depuis maintenant 6 ans, j'ai une grande affinité avec le mobile et les tests bien fait. Pas full-stack mais touche à tout, je suis également à l'aise sur du Symfony / php.