Une nouvelle façon d'écrire des applications @php[architect]
Volume 24 - Numéro 12 (12/2025) by Chris Tankersley
Comme je l'ai mentionné dans le numéro d'octobre 2025 de PHP Architect, FrankenPHP s'est imposé comme un serveur d'applications révolutionnaire pour PHP. Avec le soutien officiel de la Fondation PHP depuis mai 2025, il est en train de devenir une technologie phare dans l'écosystème PHP. Si FrankenPHP facilite le déploiement des applications PHP traditionnelles, il introduit également une nouvelle approche pour créer votre application. Celle-ci est appelée « mode Worker ».
Le mode worker de FrankenPHP est une technique d'optimisation des performances qui maintient un état d'application persistant en mémoire sur plusieurs requêtes HTTP.
Contrairement à l'exécution PHP traditionnelle, où chaque requête déclenche un démarrage complet de votre application, le mode worker démarre une seule fois, puis traite les requêtes dans une boucle continue.
Cela permet à PHP de fonctionner davantage comme d'autres langages disposant de serveurs d'applications, contrairement à une application PHP traditionnelle.
Considérez le PHP traditionnel comme un distributeur automatique qui doit être réapprovisionné et recalibré à chaque achat.
Le mode Worker, en revanche, est comme un caissier qui se présente une fois, ouvre la caisse enregistreuse, puis sert les clients toute la journée. La configuration initiale se fait une seule fois, et les opérations suivantes sont nettement plus rapides.
L'architecture tire parti des capacités de concurrence de Go, car FrankenPHP est construit sur Caddy, qui est écrit en Go, afin de gérer plusieurs threads de travail. Chaque thread traite ensuite les requêtes de manière séquentielle.
Par défaut, FrankenPHP génère 2 threads de travail par cœur de processeur, mais cela est configurable en fonction des besoins de votre application.
Mode traditionnel FrankenPHP : le cycle de vie de la requête
Avant de nous plonger dans le mode worker, examinons comment fonctionne l'exécution PHP traditionnelle, même sous l'architecture serveur moderne de FrankenPHP.
En mode traditionnel, chaque requête HTTP suit un cycle de vie familier.
Lorsqu'une requête arrive sur le serveur, le runtime PHP s'initialise avec un nouvel interpréteur. Votre application démarre alors complètement : votre framework se charge, les fichiers de configuration sont analysés et les services sont instanciés.
Ce n'est qu'une fois cette configuration terminée que la logique de votre application s'exécute pour traiter la demande. Une fois la réponse renvoyée au client, le processus PHP se termine complètement, libérant toute la mémoire et supprimant tout état qu'il avait accumulé.
Voici à quoi cela ressemble dans notre application de démonstration, qui se contentera de stocker une liste de tâches à accomplir : (voir l'image 1).
Ce modèle est simple et prévisible.
Chaque requête commence à partir d'une page blanche, ce qui élimine les problèmes liés à la pollution d'état entre les requêtes. Cependant, cette sécurité a un coût élevé : vous payez la pénalité de démarrage à chaque fois.
Le chargement de la base de données peut considérablement ralentir notre application, et nous gaspillons des cycles CPU en chargeant toutes les données, même si tout ce que nous faisons est d'ajouter une nouvelle tâche.
Mode worker : Code d'exécution long
Le mode Worker inverse ce modèle. Au lieu d'effectuer un amorçage pour chaque requête, vous effectuez un amorçage unique et traitez les requêtes dans une boucle : (voir l'image 2).
Une grande partie de ce code ressemblera à celui d'une application PHP traditionnelle.
La principale différence réside dans la fonction frankenphp_handle_request(). C'est là que réside la magie de FrankenPHP : elle met votre script en pause, attend une requête HTTP entrante, remplit les superglobales ($_GET, $_POST, $_SERVER, etc.), exécute votre gestionnaire, puis revient en boucle pour attendre la requête suivante.
Il englobe toutes les tâches désagréables que vous devez effectuer pour que votre code PHP puisse obtenir les informations attendues au cours du cycle de vie PHP.
L'autre différence majeure réside dans le fait que nous disposons désormais d'une véritable persistance entre les requêtes. Non seulement nous créons notre TaskManager une seule fois, mais nous disposons également d'une petite variable globale appelée $requestCount qui continue à compter. Nous ne stockons jamais cette valeur ailleurs que dans la variable, ce qui nous permet de constater que le mode Worker maintient la connexion active d'une requête à l'autre.
Mémoire et état
Le mode traditionnel fonctionne avec un espace mémoire frais pour chaque requête, garantissant ainsi qu'aucun état ne persiste entre les requêtes.
Cela rend les fuites de mémoire pratiquement impossibles, car le processus s'arrête après chaque requête, assurant ainsi une isolation complète entre les requêtes.
Le mode Worker, en revanche, fonctionne de manière très différente. La mémoire est partagée entre les requêtes et l'état persiste, que vous le souhaitiez ou non. Cela signifie que les fuites de mémoire peuvent s'accumuler au fil du temps jusqu'au redémarrage du worker, et que l'état partagé nécessite une gestion minutieuse et réfléchie pour éviter les bugs.
C'est un élément à prendre en compte lorsque vous utilisez des paquets tiers. La plupart d'entre eux ne sont pas conçus pour gérer les fuites de mémoire et peuvent être des tueurs silencieux. FrankenPHP dispose d'un moyen de faire tourner les workers après un certain temps ou un certain nombre de requêtes pour éviter cela, mais c'est un problème auquel vous pourriez être confronté.


