Comment gérer les entités Doctrine multi site ?

Par @jbnahan69
https://www.pxfuel.com/en/free-photo-qzhvh

Nous continuons notre projet permettant la gestion de plusieurs sites (multi tenant). Nous avons vu les solutions disponibles, et j'ai choisi de faire le projet avec un code installé séparément pour chaque client. Dans le précédent article, nous avons vu comment mettre en place le template et la configuration par client. Penchons-nous maintenant sur la gestion de la base de données.

Les entités Doctrine

La surcharge d'une entité doctrine est relativement simple et bien documentée. Les entités à surcharger (MappedSuperclass) sont stockées dans le dossier src/Platform/Entity.

Pour chaque entité à surcharger, il est nécessaire de définir également une interface. Nous en aurons besoin plus tard pour les relations.

Pour les articles, nous aurons ce schéma de base de données:

Entité et relation entre elle.

Dans ce contexte, chaque entité Doctrine MappedSuperclass devra avoir une surcharge pour chaque client.

Comment gérer les relations vers une entité inexistante

Il est très rare d'avoir un projet disposant d'entité Doctrine sans relation entre elles. Mais comment ça se passe avec les relations entre les classes MappedSuperclass ?

Comme le dit la documentation, il n'est pas possible d'avoir des relations multi-directionnel avec une MappedSuperclass. C'est l'entité qui surchargera cette MappedSuperclass qui devra les porter.

Pour résoudre le problème, nous allons utiliser les interfaces et un mécanisme de résolution des entités de Doctrine.

Pour chaque relation, nous allons utiliser l'interface comme targetEntity.

Comment gérer les relations en fonction du client courant

Grâce à la gestion des configurations par client mise en place lors de l'étape précédente, il est possible d'ajouter les correspondances entre les interfaces et les classe directement dans la configuration du bundle Doctrine.

J'ai donc modifier le fichier "packages/doctrine.yaml" pour chaque client et ajouté cette configuration selon le client :

doctrine:
    orm:
        resolve_target_entities:
            App\Platform\Entity\TodoInterface: App\ClientB\Entity\Todo
            App\Platform\Entity\PlatformUserInterface: App\ClientB\Entity\User

Bien entendu, il n'est pas nécessaire d'utiliser les interfaces pour une relation entre une entité spécifique à un client et une autre entité.

Comment gérer les migrations par client et communément

Grâce aux étapes précédentes, vous avez la possibilité de définir des entités spécifiques pour chaque client. Sachant que tout ce qui est commun sera placé dans le dossier Plateform pour éviter la duplication de code.

Maintenant que chaque client peut avoir sa personnalisation, comment gérer les migrations de base de données ?

Il y a deux façons de faire:

  • Écrire que des migrations spécifiques pour chaque client
  • Écrire des migrations communes et des migrations spécifiques pour chaque client.

La seconde option est un peut plus complexe à mettre en place. Et c'est celle que j'ai choisie (pourquoi faire simple).

Depuis DoctrineMigrationBundle 3.0 il est possible d'avoir plusieurs dossiers contenant les migrations avec un namespace spécifique par dossier.

Nous allons donc mettre un dossier de migration commun et un dossier par client le tout dans le dossier migrations.

migrations
├── ClientA
├── ClientB
└── Platform

Et dans la configuration de doctrine config/packages/doctrine_migration.yaml :

doctrine_migrations:
    migrations_paths:
        'AllPlatformDoctrineMigrations': '%kernel.project_dir%/migrations/Platform'

Cette configuration sera complété par la fichier présent dans le dossier de chaque client contenant la configuration spécifique au client (config/ClientB/packages/doctrine_migrations.yaml pour le ClientB):

doctrine_migrations:
    migrations_paths:
        'ClientBDoctrineMigrations': '%kernel.project_dir%/migrations/ClientB'

Maintenant, il est presque possible de générer les migrations avec la commande symfony console doctrine:migrations:diff --namespace=....

Oui, presque car si vous vous souvenez, dans le dossier src/Plateform/Entity il n'y a pas d'entité que des MappedSuperclass.

Il nous faut une configuration client n'ayant aucune personnalisation pour permettre la génération des migrations commune à tous les clients.

Pour cela nous allons ajouter le dossier src/EmptyClient/Entity qui contient toutes les entités qui surcharge-les MappedSuperclass et implémente les interfaces. Ce dossier aura également une utilité pour les tests.

Nous pouvons maintenant exécuter la commande CLIENT_ID=EmptyClient symfony console doctrine:migrations:diff --namespace=AllPlatformDoctrineMigrations pour générer les migrations communes à tous les clients.

Et pour le client A, la commande est CLIENT_ID=ClientA symfony console doctrine:migrations:diff --namespace=ClientADoctrineMigrations

Il y a un piège à éviter cependant. Il ne faut pas que les modifications communes soient incluses dans les migrations d'un client. Cela voudrait dire que la modification serait dans une migration par client.

Pour éviter cela, il convient d'exécuter la commande pour générer et appliquer une migration commune avant la migration pour un client.

Conclusion

Nous avons donc vu comment gérer proprement dans le code les spécificités de plusieurs clients sans ajouter un grand nombre de IF dans le code.

Mais surtout comment personnaliser les entités Doctrine pour chaque client en intégrant leurs spécificités.

Restez a l'écoute ! La suite du projet sera sur l'enregistrement et la connexions des utilisateurs.

Merci d'avoir lu jusqu'ici, c'est un sujet difficile à écrire donc si vous avez des questions, je suis disponible en commentaire ou sur les réseaux sociaux pour en discuter.

Author avatar
Jean-Baptiste Nahan

Consultant Expert Web, j'aide les entreprises ayant des difficultés avec leur projet Web (PHP, Symfony, Sylius).

@jbnahan69 | Macintoshplus | Linkedin | JB Dev Labs