Cloisonner les données par client avec Symfony

Fabien Lemoine
5 min readMay 19, 2021

--

Crédit : Boba Jaglicic via Unsplash

Pour de nombreux projets, il est souhaitable qu’une seule application soit en mesure d’accueillir plusieurs clients, avec chacun leurs données propres. Et il n’y aura à aucun moment d’interaction entre ces différents clients. S’il est naturellement possible de créer n instances de la même application, avec chacune sa propre base de données, nous allons plutôt explorer la possibilité de n’utiliser qu’une seule instance de l’application, avec une unique base de données. Cette approche est notamment pertinente pour lancer un SaaS (Software as a Service) tout en limitant les coûts d’infrastructure.

L’approche peut sembler fort contraignante, notamment en ajoutant une couche de complexité à chaque phase de développement de l’application, mais nous allons voir que certains mécanismes du framework Symfony vont grandement nous faciliter la tâche.

Implémentation avec Symfony

Symfony va grâce à sa modularité encore une fois nous permettre de répondre à ce besoin sans trop d’efforts. Lançons-nous dans le vif du sujet.

Prérequis

L’exemple ci-dessous est conçu avec Symfony 5+ avec autowire et autoconfig activés, le code peut néanmoins être adapté pour fonctionner avec des versions antérieures.

Objectifs

  • Concevoir un modèle de données permettant le cloisonnement par client
  • Filtrer automatiquement les données existantes en fonction du client connecté
  • Rattacher automatiquement les données nouvellement créées au client connecté

Sommaire

  • Création/adaptation du modèle de données
  • Création d’une interface pour identifier les entités Doctrine à filtrer par espace de travail
  • Création d’un filtre SQL avec Doctrine
  • Activation du filtre SQL Doctrine en indiquant sur quel espace de travail filtrer les données
  • Rattachement des entités Doctrine créées à l’espace de travail de l’utilisateur connecté
  • Contrevenir au cloisonnement des données pour gérer des cas particuliers

Création/adaptation du modèle de données

Les données de chaque client devront être cloisonnées, mais à l’inverse, chaque utilisateur d’un même client devra pouvoir interagir avec les mêmes données.

Pour ce faire, je propose d’introduire la notion d’espace de travail. Le filtrage et le rattachement des données se fera donc à partir d’un espace de travail et non d’un utilisateur. Chaque espace de travail pourra contenir plusieurs utilisateurs, et les utilisateurs seront donc, comme toutes les autres entités Doctrine, eux aussi cloisonnés par espace de travail.

Voici le modèle de données proposé :

Modèle de données

Bien entendu, il peut toujours exister d’autres relations entre vos entités, et certaines entités peuvent ne pas être liées à l’espace de travail (par exemple des données de référence communes à tous les clients).

Voici le code minimaliste pour mettre en place l’entité Doctrine Workspace :

Entité Workspace

Création d’une interface pour identifier les entités Doctrine à filtrer par espace de travail

Afin de faciliter le cloisonnement des données, nous allons maintenant mettre en place une interface (au sens programmation orientée objet) pour nous permettre de reconnaître toute entité Doctrine dépendant d’un espace de travail.

Interface Workspaceable

Il faut maintenant implémenter cette interface dans toutes les entités Doctrine le nécessitant. Voici un exemple avec l’entité User.

Implémentation de l’inferface Workspaceable dans l’entité User

La relation à créer depuis l’entité qui implémente WorkspaceableInterface est de type ManyToOne. Chaque {MonEntité} est liée à un Workspace. Chaque Workspace peut être lié à plusieurs {MonEntité}.

Pour plus de rapidité, la commande php bin/console make:entity peut vous assister dans la création des relations avec l’entité Workspace.

Création d’un filtre SQL avec Doctrine

Un filtre SQL Doctrine est un fragment de requête SQL qui va être ajouté automatiquement à toutes les requêtes Doctrine réalisées par votre application, sous certaines conditions.

Dans notre cas, le filtre devra être renvoyé lorsqu’une requête est construite pour une entité Doctrine qui implémente WorkspaceableInterface.

Le filtre SQL pour Doctrine

