Rétrospective sur le projet multi site

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

Après avoir mis en place la configuration et la gestion des templates, après avoir défini les entités et géré les migrations, et enfin avoir mis en place l'inscription et la connexion au site pour deux clients, faisons une petite rétrospection sur ce qui a bien fonctionné et ce qui a moins bien fonctionné.

Un jour où j'expliquais ce projet à un collègue, il m'a dit que je faisais du polymorphisme. En un sens, c'est le cas, sauf qu'une seule "forme" est utilisée à un instant T. En développement, à mon sens, le polymorphisme est la possibilité d'utiliser des formes différentes à différent endroit dans le code.

La gestion de la configuration

Je trouve que la façon de gérer la configuration est bonne, mais demande trop souvent de devoir vider le cache.

Je préférerais pouvoir ajouter à Symfony les dossiers de configuration des clients pour qu'il recompile le cache automatiquement s'ils ont changé (mettez un commentaire si vous savez faire).

Dans certains cas, il est possible de pallier ce problème avec Make et son fichier de configuration Makefile.

Modification du 25 octobre 2022 :

J'ai trouvé comment demander à Symfony de reconstruire le cache lorsque je change le fichier .env et .env.local. Dans le fichier src/Kernel.php, j'ai ajouter la méthode build pour inscrire les deux fichiers comme fichier ayant servi à construire la configuration.

<?php

namespace App;
//[...]

final class Kernel extends BaseKernel
{
    protected function build(ContainerBuilder $container)
    {
        $container->addResource(new FileResource(__DIR__.'/../.env'));
        $dotEnvLocal = __DIR__.'/../.env.local';
        if (file_exists($dotEnvLocal)) {
            $container->addResource(new FileResource($dotEnvLocal));
        }
    }
}

La gestion des templates

Dans ce projet, j'ai utilisé le bundle "sylius/theme-bundle" pour la gestion des templates et leur surcharge par client.

Je n'ai pas plus développé ce point, car j'en ai déjà parlé et je n'ai aucun doute sur les capacités de Twig et de ce bundle.

Cependant, pour ceux qui souhaitent éviter l'utilisation d'un Bundle tiers (ce que je comprends très bien), la solution consiste à ajouter un fichier de configuration twig.yaml pour chaque client en ajoutant le chemin vers le dossier spécifique au client.

Exemple de configuration pour le ClientB (fichier: config/ClientB/packages/twig.yaml)

twig:
  paths:
    '%kernel.project_dir%/themes/ClientB/templates': ~

Symfony cherchera les templates dans les dossiers définis dans twig.paths puis dans le dossier par défaut défini dans le fichier config/packages/twig.yaml.

Si vous utilisez le Bundle ou avez ajouté un dossier dans la configuration de Twig, je vous mets en garde sur les performances. Plus il y a de dossiers dans lesquels chercher un template, plus cela prend du temps. J'ai remarqué ce petit problème lors d'optimisation d'une application Sylius.

Pour mémoire, il est recommandé que le serveur envoie les premières données en moins de 200 ms pour un blog (500 ms pour un site e-commerce).

La gestion des migrations

Lors de la mise en place des migrations, j'ai fait le choix d'avoir des migrations pour tout ce qui touche la plateforme commune aux clients, puis des migrations par client.

L'usage de cette solution s'est révélé peu confortable. Il est nécessaire de générer la migration pour la partie commune, la vérifier puis l'exécuter avant de pouvoir réaliser la même manipulation pour chaque client.

Pour m'aider dans cette tâche, j'ai ajouté un fichier Makefile à la racine du projet pour ajouter les commandes à exécuter.

L’intérêt est également de vider le cache et définir le CLIENT_ID selon le client traiter.

SYMFONY_CMD=symfony
CONSOLE_CMD=${SYMFONY_CMD} console

.PHONY: generate-migration-all g-m-all
g-m-all: start generate-migration-platform generate-migration-clienta generate-migration-clientb
generate-migration-all: start generate-migration-platform generate-migration-clienta generate-migration-clientb