Profil de performance
Le mode traditionnel offre une surcharge constante par requête avec une utilisation prévisible de la mémoire. Cependant, cette cohérence a un coût. Les cycles CPU sont utilisés à plusieurs reprises pour démarrer l'application, ce qui entraîne une latence plus élevée qui devient particulièrement perceptible dans les applications basées sur un framework.
Le mode Worker transforme complètement ce profil. Le coût du démarrage est amorti sur des milliers de requêtes, ce qui permet un traitement des requêtes beaucoup plus rapide lors d'une exécution à grande échelle.
L'utilisation du processeur peut diminuer considérablement, car le démarrage n'a lieu qu'une seule fois. En contrepartie, l'utilisation de la mémoire peut augmenter progressivement au fil du temps si votre application ou ses dépendances présentent des fuites.
Complexité
Le mode traditionnel offre un modèle mental simple sans considérations particulières pour le traitement des requêtes : les applications existantes fonctionnent sans modification. Il respecte les attentes normales du cycle de vie PHP, votre code n'a donc pas besoin de se soucier de quoi que ce soit de supplémentaire.
Le mode Worker est plus exigeant pour les développeurs. Vous devez comprendre le comportement de l'état persistant, gérer avec soin ce qui persiste et ce qui est réinitialisé entre les requêtes, implémenter des routines de nettoyage explicites et tester minutieusement les bugs de fuite d'état qui ne peuvent tout simplement pas se produire dans le PHP traditionnel.
Prenons notre classe TaskManager. Elle suppose qu'il existe une seule file d'attente de tâches globale, sans tenir compte des différents utilisateurs. Toute personne soumettant une tâche l'ajoutera à la file d'attente. Si vous souhaitez pouvoir l'utiliser avec différents utilisateurs, vous devrez la modifier. Il en va de même pour $requestCount. Quelle que soit la personne qui effectue la requête, cette valeur augmentera.
Cela peut constituer un problème de sécurité majeur pour le code PHP existant, qui peut s'attendre à ce qu'un autre processus gère le sous-ensemble de données, ou qui définit quelque chose comme un utilisateur global au démarrage, ou d'autres hypothèses de ce type. C'est là que se concentre l'essentiel de la refactorisation du code, passant du mode traditionnel au mode Worker.
Passage au mode worker : considérations essentielles
La conversion d'une application du mode traditionnel au mode worker n'est pas aussi simple que de modifier une variable de configuration. Comme je l'ai déjà mentionné, il existe certains problèmes structurels que vous devrez peut-être prendre en compte si vous transférez l'intégralité de votre application vers le mode worker.
1. État global et singletons
Traditionnellement en PHP, l'état global est réinitialisé à chaque requête. En mode worker, il persiste.