Il faut ensuite déclarer ce filtre au niveau de la configuration de Doctrine, mais nous ne l’activons pas par défaut. Le filtre sera en effet activé dynamiquement afin de permettre la transmission de l’identifiant de l’espace de travail de l’utilisateur connecté à l’application.

Activation du filtre SQL Doctrine en indiquant sur quel espace de travail filtrer les données

Il est maintenant nécessaire d’activer le filtre à l’exécution de chaque requête HTTP traitée par l’application si un utilisateur est connecté. Nous nous basons sur l’événement Request du Kernel de Symfony, que nous écoutons avec un EventSubscriber.

Activation du filtre via un EventSubscriber

La méthode onKernelRequest ci-dessus offre aussi l’opportunité d’introduire des exceptions pour des cas d’utilisation avancés. Par exemple, il serait possible de ne pas activer le filtre si l’utilisateur connecté détient le rôle “ROLE_SUPER_ADMIN”.

Le filtrage des données par espace de travail est opérationnel à ce stade pour toutes les entités qui implémentent WorkspaceableInterface.

Rattachement des entités Doctrine créées à l’espace de travail de l’utilisateur connecté

Ne nous arrêtons pas en si bon chemin. Nous souhaitons maintenant que lorsqu’un utilisateur créé de nouvelles données, celles-ci soient automatiquement rattachées à son espace de travail. Nous allons là encore nous appuyer sur un EventSubscriber, mais cette fois-ci dépendant de Doctrine et non de Symfony comme au chapitre précédent.

EventSubscriber Doctrine

Les EventSubscriber de Doctrine ne sont pas auto configurés par Symfony, il faut donc ajouter ces quelques lignes au fichier services.yaml :

Les entités implémentant WorkspaceableInterface sont désormais automatiquement rattachées au bon espace de travail à leur création.

Contrevenir au cloisonnement des données pour gérer des cas particuliers

Comme nous l’avons vu, il est possible d’ajouter de la logique métier au sein du FilterConfiguratorSubscriber afin de gérer des cas particuliers mais au niveau d’une requête HTTP. Voyons maintenant comment s’affranchir du filtrage au cas par cas par requête Doctrine.

Supposons que vous souhaitiez qu’il n’y ait aucun utilisateur de l’application qui utilise la même adresse e-mail. Vous allez alors utiliser la contrainte de validation UniqueEntity sur votre entité User. Mais celle-ci sera également concernée par le filtrage des données que nous avons mis en place, et un utilisateur pourra s’inscrire avec la même adresse e-mail sur plusieurs espaces de travail, ce que nous voulons éviter.

Nous ajustons la contrainte de validation UniqueEntity sur la classe User et nous déclarons une méthode de repository Doctrine pour avoir la main sur la requête.

Contrainte UniqueEntity

Nous créons ensuite la méthode dans le UserRepository, en prenant soin de désactiver, puis de réactiver le filtrage.

Désactivation du filtrage pour une requête dans un repository Doctrine

Le mot de la fin

Ces quelques étapes nous ont permis de cloisonner efficacement les données par espace de travail, mais plus encore, de rendre ce fonctionnement quasiment transparent pour les futurs développements.

Les seules actions à garder en tête sont désormais l’implémentation de WorkspaceableInterface sur les nouvelles entités ajoutées au projet, et la gestion des cas particuliers.

J’espère que ce petit tutoriel vous aura été utile. N’hésitez pas à me faire part de vos retours dans les commentaires.

Bibliographie

SaaS : https://fr.wikipedia.org/wiki/Software_as_a_service

EventSubscriber Symfony : https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber

EventSubscriber Doctrine : https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/events.html#listening-and-subscribing-to-lifecycle-events

Filtres SQL Doctrine : https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/filters.html

Tutoriel pour (entre autres choses) créer les relations entre les entités Doctrine avec la commande make:entity : https://www.youtube.com/watch?v=eSwtIyAfsxk

Contrainte de validation UniqueEntity : https://symfony.com/doc/current/reference/constraints/UniqueEntity.html

Merci au youtuber yoandev co dont les vidéos sur Symfony m’ont donné envie d’écrire de nouveaux tutoriels. https://www.youtube.com/c/yoandevco/featured

Crédit photo : Boba Jaglicic via Unsplash

--

--