Comment PHP sâexĂ©cute
PHP nâexĂ©cute pas votre application : il exĂ©cute votre script, une fois, pour une requĂȘte, puis il sâarrĂȘte. Il nây a pas dâobjet serveur Ă instancier, pas dâappel Ă listen(), pas de boucle dâĂ©vĂ©nements. Quelque chose dâextĂ©rieur Ă PHP (un serveur web, ou vous dans un terminal) lance lâinterprĂ©teur, lâinterprĂ©teur exĂ©cute un fichier de haut en bas, et tout ce quâil a allouĂ© est libĂ©rĂ© quand le fichier se termine.
Tous les autres chapitres de ce livre sont plus faciles une fois celui-ci assimilé.
Deux portes dâentrĂ©e
Depuis un terminal, PHP se comporte comme Python ou Ruby :
php hello.php
php -r 'echo PHP_VERSION, PHP_EOL;'
php -a # interactive shell
php -l file.php # syntax check only
php -S localhost:8000 # development web server, current folder as document root
Sur le web, PHP nâest pas le serveur. Le serveur web reçoit la requĂȘte HTTP et la transmet Ă PHP, le plus souvent via PHP-FPM, un pool de processus PHP en attente derriĂšre nginx, Apache ou Caddy. Un processus prend la requĂȘte, exĂ©cute le script auquel lâURL correspond, Ă©crit la rĂ©ponse, nettoie, et retourne attendre la suivante. Le pool compte autant de processus que vous en configurez, et câest ainsi que PHP utilise tous vos cĆurs, avec des processus plutĂŽt quâavec des threads.
La couche qui relie lâinterprĂ©teur au monde extĂ©rieur sâappelle une SAPI (server API). La ligne de commande, FPM et le mod_php dâApache sont trois SAPI diffĂ©rentes posĂ©es sur le mĂȘme moteur ; seule la plomberie change de lâune Ă lâautre.
Rien de partagé
Câest la partie qui change votre façon dâĂ©crire du code. Rien ne survit dâune requĂȘte Ă la suivante Ă lâintĂ©rieur de PHP. Une propriĂ©tĂ© statique que vous affectez, une globale, une connexion ouverte, un objet mis en cache dans un tableau : tout cela existe le temps dâune requĂȘte, puis disparaĂźt.
<?php
declare(strict_types=1);
final class Counter
{
public static int $hits = 0;
}
Counter::$hits++;
echo Counter::$hits; // 1, on every single request, forever
ExĂ©cutez ce code sous un serveur web, rechargez la page cent fois, et il affiche 1 cent fois, lĂ oĂč le mĂȘme code en Node ou en Java compterait jusquâĂ 100. Aucun des deux comportements nâest un bug, ce sont simplement deux modĂšles diffĂ©rents.
Les consĂ©quences sâenchaĂźnent :
- Un bug nâaffecte quâune requĂȘte. Fuite mĂ©moire, boucle infinie, exception non rattrapĂ©e : le processus qui traitait cette requĂȘte meurt ou est recyclĂ©, et la requĂȘte suivante en reçoit un neuf.
- La montĂ©e en charge est horizontale par construction. Plus de trafic demande plus de processus FPM ou plus de machines, et rien dans lâapplication nâa besoin dâĂȘtre thread-safe, puisque rien nâest partagĂ©.
- LâĂ©tat vit hors de PHP. Les sessions vont dans des fichiers, une base de donnĂ©es ou un stockage clĂ©-valeur. Les caches vont dans OPcache et APCu (mĂ©moire partagĂ©e entre les processus dâune machine) ou dans un stockage externe comme Redis ou Memcached. La connexion Ă la base sâouvre au dĂ©but de la requĂȘte et se ferme Ă la fin ; le pooling, si vous en avez besoin, se fait dans un pooler devant la base, pas dans PHP.
- Le coĂ»t de dĂ©marrage se paie Ă chaque requĂȘte. Câest ce qui explique que PHP dĂ©marre vite, et que lâĂ©cosystĂšme accorde autant dâattention Ă lâautoloading, Ă OPcache et au prĂ©chargement.
Si vous vous surprenez Ă concevoir un singleton pour « garder la connexion ouverte entre les requĂȘtes », arrĂȘtez-vous : dans ce modĂšle, il nâexiste aucun moment entre deux requĂȘtes.
Le cache de bytecode
Lire et compiler chaque fichier Ă chaque requĂȘte serait lent, alors PHP ne le fait pas. OPcache conserve la forme compilĂ©e de chaque fichier en mĂ©moire partagĂ©e, et la requĂȘte suivante la rĂ©utilise. Il est livrĂ© avec PHP et activĂ© par dĂ©faut dans toute installation sĂ©rieuse.
En dĂ©veloppement, OPcache vĂ©rifie les dates des fichiers et recompile ce qui a changĂ©, donc la boucle Ă©diter-recharger fonctionne telle quelle, sans Ă©tape de build ni watcher. En production, la vĂ©rification des dates est en gĂ©nĂ©ral dĂ©sactivĂ©e pour gagner du temps, ce qui signifie quâun dĂ©ploiement doit rĂ©initialiser le cache, en redĂ©marrant FPM ou en appelant opcache_reset(). Quand on lâoublie, on obtient le classique « jâai dĂ©ployĂ© et rien nâa changĂ© ».
OPcache hĂ©berge aussi le compilateur JIT (PHP 8.0). Il aide surtout les scripts gourmands en CPU, et ce nâest pas lui qui rend une application web rapide, donc voyez-le comme une option Ă essayer plutĂŽt que comme une fondation.
PHP persistant
Le modĂšle sans Ă©tat partagĂ© est le comportement par dĂ©faut, pas une loi. Plusieurs runtimes gardent votre application en mĂ©moire entre les requĂȘtes, comme le ferait un serveur Node ou Java : FrankenPHP en mode worker, RoadRunner, et Swoole ou OpenSwoole. Votre amorçage sâexĂ©cute une fois, puis une boucle vous tend les requĂȘtes lâune aprĂšs lâautre.
Le gain est rĂ©el, puisque le coĂ»t dâamorçage disparaĂźt et que les connexions peuvent vraiment rester ouvertes. Le coĂ»t est celui que vous connaissez dĂ©jĂ des autres langages : lâĂ©tat fuit si vous ne le nettoyez pas, une fuite mĂ©moire grossit, et un compteur statique compte pour de bon. Les frameworks qui prennent en charge ces runtimes rĂ©initialisent leur conteneur entre deux requĂȘtes prĂ©cisĂ©ment pour cela. Commencez avec FPM, et passez Ă un runtime worker quand vous aurez mesurĂ© une raison de le faire.
Ce quâil y a dans la boĂźte
LâinterprĂ©teur est un cĆur en C plus des extensions, certaines intĂ©grĂ©es et toujours actives, dâautres compilĂ©es au build, dâautres installĂ©es Ă part. php -m liste ce que contient votre build. Celles dont vous remarquerez lâabsence sur une installation fraĂźche sont en gĂ©nĂ©ral pdo_mysql ou pdo_pgsql (pilotes de base de donnĂ©es), intl (collation Unicode, formatage), mbstring (chaĂźnes multi-octets), curl, gd ou imagick (images), et xdebug (dĂ©bogueur, dĂ©veloppement seulement).
Votre gestionnaire de paquets les fournit sous forme de paquets sĂ©parĂ©s (php-intl, php-mbstring, etc.), et lâimage Docker officielle fournit docker-php-ext-install. Les extensions non livrĂ©es avec PHP viennent de PECL, ou de PIE, lâinstalleur dâextensions plus rĂ©cent, dans lâesprit de Composer.
La configuration vit dans php.ini. La ligne de commande et FPM lisent des fichiers ini diffĂ©rents, ce qui explique quâun script se comporte dâune façon dans un terminal et dâune autre sous le serveur web. php --ini montre les fichiers que charge la CLI, et phpinfo() dans une page montre ceux que charge FPM. Deux rĂ©glages comptent dĂšs le premier jour : memory_limit (128 Mo par dĂ©faut sous FPM, illimitĂ© en CLI) et max_execution_time (30 secondes sous FPM, illimitĂ© en CLI).
Le piĂšge
En venant dâun runtime persistant, la premiĂšre erreur est dâattendre que la mĂ©moire persiste, avec un cache dans un tableau statique, une classe « pool de connexions » ou un compteur pour limiter le dĂ©bit. Sous FPM, tout cela ne sert silencieusement Ă rien. La seconde erreur est le miroir de la premiĂšre : aprĂšs le passage Ă un runtime worker, une valeur propre Ă une requĂȘte rangĂ©e dans une statique se retrouve partagĂ©e entre les utilisateurs.
Posez une seule question sur tout Ă©tat : doit-il survivre Ă cette requĂȘte ? Si oui, sa place nâest pas dans la mĂ©moire de PHP, mais dans la base, dans un cache ou dans la session.
Une fois ce modĂšle en place, la syntaxe est la partie facile, et câest lâobjet du chapitre La syntaxe.