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 :
- 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.
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