.PHONY: generate-migration-platform g-m-p
g-m-p:
generate-migration-platform:
	rm -rf var/cache/*
	CLIENT_ID=ClientEmpty ${CONSOLE_CMD} doctrine:migration:diff --namespace=AllPlatformDoctrineMigrations

.PHONY: generate-migration-clienta g-m-a
g-m-a:
generate-migration-clienta:
	rm -rf var/cache/*
	CLIENT_ID=ClientA ${CONSOLE_CMD} doctrine:migration:diff --namespace=ClientADoctrineMigrations

.PHONY: generate-migration-clientb g-m-b
g-m-p:
generate-migration-clientb:
	rm -rf var/cache/*
	CLIENT_ID=ClientB ${CONSOLE_CMD} doctrine:migration:diff --namespace=ClientBDoctrineMigrations

J'ai donc ajouté des cibles Make pour générer les migrations Doctrine. Avant cela, le cache est vidé et la valeur de la variable CLIENT_ID est définie.

En une commande make (ex make generate-migration-platform) le cache est vidé et la migration est générée.

J'ai également ajouté les commandes permettant l'exécution des migrations en base de données :

.PHONY: migration-all m-all
m-all: start migration-platform migration-clienta migration-clientb
migration-all: start migration-platform migration-clienta migration-clientb

.PHONY: migration-platform m-p
m-p:
migration-platform:
	rm -rf var/cache/*
	CLIENT_ID=ClientEmpty ${CONSOLE_CMD} doctrine:migration:migrate

.PHONY: migration-clienta m-a
m-a:
migration-clienta:
	rm -rf var/cache/*
	CLIENT_ID=ClientA ${CONSOLE_CMD} doctrine:migration:migrate

.PHONY: migration-clientb m-b
m-p:
migration-clientb:
	rm -rf var/cache/*
	CLIENT_ID=ClientB ${CONSOLE_CMD} doctrine:migration:migrate

Mais au final, je changerais quand même une chose : étant donné que ce projet est pour mes clients et qu'il n'est pas question de partager le code à l’extérieur (sur GitHub en public par exemple), il est plus simple de générer des migrations uniquement pour les clients.

Serte, certaines requêtes SQL seront dupliquées pour chaque client et certaines migrations devront être réalisées pour chaque client. Mais cela ne gêne pas, car chaque client a son propre "serveur".

Les contrôleurs, services et formulaires

Pour les contrôleurs, service et formulaire, leur utilisation et leur surcharge pour un client sont exactement la même chose que pour tout projet Symfony.

Je note deux petites différences:

La première, sur un projet Symfony il n'y a qu'un seul fichier (en général) pour définir les services. Ici il y en a un pour les services communs (platform) et un pour chaque client.

La seconde, pour personnaliser un formulaire pour un client, il faudra utilise le mécanisme d'extension des formulaires de Symfony. Si ce point vous intéresse, dites-le-moi en commentaire.

Cette similitude avec les autres projets Symfony permettra une meilleure prise en main du projet pour les nouveaux arrivants.

Le déploiement

Ce projet utilise la solution "un code, plusieurs sites, plusieurs bases de données" décrite dans le premier post sur le sujet.

Cela implique que le code sera déployé avec un nom de domaine spécifique au site (sur un vhost ou un serveur par client) et autant de fois qu'il y a de site géré dans le code.

Conclusion

Avec ce projet, j'ai appris quelques petites choses sur la façon de gérer plusieurs sites dans le même code et sur Symfony.

J'ai pu constater que Symfony est extrêmement souple et je comprends encore mieux pourquoi il y a de nombreux projets qui l'utilisent comme framework.

J'espère que cela vous a également plu, je suis disponible en commentaire ou sur les réseaux sociaux pour en discuter, ou discuter de vos problématiques.

Rester connecter par le flux RSS ou les réseaux sociaux pour la suite des aventures.

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