Ici, nous passons de l'utilisation d'une interface singleton statique pour obtenir l'utilisateur actuel à une classe instanciée pour conserver l'utilisateur actuel. Les singletons deviennent très sensibles aux fuites de données ou peuvent introduire des bogues si leurs données ne sont pas réinitialisées correctement. Partout où vous pouvez avoir un singleton, vous pouvez passer à une instanciation plus explicite et transmettre les données nécessaires.
Vous devez explicitement réinitialiser tous les objets avec état entre les requêtes. Cela inclut les sessions utilisateur, l'état d'authentification et les caches spécifiques aux requêtes. Les connexions à la base de données persistent également, mais vous devez vous assurer qu'elles restent valides. N'oubliez pas les descripteurs de fichiers temporaires et autres ressources qui pourraient sinon s'accumuler.
2. Comportement des superglobales
Les superglobaux se comportent différemment en mode worker.
Initialement, ils contiennent les valeurs du script worker. Après le premier appel à la fonction frankenphp_handle_request(), ils reflètent la requête HTTP en cours
Si vous avez besoin d'accéder à l'environnement de travail d'origine dans votre gestionnaire, vous devrez le capturer dans une closure :

3. Gestion des exceptions
Traditionnellement en PHP, les exceptions non interceptées mettent fin au script. En mode worker, set_exception_handler() ne se déclenche que lorsque le script worker lui-même se termine et ne se déclenche pas pendant le traitement de la requête.
Vous devez encapsuler votre logique de gestion dans des blocs try-catch :

Vous devriez le faire de toute façon, mais de nombreux scripts plus anciens se contentent de s'appuyer sur le comportement de terminaison de PHP et exécutent à nouveau le code plus tard. Vous devrez peut-être être plus explicite dans votre gestion des erreurs avec le mode Worker.
4. Gestion de la mémoire
PHP n'a pas été conçu pour les processus de longue durée, et certaines bibliothèques peuvent présenter des fuites de mémoire. Pour les anciens codes, vous pourriez vouloir appeler manuellement le ramasse-miettes pour nettoyer après des tâches complexes.
Si vous avez déjà écrit des démons à exécution longue, comme des programmes en ligne de commande, vous avez déjà fait quelque chose de similaire.
Vous pouvez également tirer parti des destructeurs des classes PHP, qui sont appelés lorsque quelque chose est sur le point d'être récolté par le ramasse-miettes.
Cela peut aider le moteur à savoir quand d'autres ressources ne sont plus nécessaires, ce qui réduit encore l'utilisation de la mémoire.
Vous pouvez également configurer FrankenPHP pour redémarrer un worker après un certain nombre de requêtes. Cela peut être une solution rapide pour des problèmes de gestion de mémoire plus complexes en contournant complètement le problème !
FrankenPHP recherchera une variable d'environnement nommée MAX_REQUESTS et redémarrera les workers une fois le nombre de requêtes atteint.
Cela empêche les opérateurs d'accumuler de la mémoire au fil des jours de fonctionnement.
5. Connexions à la base de données
La plupart des applications PHP laissent la base de données gérer la persistance des connexions, mais vous pouvez l'aider en spécifiant que vous souhaitez que vos connexions soient persistantes. Dans PDO, il s'agit de l'attribut PDO::ATTR_PERSISTENT que vous pouvez utiliser lors de la connexion à la base de données.
Si vous utilisez un framework ou une bibliothèque, vous devrez consulter la documentation pour activer les connexions persistantes.
Ce n'est pas une solution miracle. MySQL aura tendance à rencontrer un problème avec des erreurs « MySQL has gone away » qui apparaissent au fil du temps.
Vous pouvez ajouter une logique supplémentaire pour vérifier si la connexion à la base de données fonctionne toujours avant de renvoyer l'objet PDO :

