Alors… on fait déjà ce qu’on peut coté doc : il y a déjà bien plus de doc de migration que pour n’importe quelle autre version précédente de SPIP qu’il a pu exister dans le passé je crois bien ; et par ailleurs, il n’existe pas vraiment de doc d’architecture du code actuel 4.4 non plus nul part (à part des bouts rédigés sur Programmer par bibi tant bien que mal)…
Mais aussi ça évolue encore, car au fil des avancées on se retrouve avec de nouveaux problèmes, et c’est d’une bonne complexité (je trouve) de démêler tout ça. Cela dit on avait envoyé un dossier « Architecture » Architecture SPIP 5.0 - SPIP 5.0 - Documentation technique dans la doc récemment. Et on continuera de la faire évoluer et compléter au fur et à mesure de certaines stabilités.
Cette doc globalement sera à rendre plus humaine (plus résumée à des endroits, plus détaillée dans d’autres) mais elle a le mérite d’exister au moins et déjà un peu relue quand même.
Sur la problématique « Container » (fallback / complet), je vais tenter d’y répondre car c’est un peu tricky.
1) Conteneur
Déjà, ce qui est appelé « Container », ou Conteneur ou parfois « le DI » donc, est un « Conteneur de service » , au sens le plus habituel dans le monde PHP et symfony (psr/container, symfony/dependency-injection). On y déclare des services (ex: un logger), et quand on fait $container->get('logger') : le service est instancié 1 première fois au premier appel (tout ce qui est dans le conteneur est « lazy » par défaut, c’est à dire pas instancié s’il n’est pas réellement utilisé ; et 1 seul sera instancié à l’utilisation, un peu comme un singleton) ; Si le logger nécessitait pour être instancié d’autres dépendances (monolog par exemple), elles seront gérées automatiquement (c’est le côté « DI » : l’injection est automatique) : tu n’as pas à faire je sais pas $logger = new Logger(new Monolog()) toi même. Et surtout chaque service reste lazy et instancié 1 seule fois (par défaut du moins).
2) Conteneur compilé
Ce qui est appelé conteneur compilé est une étape dans symfony/dependency-injection : une fois que tu as défini tous les paramètres et services qui doivent rentrer dans le conteneur (c’est un calcul qui peut être coûteux, car cela passe par des passes de compilations, d’analyse d’attributs #[XXX], etc), tu en fais un cache compilé : c’est ce cache que tu reliras aux hits suivants → ultra rapide / performant.
Et il y a des moyens de vérifier sa fraîcheur sur certaines actions (chez nous par exemple sur var_mode=recalcul), et de l’invalider s’il n’est pas frais (ie: si des fichiers de déclaration de services qui ont servis à créer le conteneur ont étés modifiés).
Une fois que le conteneur est compilé (où que c’est lui que tu utilises), tu ne peux plus faire certaines actions dessus : notamment tu ne peux plus déclarer de nouveaux services dessus, en tout cas ça devient limité, et c’est assez logique.
3) DI et HttpKernel dans symfony standard
Tu passes de « conteneur » (builder) à « conteneur compilé » s’il n’existe pas de façon transparente car symfony connaît tous ses services / paramètres sans avoir besoin d’exécuter / démarrer l’application ; En gros tu as sur un hit http : le Kernel boot (il crée son conteneur ou lit le cache), puis à un moment après ça va faire qqc comme $response = $kernel->handle($request) (j’ai plus exactement en tête), mais c’est dedans que vont se faire toute l’analyse de la requête (Get Post Cookies, le choix des routes, controleurs et réponses envoyées), et par l’intermédiaire de « listeners » (un peu comme nos pipelines) (kernel.request, kernel.response notamment) tu peux agir à différents endroits : c’est partiellement décrit là dedans The HttpKernel Component (Symfony Docs)
4) DI de symfony ET donc le httpKernel de symfony dans SPIP
Depuis une PR récente. On avait fait cela en gros
- boot de SPIP (inc_version.php)
- puis le httpKernel donc handle(request) avec quelques listeners
Le problème est que inc_version.php, ce qui boot SPIP (et va analyser les plugins), fait beaucoup de choses aussi autour du cycle HTTP, en faisant des traitements et actions autour de $_GET, $_POST, etc : et nous on veut que tous ces éléments soit dans la Request de symfony créée (sinon on n’arrivera jamais a avoir un cycle propre Request → Response).
- Donc on voudrait accéder Request dans des morceaux chargés par inc_version.php et dans des fichiers d’options
- On veut que les plugins puissent déclarer des paramètres et services au conteneur builder.
Plus récemment autre PR, on a tenté pensant bien faire une autre solution
- plutôt que de boot inc_version avant le handle(request), on le boot dans le cycle HTTP, dans un listener précoce
- ça paraissait prometteur (pour le coup on peut modifier la Request), mais on se retrouve avec d’autres problèmes
5) on ne peut pas avoir un conteneur → conteneur compilé
Étant donné que SPIP doit démarrer inc_version.php, (qui va utiliser des éléments du conteneur) : si le conteneur n’est pas encore compilé (ou en absence de cache des plugins) on va devoir déclarer les paramètres & services & pipelines (transformés en event listeners) des plugins au conteneur builder, en démarrant SPIP (comme il fait habituellement) avec ses fichiers d’options (qui vont influer _request, set_request => la Request pour certains)
Pour que ça marche partiellement il faut un conteneur minimal / fallback incorrect pour la suite du hit une fois le conteneur complet prêt, et faire un « remplacement » de l’un par l’autre pour la suite du hit.
Je passe les détails mais qu’on ait inc_version.php avant handle(request) ou dans handle(request) : ça ne va pas en l’état du code qu’on a.
SPIP doit démarrer pour savoir 1) les plugins actifs, 2) les pipelines et autres joyeusetés de ces plugins ajoutés par les fichiers _options.php notamment. C’est là où arrive le problème : on devrait démarrer (hors cache) un conteneur et conserver le même jusqu’à la compilation. mais on ne peut pas.
Tout cela est tricky car SPIP dans son boot mélange plusieurs choses : l’analyse des plugins / pipelines, initialisations de constantes / globales ET des actions sur hit HTTP courant.
6) Solution envisagée à tester
Pour faire face à ce problème on va certainement devoir découper plus finement le boot (et faire d’autres BC break malheureusement certainement). L’idée que j’aimerais tester est la suivante :
- un moyen de boot juste pour générer le cache des plugins et services, sans autre choses relatives au cycle HTTP, et notamment SANS charger les fichiers d’options qui font des actions runtime indésirables : on ferait CE boot (qui créera le conteneur compilé) avant le handle(request), le reste du cycles HTTP serait traité en listeners.
- les fichiers d’options seraient chargés dans le cycle HTTP donc (listener précoce), mais ils ne pourraient plus modifier au runtime (à l’exécution donc) des éléments relatifs à la conf du conteneur (surcharges de constantes _DIR, ajout de pipeline via
$GLOBALS['spip_pipeline], et peut être d’autres trucs que j’ai pas en tête)
- déclarer des pipelines passerait uniquement par paquet.xml ou mieux le nouveau config/services.php + les attributs
#[AsPipelineListener]
- plus possible d’avoir un pipeline (listener) ajouté au runtime en fonction de condition de la request (
if _request('truc')) { $GLOBALS['spip_pipeline']['truc'] .= ... }) : dans ce cas il faut déclarer le pipeline systématiquement, et tester le côté runtime dans le listener / pipeline.
Bon, je sens bien que ça ne va pas être aussi « simple » (huhu) que sur le papier, mais si on veut pouvoir profiter plus serainement du cycle http de symfony et du conteneur performant, on va devoir passer par là.