6. Descripteurs de fichiers et ressources
En général, les descripteurs de fichiers, les sockets et autres ressources ne se ferment pas automatiquement.
Vous devrez peut-être modifier votre code afin de fermer plus explicitement ces ressources lorsque vous avez terminé.
C'est là que les destructeurs peuvent s'avérer utiles, mais prenez l'habitude de fermer explicitement toutes les connexions que vous ouvrez.
Considérations sur le processus de développement
Le mode Worker modifie votre flux de travail de développement.
En mode traditionnel, les modifications du code sont immédiatement prises en compte.
En mode Worker, vous devez redémarrer les workers pour voir les modifications.
Heureusement, il existe un flag --watch que vous pouvez utiliser pendant le développement :
Cela redémarre automatiquement les workers lorsque les fichiers changent.
Les outils tels que Xdebug fonctionnent en mode worker, mais n'oubliez pas que les points d'arrêt à l'intérieur de la boucle worker interrompent le traitement des requêtes.
Même avec Fibers et les modifications apportées par FrankenPHP au moteur, cela peut causer des problèmes avec d'autres codes et empêcher leur exécution si vous vous trouvez dans un point d'arrêt.
Votre suite de tests doit tenir compte de la persistance de l'état. Envisagez d'ajouter des tests spécialement conçus pour détecter les fuites d'état entre les requêtes, surveiller l'utilisation de la mémoire sur plusieurs requêtes et vérifier que vos méthodes de réinitialisation nettoient correctement tout l'état de l'application.
Quand utiliser le mode Worker
Le mode Worker n'est pas toujours le bon choix. Envisagez de l'adopter lorsque votre application a une charge de démarrage importante, que vous avez besoin d'une performance maximale et que vous pouvez gérer la complexité supplémentaire, que vos modèles de trafic justifient les économies d'infrastructure et que vous pouvez tester correctement les bogues liés à l'état.
Restez en mode traditionnel lorsque votre application est simple avec une surcharge minimale au démarrage, lorsque le débogage des problèmes d'état persistant l'emporterait sur les gains de performances, si vous utilisez des bibliothèques connues pour leurs fuites de mémoire, ou si votre équipe n'est pas familiarisée avec la gestion des processus PHP de longue durée. Parfois, la simplicité du mode traditionnel vaut le coût en termes de performances.
Conclusion
Le mode worker de FrankenPHP représente une évolution significative dans la manière dont les applications PHP peuvent être déployées et exploitées.
En conservant un état persistant d'une requête à l'autre, il atteint des niveaux de performance qui nécessitaient auparavant d'autres langages ou des stratégies de mise en cache complexes.
Même si je continue de penser que la plupart des applications PHP n'ont pas besoin d'opérations asynchrones, avec le soutien officiel de la Fondation PHP, le mode worker est en passe de devenir un modèle de déploiement standard pour les applications PHP hautes performances.
Cependant, ces performances s'accompagnent de responsabilités.
Les développeurs doivent réfléchir attentivement à la gestion de l'état, mettre en œuvre un nettoyage approprié et tester minutieusement les problèmes qui n'existent tout simplement pas dans le PHP traditionnel.
Le modèle mental passe de « rien n'est partagé » à « tout est partagé sauf si explicitement réinitialisé ».
Pour les équipes qui développent des applications critiques en termes de performances et qui sont prêtes à investir pour comprendre les nuances du mode worker, les avantages sont considérables.
Commencez par diviser l'application en petits morceaux, en services plus petits, et voyez comment ils fonctionnent pour vous.
Qui sait, je me trompe peut-être, et le mode asynchrone et worker est l'avenir. Nous ne le saurons qu'après avoir essayé.