đ Et maintenant, PHP
Un guide de PHP moderne qui se lit en deux Ă trois heures, pour les dĂ©veloppeurs venus dâun autre langage.
Vous savez coder, et on vient de vous confier un projet PHP, ou bien vous revenez Ă ce langage aprĂšs des annĂ©es passĂ©es ailleurs. Vous nâavez pas besoin dâun tutoriel, mais du delta entre ce que vous savez dĂ©jĂ et ce que PHP fait Ă sa maniĂšre : comment il sâexĂ©cute, comment il type, comment il organise le code en paquets, oĂč il va vous surprendre, et Ă quoi ressemble le PHP dâaujourdâhui une fois lâancienne rĂ©putation mise de cĂŽtĂ©.
Ce livre se lit en deux à trois heures. Chaque chapitre répond à une question, « comment PHP fait-il ça ? », puis passe à la suivante.
Le PHP décrit ici est PHP 8.5. Les fonctionnalités plus récentes que PHP 8.2 portent leur numéro de version, pour que vous sachiez ce que votre projet peut utiliser.
Introduction
On vous a confiĂ© une base de code PHP, que vous lâayez demandĂ©e ou non. Vous ouvrez le premier fichier, vous y trouvez $this->, des ->, des :: et une fonction qui sâappelle htmlspecialchars, et vous vous demandez si câest bien le langage dont tout le monde vous a dit du mal.
En bonne partie, oui, mais pas celui dâaujourdâhui. Le PHP dont on vous a parlĂ© a existĂ©, et il a presque entiĂšrement disparu. Ce qui reste est un langage typĂ©, orientĂ© objet, avec ramasse-miettes, un gestionnaire de paquets, un style de code standardisĂ©, un Ă©cosystĂšme dâanalyse statique sĂ©rieux, une version par an chaque novembre, et un modĂšle dâexĂ©cution qui ne ressemble Ă rien de ce que vous avez utilisĂ© jusquâici. Ce modĂšle dâexĂ©cution est la seule chose que vous ayez vraiment Ă apprendre.
Ce qui était vrai
PHP est nĂ© en 1995 comme un jeu de gabarits avec un peu de logique dedans, et pendant sa premiĂšre dĂ©cennie il a Ă©tĂ© permissif jusquâĂ lâabsurde. Les variables apparaissaient de nulle part, "abc" == 0 valait vrai, les erreurs sâimprimaient dans la page et lâexĂ©cution continuait, les requĂȘtes SQL se construisaient par concatĂ©nation, et la bibliothĂšque standard sâest assemblĂ©e une fonction Ă la fois, au grĂ© des besoins de chacun, ce qui explique que strpos voisine avec str_replace et array_key_exists avec in_array.
Toute une gĂ©nĂ©ration a appris Ă programmer sur ce PHP-lĂ , en a Ă©crit Ă©normĂ©ment, et une bonne partie de ce code tourne encore. Câest de ce PHP que parlent les blagues.
Ce qui a changé
PHP 7 (2015) a doublĂ© les performances et ajoutĂ© les dĂ©clarations de types scalaires. PHP 8 (2020) a apportĂ© un vrai systĂšme de types avec les types union, match, les arguments nommĂ©s, les attributs, les Ă©numĂ©rations, readonly, les callables de premiĂšre classe et un compilateur JIT. PHP 8.4 a ajoutĂ© les hooks de propriĂ©tĂ© et la visibilitĂ© asymĂ©trique, et PHP 8.5 un opĂ©rateur pipe. Dans le mĂȘme mouvement, les rĂšgles de comparaison ont Ă©tĂ© corrigĂ©es, les propriĂ©tĂ©s dynamiques dĂ©prĂ©ciĂ©es, les vieilles fonctions mysql_* supprimĂ©es, et lâinterprĂ©teur lĂšve dĂ©sormais une TypeError lĂ oĂč il devinait.
Autour du langage, la communautĂ© a construit Composer, le gestionnaire de paquets que tout projet utilise ; les standards du PHP-FIG, pour que les bibliothĂšques dâauteurs diffĂ©rents sâemboĂźtent ; PHPUnit et Pest pour les tests ; PHPStan et Psalm, deux analyseurs statiques qui vous donnent lâessentiel de ce quâun compilateur donnerait ; et Rector, qui réécrit le vieux code en syntaxe moderne. Depuis 2021, la PHP Foundation salarie des dĂ©veloppeurs du cĆur du langage, pour que son avenir ne dĂ©pende plus des soirĂ©es de bĂ©nĂ©voles.
Ce qui pique encore
Tout nâa pas Ă©tĂ© corrigĂ©, et un livre pour gens pressĂ©s doit le dire dâemblĂ©e. Les chaĂźnes sont des suites dâoctets, donc strlen('Ă©') vaut 2 et il vous faut les fonctions mb_ dĂšs quâil sâagit de texte. La bibliothĂšque standard garde ses noms et ses ordres dâarguments incohĂ©rents, == convertit toujours ses opĂ©randes, quoique bien moins sauvagement quâavant, le typage strict est un interrupteur par fichier quâil faut activer, les tableaux se copient Ă lâaffectation, et le langage lui-mĂȘme nâa pas de gĂ©nĂ©riques. Chacun de ces points a une pratique moderne qui le neutralise, et chacun est traitĂ© au chapitre oĂč vous le rencontrerez.
La rĂ©putation de PHP est restĂ©e en 2010, alors que le langage a continuĂ© dâavancer.
LâidĂ©e Ă saisir en premier
Une requĂȘte web PHP dĂ©marre sans rien, exĂ©cute votre code de haut en bas, envoie sa rĂ©ponse et jette tout. Il nây a pas de mĂ©moire partagĂ©e entre les requĂȘtes, pas dâobjet application qui reste en vie, pas de boucle dâĂ©vĂ©nements et pas de threads : le serveur web tend une requĂȘte Ă PHP, PHP produit une rĂ©ponse, puis oublie tout ce quâil vient de faire.
Si vous venez de Node, de Java, de Go ou dâun serveur Python ASGI, câest la diffĂ©rence la plus importante, et la plupart des idiomes PHP en dĂ©coulent : lâabsence de pool de connexions par dĂ©faut, « lâĂ©tat global » qui nâest quâune notion par requĂȘte, le plantage qui nâaffecte quâun visiteur, la montĂ©e en charge par ajout de processus, lâexistence dâOPcache, et les serveurs PHP persistants comme FrankenPHP ou RoadRunner qui forment un sujet Ă part entiĂšre. Comment PHP sâexĂ©cute traite tout cela, et câest le seul chapitre Ă ne pas sauter.
Comment lire ce livre
Chaque chapitre rĂ©pond Ă une question et se suffit Ă lui-mĂȘme. Les phrases en gras portent le fil : en ne lisant que celles-lĂ , vous obtenez le delta entre PHP et ce que vous connaissez dĂ©jĂ . Les blocs de code vous donnent la syntaxe, et le reste du texte est lĂ pour le jour oĂč un dĂ©tail compte pour vous.
Les comparaisons avec Python, JavaScript, Java et quelques autres langages sont des points de repĂšre, pas des traductions, et vous pouvez sauter celles des langages que vous ne connaissez pas sans rien perdre de lâexplication.
Chaque exemple tourne sur une installation nue de PHP avec php fichier.php, sans framework ni bibliothĂšque, pour que la leçon porte sur PHP lui-mĂȘme. Installez dâabord PHP : votre gestionnaire de paquets lâa, php.net liste les builds officiels, et lâimage Docker officielle est php:8.5-cli. Gardez ensuite un terminal ouvert et exĂ©cutez ce que vous lisez.
Trois chapitres sâadressent Ă un lecteur en particulier. Revenir Ă PHP aprĂšs des annĂ©es fait correspondre les vieilles habitudes aux pratiques modernes, pour ceux qui reviennent au langage. Venir de Python, JavaScript ou Java est une table de correspondance, et PHP 8.0 Ă 8.5 en un coup dâĆil vous dit ce que la version PHP de votre projet sait faire.
Dans deux ou trois heures, la base de code que vous avez ouverte aura changĂ© dâaspect : pas forcĂ©ment plus simple, mais lisible.
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.
La syntaxe
PHP ressemble Ă du C avec un $ devant chaque variable, -> pour les membres et . pour concatĂ©ner les chaĂźnes. Si vous lisez du Java, du JavaScript ou du C#, vous lisez dĂ©jĂ du PHP, et le reste de ce chapitre est la liste des endroits oĂč vos doigts vont taper la mauvaise chose.
Un fichier, de haut en bas
<?php
declare(strict_types=1);
$name = 'world';
$count = 3;
echo "Hello, {$name}!", PHP_EOL;
echo 'Hello, ' . $name . '! Count: ' . $count . PHP_EOL;
Le fichier sâouvre sur <?php. Tout ce qui prĂ©cĂšde cette balise, mĂȘme une ligne vide, part tel quel dans la sortie, parce que PHP a commencĂ© sa vie comme langage de gabarits. Un fichier qui ne contient que du code sâouvre sur <?php et ne referme jamais la balise, parce quâun ?> final suivi dâun saut de ligne oubliĂ© ferait fuir ce saut de ligne dans votre rĂ©ponse HTTP.
declare(strict_types=1); vient ensuite, sur sa propre ligne, et dĂ©sactive la conversion automatique des arguments scalaires dans les appels faits depuis ce fichier. Le systĂšme de types explique prĂ©cisĂ©ment ce que cette ligne couvre et ce quâelle ne couvre pas ; pour lâinstant, mettez-la dans chaque fichier, et considĂ©rez un fichier qui ne lâa pas comme un fichier portant un panneau dâavertissement.
Les variables commencent par $ et ne se dĂ©clarent jamais : vous affectez, et la variable existe, sans let, sans var et sans type devant. Les noms de variables sont sensibles Ă la casse, alors que les noms de fonctions et de classes ne le sont pas, mĂȘme si personne ne sâappuie sur cette tolĂ©rance.
Les instructions se terminent par un point-virgule. echo prend une liste sĂ©parĂ©e par des virgules et lâaffiche, print fait la mĂȘme chose avec un seul argument et se voit rarement, et PHP_EOL est le saut de ligne de la plateforme.
Les chaĂźnes
LâopĂ©rateur de concatĂ©nation est ., pas +. Ăcrire $a + $b avec deux chaĂźnes pousse PHP Ă les additionner comme des nombres, ce qui donne une TypeError pour des chaĂźnes non numĂ©riques sous PHP 8.
Les guillemets simples vous donnent les octets que vous avez tapĂ©s, tandis que les guillemets doubles interpolent les variables et traduisent les sĂ©quences comme \n. Entourez dâaccolades tout ce qui dĂ©passe une simple variable :
<?php
declare(strict_types=1);
$user = ['name' => 'Ada'];
$items = 3;
echo "Hi {$user['name']}, you have {$items} items\n";
echo 'Hi {$user[name]}\n'; // printed literally, backslash included
Pour du texte sur plusieurs lignes, un heredoc interpole et un nowdoc nâinterpole pas. Le marqueur de fin peut ĂȘtre indentĂ© (depuis PHP 7.3), et cette indentation est retirĂ©e de chaque ligne :
<?php
declare(strict_types=1);
$title = 'Report';
$html = <<<HTML
<h1>{$title}</h1>
<p>Generated by PHP</p>
HTML;
$raw = <<<'TXT'
No {$interpolation} in here.
TXT;
echo $html, PHP_EOL, $raw, PHP_EOL;
Trois flĂšches
PHP a trois symboles en forme de flÚche, et leurs rÎles ne se recouvrent jamais. -> entre dans une instance, comme dans $user->name ou $user->save(). :: entre dans une classe, comme dans User::create(), User::MAX_AGE ou self::$count. => associe une clé à une valeur dans un tableau littéral ou une branche de match, comme dans ['id' => 1].
<?php
declare(strict_types=1);
final class Money
{
public const string CURRENCY = 'EUR'; // typed constant, PHP 8.3
public function __construct(public readonly int $cents)
{
}
public static function zero(): self
{
return new self(0);
}
}
$price = new Money(1999);
echo $price->cents, ' ', Money::CURRENCY, ' ', Money::zero()->cents, PHP_EOL;
// 1999 EUR 0
Dans une mĂ©thode, lâinstance courante est $this. Les classes et les objets ont leur propre chapitre, Les classes, et cet exemple montre seulement quelle flĂšche va oĂč.
LâĂ©galitĂ©
Utilisez ===, et considĂ©rez == comme un opĂ©rateur hĂ©ritĂ©. Le double Ă©gal convertit les deux cĂŽtĂ©s vers un type commun avant de comparer, ce qui a produit les absurditĂ©s cĂ©lĂšbres du vieux PHP. Depuis PHP 8, un nombre comparĂ© Ă une chaĂźne non numĂ©rique est comparĂ© comme une chaĂźne, donc 0 == 'foo' vaut false, mais '1' == '01' vaut toujours true : le comportement est plus sain quâavant, et reste un jeu de devinettes. Le triple Ă©gal compare la valeur et le type, sans rien deviner.
La mĂȘme rĂšgle vaut pour !== contre !=, et pour in_array() et array_search(), qui prennent un troisiĂšme argument true pour comparer strictement.
<=> est lâopĂ©rateur vaisseau spatial : il renvoie -1, 0 ou 1, et existe pour les fonctions de tri.
Null, en trois opérateurs
<?php
declare(strict_types=1);
$config = ['debug' => null, 'owner' => null];
$debug = $config['debug'] ?? false; // false: ?? treats null like missing
$config['level'] ??= 'info'; // assign only if null or missing
$length = $config['owner']?->name; // null, no error: nullsafe chain
$mode = $config['debug'] ? 'on' : 'off'; // plain ternary, same as everywhere
var_dump($debug, $config['level'], $length, $mode);
?? est la coalescence de null, et il avale aussi « clĂ© indĂ©finie » et « variable indĂ©finie », ce qui en fait la façon idiomatique de lire une entrĂ©e optionnelle dâun tableau. ??= nâaffecte que si le cĂŽtĂ© gauche est null ou absent, et ?-> (PHP 8.0) arrĂȘte une chaĂźne dâappels au premier null et renvoie null. Le ternaire abrĂ©gĂ© ?: existe aussi ($a ?: $b renvoie $a sâil est vrai au sens large) ; il convient aux boolĂ©ens et trompe avec tout le reste.
Le flux de contrĂŽle, dâun seul coup
<?php
declare(strict_types=1);
$scores = ['ada' => 92, 'linus' => 71, 'grace' => 85];
foreach ($scores as $who => $score) {
if ($score >= 90) {
$grade = 'A';
} elseif ($score >= 80) {
$grade = 'B';
} else {
$grade = 'C';
}
echo "{$who}: {$grade}", PHP_EOL;
}
for ($i = 0; $i < 3; $i++) {
echo $i;
}
echo PHP_EOL;
$attempts = 0;
while ($attempts < 3) {
$attempts++;
}
$label = match (true) {
$attempts === 0 => 'never tried',
$attempts < 3 => 'a few tries',
default => 'gave it everything',
};
echo $label, PHP_EOL;
Rien ici ne demande dâexplication, Ă part deux dĂ©tails. foreach est la boucle des tableaux et de tout itĂ©rable, et elle vous donne la clĂ© et la valeur ensemble avec as $key => $value. match est une expression qui compare avec ===, ne tombe pas dans la branche suivante et lĂšve une exception si rien ne correspond, alors que switch, que vous trouverez dans le code ancien, compare avec == et enchaĂźne les branches. ĂnumĂ©rations et match montre Ă quoi sert match.
Tout le reste sur un écran
<?php
declare(strict_types=1);
namespace App\Billing; // one per file, mirrors the folder
use App\Money; // import a class from another namespace
use function App\format_cents; // functions and constants can be imported too
const TAX_RATE = 0.2; // compile-time constant, namespaced
define('LEGACY', true); // runtime constant, global, older style
// one-line comment
# also a one-line comment, rarer
/* block
comment */
/**
* Docblock: read by editors, PHPStan and Psalm, not by PHP itself.
*/
function total(int ...$cents): int
{
return array_sum($cents);
}
$parts = [100, 250];
echo total(...$parts), PHP_EOL; // 350: ... unpacks on both sides
... déplie un tableau en arguments et, dans une liste de paramÚtres, rassemble le reste dans un tableau. Les arguments nommés (total(cents: 5)) existent depuis 8.0. null est une valeur, en minuscules par convention, comme true et false. Les espaces de noms et use font exactement ce que vous attendez des packages Java ou des imports ES, et Namespaces, Composer et autoloading explique comment les fichiers sont trouvés.
CĂŽte Ă cĂŽte
| Python | JavaScript | Java | PHP | |
|---|---|---|---|---|
| Concaténation | a + b | a + b | a + b | $a . $b |
| ĂgalitĂ© stricte | a == b | a === b | a.equals(b) | $a === $b |
| Coalescence de null | a or b | a ?? b | (aucun) | $a ?? $b |
| AccÚs sûr au membre | (aucun) | a?.b | (aucun) | $a?->b |
| Lambda | lambda x: x * 2 | x => x * 2 | x -> x * 2 | fn($x) => $x * 2 |
| Dictionnaire littéral | {'k': 1} | {k: 1} | Map.of("k", 1) | ['k' => 1] |
| Interpolation | f"{x}" | `${x}` | (aucune) | "{$x}" |
| Membre dâinstance | obj.name | obj.name | obj.name | $obj->name |
| Membre statique | Cls.name | Cls.name | Cls.name | Cls::$name |
| Ternaire | a if c else b | c ? a : b | c ? a : b | $c ? $a : $b |
Le piĂšge
Deux choses font trĂ©bucher un nouveau venu dans la premiĂšre heure. La premiĂšre est ==, une comparaison qui a lâair juste et passe les tests Ă©vidents, puis dĂ©cide que '1e1' == '10' et que null == false. Tapez === jusquâĂ ce que ce soit un rĂ©flexe, et laissez strict_types attraper le reste.
La seconde est la bibliothĂšque standard, oĂč lâon trouve strpos($haystack, $needle) mais array_search($needle, $haystack), str_replace avec un tiret bas mais strlen sans, et array_key_exists Ă cĂŽtĂ© de in_array. Le nommage nâa jamais Ă©tĂ© conçu, il sâest accumulĂ© au fil des versions. Les fonctions rĂ©centes sont cohĂ©rentes (str_contains, array_is_list, array_find), les anciennes ne changeront pas, et le remĂšde nâest pas la mĂ©moire mais un Ă©diteur avec complĂ©tion et un analyseur statique qui signale lâordre dâarguments inversĂ©.
Une fois la syntaxe rĂ©glĂ©e, les questions intĂ©ressantes commencent, Ă commencer par ce que garantit vraiment int. Le systĂšme de types y rĂ©pond avec plus de nuances que vous ne lâauriez souhaitĂ©.
Le systĂšme de types
PHP est typĂ© dynamiquement, et chaque type que vous Ă©crivez est vĂ©rifiĂ© Ă lâexĂ©cution, pas Ă la compilation comme en Java, et pas effacĂ© comme en TypeScript. Un paramĂštre dĂ©clarĂ© int reçoit un int, sinon lâappel lĂšve une TypeError, Ă chaque fois, y compris en production. Tout le modĂšle tient dans cette phrase, et la nuance se cache dans ce que « reçoit un int » veut dire, parce que cette nuance dĂ©pend dâun interrupteur.
Les types que vous pouvez écrire
<?php
declare(strict_types=1);
function describe(int|float $n, ?string $label, bool $verbose = false): string
{
return ($label ?? 'value') . ': ' . $n . ($verbose ? ' (verbose)' : '');
}
echo describe(3, null), PHP_EOL; // value: 3
echo describe(2.5, 'pi-ish', true), PHP_EOL; // pi-ish: 2.5 (verbose)
Les scalaires sont int, float, string et bool. Les types composĂ©s sont array, object, callable, iterable, et tout nom de classe ou dâinterface. Par-dessus, PHP a une poignĂ©e de types qui nâont de sens quâĂ une position : void et never comme types de retour (never signifie que la fonction lĂšve une exception ou quitte le programme), static comme type de retour dâune mĂ©thode fluide, self pour la classe courante, mixed quand vous acceptez vraiment nâimporte quoi, et null, true, false comme types autonomes depuis PHP 8.2.
Ces types se combinent : ?string est string|null, int|string est une union (PHP 8.0), Countable&Traversable est une intersection (PHP 8.1) qui exige que lâobjet implĂ©mente les deux, et (Countable&Traversable)|null mĂ©lange les deux formes (PHP 8.2). La grammaire sâarrĂȘte lĂ , sans gĂ©nĂ©riques, sans tuples et sans typage structurel.
Les types se posent sur les paramÚtres, les valeurs de retour, les propriétés, et depuis PHP 8.3 sur les constantes de classe :
<?php
declare(strict_types=1);
final class Temperature
{
public const string UNIT = 'C';
public function __construct(
public readonly float $degrees,
) {
}
}
$t = new Temperature(21.5);
var_dump($t->degrees); // float(21.5)
Les paramÚtres et propriétés non typés existent toujours et signifient mixed, mais le code moderne ne les laisse pas sans type.
Ce que int accepte vraiment
Câest ici que PHP diffĂšre de tout le reste. Sans strict_types, PHP convertit les arguments scalaires vers le type dĂ©clarĂ© quand il le peut. Passez la chaĂźne "12" Ă un paramĂštre int et la fonction reçoit lâentier 12 ; passez "12abc" et vous obtenez une TypeError ; passez 1.5 et vous obtenez 1 avec un avertissement de dĂ©prĂ©ciation.
| Vous passez | paramĂštre int, mode coercitif | paramĂštre int, mode strict |
|---|---|---|
12 | 12 | 12 |
"12" | 12 | TypeError |
"12abc" | TypeError | TypeError |
12.0 | 12 | TypeError |
1.5 | 1, déprécié depuis 8.1 | TypeError |
true | 1 | TypeError |
null | TypeError (?int lâaccepte) | TypeError (?int lâaccepte) |
La coercition ne concerne que les scalaires : un tableau nâest jamais transformĂ© en chaĂźne, ni un objet en entier. Une seule conversion reste permise mĂȘme en mode strict, celle dâun int lĂ oĂč un float est attendu, parce quâelle ne perd jamais dâinformation.
Lâinterrupteur est par fichier, et cĂŽtĂ© appelant
<?php
declare(strict_types=1);
function double(int $n): int
{
return $n * 2;
}
echo double(21), PHP_EOL; // 42
echo double('21'), PHP_EOL; // TypeError: must be of type int, string given
Supprimez la ligne declare et le second appel affiche 42. Cet interrupteur a trois propriĂ©tĂ©s, et chacune surprend quelquâun.
Il est par fichier. Il nâexiste ni rĂ©glage global, ni option dans php.ini, ni paramĂštre Ă lâĂ©chelle du projet : chaque fichier annonce son propre mode, et un fichier sans la ligne est en mode coercitif.
Il sâapplique aux appels faits depuis le fichier, pas aux fonctions qui y sont dĂ©finies. Si double() vit dans un fichier strict et quâun fichier coercitif lâappelle, double('21') convertit, parce que câest lâappelant qui dĂ©cide. Ce choix est voulu : lâauteur dâune bibliothĂšque ne peut pas imposer la rigueur au code qui lâappelle, et le code ancien continue de fonctionner quand il appelle une bibliothĂšque moderne.
Il couvre aussi les valeurs de retour. Une fonction en mode strict qui déclare : int et renvoie "42" lÚve une exception.
La rĂšgle pratique est courte : mettez declare(strict_types=1); en tĂȘte de chaque fichier que vous Ă©crivez, laissez votre outil de style de code lâimposer, et nây pensez plus.
strict_typesnâest pas « PHP avec les types activĂ©s », puisque PHP vĂ©rifie toujours les types. Lâinterrupteur dĂ©cide seulement si une chaĂźne qui ressemble Ă un nombre compte comme un nombre.
Convertir volontairement
Quand vous voulez une conversion, dites-le :
<?php
declare(strict_types=1);
var_dump((int) '42'); // int(42)
var_dump((int) '42 apples'); // int(42): a cast takes the leading digits
var_dump((int) 'apples'); // int(0)
var_dump((string) 3.0); // string(1) "3"
var_dump((bool) '0'); // bool(false): "0" is falsy, "0.0" is not
var_dump(intval('0x1A', 16)); // int(26)
Les transtypages ne lĂšvent jamais dâexception et nâĂ©mettent jamais dâavertissement : ils font de leur mieux avec ce quâils reçoivent. Cela en fait le bon outil pour une entrĂ©e utilisateur dĂ©jĂ validĂ©e, et le mauvais pour tout ce qui ne lâest pas. Pour vĂ©rifier avant de convertir, is_int(), is_string(), is_numeric() et leurs cousines renvoient des boolĂ©ens, et filter_var($x, FILTER_VALIDATE_INT) renvoie lâentier ou false.
Pour voir ce que vous tenez, var_dump() affiche le type et la valeur. get_debug_type() (PHP 8.0) renvoie le nom que vous Ă©cririez dans une dĂ©claration (int, string, App\User), lĂ oĂč lâancien gettype() renvoie integer et object.
Null et la bibliothĂšque standard
Vos propres fonctions refusent null pour un paramĂštre non nullable, dans les deux modes. Les fonctions natives sont plus tolĂ©rantes, et cette tolĂ©rance est en voie de disparition. strlen(null) renvoie 0 aujourdâhui avec un avertissement de dĂ©prĂ©ciation (depuis PHP 8.1), et la prochaine version majeure devrait lever une exception. Un code qui lit strlen($_GET['q']) sur un paramĂštre absent vit donc en sursis, alors que strlen($_GET['q'] ?? '') continuera de fonctionner.
La mĂȘme tolĂ©rance se voit dans lâautre sens. LĂ oĂč une fonction renvoyait false en cas dâĂ©chec, les plus rĂ©centes lĂšvent une ValueError (PHP 8.0), et celles qui renvoient encore false sont documentĂ©es ainsi. Quand la documentation dit string|false, testez false avec === et jamais avec if (!$result), parce que "0" et "" sont faux au sens large, eux aussi.
Les nombres, briĂšvement
Un int fait 64 bits sur toutes les plateformes que vous rencontrerez, et le dépassement ne boucle pas : un entier qui déborde devient un flottant, en silence.
<?php
declare(strict_types=1);
var_dump(PHP_INT_MAX + 1); // float(9.223372036854776E+18)
var_dump(0.1 + 0.2 === 0.3); // bool(false), as in every IEEE 754 language
var_dump(intdiv(7, 2), 7 / 2); // int(3), float(3.5)
La division produit toujours un flottant, sauf si les deux opĂ©randes sont des entiers et que le rĂ©sultat est exact. intdiv() donne la division entiĂšre, % est le modulo entier et fmod() celui des flottants. Pour la monnaie ou tout ce qui est dĂ©cimal, lâextension bcmath et sa classe BcMath\Number (PHP 8.4) font de lâarithmĂ©tique Ă prĂ©cision arbitraire.
OĂč sont passĂ©s les gĂ©nĂ©riques
Le langage nâen a pas. array est le type de tout tableau, quel que soit son contenu, et une classe Collection ne peut pas dire ce quâelle collectionne. LâĂ©cosystĂšme a rĂ©pondu avec des docblocks lus par les analyseurs statiques, et sur un projet maintenu cette rĂ©ponse a autant de poids quâun compilateur :
<?php
declare(strict_types=1);
/**
* @template T
* @param list<T> $items
* @param callable(T): bool $keep
* @return list<T>
*/
function keep(array $items, callable $keep): array
{
return array_values(array_filter($items, $keep));
}
/** @var list<int> $evens */
$evens = keep([1, 2, 3, 4], fn (int $n) => $n % 2 === 0);
PHP lit array et callable, tandis que PHPStan et Psalm lisent list<T> et callable(T): bool, en dĂ©duisent que $evens contient des entiers, et font Ă©chouer le build si vous passez des chaĂźnes. Avec list<int>, array<string, User>, non-empty-string ou int<1, max>, le vocabulaire est plus riche que le langage, et les deux outils sâaccordent sur lâessentiel. Tests, analyse statique et outillage montre comment en installer un.
Le piĂšge
La premiĂšre erreur est dâĂ©crire function f(int $n) dans un fichier sans strict_types, de passer "12" dans un test, de voir que ça marche, et dâen conclure que PHP ne vĂ©rifie rien, alors quâil a vĂ©rifiĂ© et converti dans la foulĂ©e. La seconde est lâinverse : ajouter declare(strict_types=1) Ă un fichier et sâattendre Ă ce que toute lâapplication devienne stricte, alors que seuls les appels de ce fichier ont changĂ©.
Les deux ont le mĂȘme remĂšde. La ligne va dans chaque fichier, une rĂšgle de style de code lâimpose, et un analyseur statique attrape au build les cas que le moteur attraperait en production.
Les tableaux ont Ă©tĂ© mentionnĂ©s trois fois dans ce chapitre sans un mot sur ce quâils sont, parce quâils ne sont pas ce que votre langage appelle un tableau. Les tableaux remet les choses en place.
Les tableaux
Un tableau PHP est une table de hachage ordonnĂ©e, et câest la seule collection native. Il joue Ă la fois le rĂŽle de la liste et du dictionnaire de Python, du tableau et de lâobjet de JavaScript, de lâArrayList et de la LinkedHashMap de Java. Les clĂ©s sont des entiers ou des chaĂźnes, les valeurs sont nâimporte quoi, et lâordre dâinsertion est toujours conservĂ©.
<?php
declare(strict_types=1);
$list = ['apple', 'pear']; // keys 0, 1
$map = ['name' => 'Ada', 'born' => 1815]; // string keys
$mixed = [5 => 'five', 'six', 'x' => 'ex']; // keys 5, 6, 'x'
$list[] = 'plum'; // append, key 2
$map['died'] = 1852; // insert, at the end
var_dump(array_is_list($list)); // true
var_dump(array_is_list($map)); // false
Il nây a pas de type liste Ă part. Une « liste » est un tableau dont les clĂ©s se trouvent ĂȘtre 0, 1, 2 et ainsi de suite, dans cet ordre, et array_is_list() (PHP 8.1) vous dit si câest le cas. La distinction compte quand le tableau quitte PHP : json_encode() produit [...] pour une liste et {...} pour tout le reste.
Les clés sont normalisées
Une clĂ© est soit un int, soit une string, et PHP convertit tout le reste Ă lâentrĂ©e. Une chaĂźne numĂ©rique devient lâentier quâelle nomme, un flottant perd ses dĂ©cimales, un boolĂ©en devient 0 ou 1, et null devient la chaĂźne vide :
<?php
declare(strict_types=1);
$a = [];
$a['1'] = 'a'; // key 1, not '1'
$a[1.7] = 'b'; // key 1, overwrites (and a deprecation notice since 8.1)
$a[true] = 'c'; // key 1, overwrites again
$a[null] = 'd'; // key ''
var_dump($a); // [1 => 'c', '' => 'd']
Ces trois Ă©critures visent la mĂȘme clĂ©. La rĂšgle rend service quand une base de donnĂ©es renvoie des identifiants sous forme de chaĂźnes, et elle devient un piĂšge quand vous comptiez sur '1' et 1 pour former deux entrĂ©es distinctes, ce qui nâarrive jamais.
Lire une clé absente
Lire une clĂ© absente Ă©met un avertissement et renvoie null. Deux fonctions vous disent si une clĂ© existe, et elles ne sont pas dâaccord sur null :
<?php
declare(strict_types=1);
$user = ['name' => 'Ada', 'email' => null];
var_dump(isset($user['email'])); // false: the value is null
var_dump(array_key_exists('email', $user)); // true: the key is there
var_dump(isset($user['phone'])); // false, no warning
$phone = $user['phone'] ?? 'unknown'; // no warning, default applied
isset() rĂ©pond « y a-t-il ici une valeur non nulle ? », array_key_exists() rĂ©pond « la clĂ© est-elle prĂ©sente ? ». Dans presque tous les cas, câest ?? que vous voulez : il lit la clĂ© si elle existe et nâest pas null, et se rabat sur la valeur par dĂ©faut sinon, en silence.
Les tableaux sont des valeurs
Si vous ne retenez quâune chose de ce chapitre, retenez celle-ci. Affecter un tableau, le passer Ă une fonction ou le renvoyer produit Ă chaque fois une copie, et lâoriginal ne voit jamais ce qui arrive Ă cette copie.
<?php
declare(strict_types=1);
function addItem(array $cart, string $item): array
{
$cart[] = $item;
return $cart;
}
$cart = ['book'];
$bigger = addItem($cart, 'pen');
var_dump(count($cart)); // 1
var_dump(count($bigger)); // 2
En JavaScript, en Python ou en Java, cart contiendrait maintenant deux éléments, parce que ces langages font circuler une référence vers une structure partagée. En PHP, la fonction a reçu son propre tableau, et pour vous donner le résultat elle doit le renvoyer.
Le coĂ»t est plus faible quâil nây paraĂźt, parce que PHP partage la mĂ©moire sous le capot et ne duplique les donnĂ©es quâĂ la premiĂšre Ă©criture, un mĂ©canisme appelĂ© copie Ă lâĂ©criture. Passer un tableau de dix mille Ă©lĂ©ments Ă une fonction qui se contente de le lire ne coĂ»te donc rien.
Vous pouvez y renoncer avec une référence, &, sur le paramÚtre :
function addItemInPlace(array &$cart, string $item): void
{
$cart[] = $item;
}
RĂ©servez cette Ă©criture Ă la rare boucle critique oĂč la copie se mesure. Une fonction qui renvoie un nouveau tableau se lit, se teste et se type plus facilement, et si la famille sort() modifie sur place par rĂ©fĂ©rence, câest une exception hĂ©ritĂ©e de lâhistoire du langage plutĂŽt quâun modĂšle Ă suivre.
Les objets se comportent Ă lâinverse : une variable objet est un identifiant, et les copies de cet identifiant dĂ©signent le mĂȘme objet, comme dans tous les langages que vous connaissez. Quand il vous faut une sĂ©mantique de rĂ©fĂ©rence pour une collection, enveloppez-la dans une classe, ce que le chapitre Les classes dĂ©taille.
foreach, par valeur et par référence
foreach itĂšre sur une copie, donc modifier $item dans la boucle ne change rien :
<?php
declare(strict_types=1);
$prices = [10, 20, 30];
foreach ($prices as $price) {
$price *= 2; // local copy, the array is untouched
}
foreach ($prices as &$price) {
$price *= 2; // writes through
}
unset($price); // break the reference
var_dump($prices); // [20, 40, 60]
Le unset() qui suit la boucle par rĂ©fĂ©rence a une vraie fonction : sans lui, $price pointe toujours vers le dernier Ă©lĂ©ment, et un $price = 0; innocent plus loin dans le fichier Ă©crase $prices[2]. La plupart des dĂ©veloppeurs PHP se sont fait prendre une fois. Un foreach avec $key => $value et une Ă©criture dans $prices[$key] Ă©vite la question, et array_map() lâĂ©vite tout autant.
Démonter et remonter des tableaux
La déstructuration fonctionne sur les listes et sur les dictionnaires :
<?php
declare(strict_types=1);
[$x, $y] = [3, 4];
['id' => $id, 'name' => $name] = ['id' => 7, 'name' => 'Ada'];
[, $second] = ['skip', 'keep']; // holes are allowed
$defaults = ['color' => 'blue', 'size' => 'M'];
$order = [...$defaults, 'size' => 'L']; // string keys spread since 8.1
var_dump($order); // ['color' => 'blue', 'size' => 'L']
Le dépliage avec des clés chaßnes se comporte comme {...defaults, size: 'L'} en JavaScript : les derniÚres entrées gagnent. Avec des clés entiÚres, le dépliage renumérote, donc [...[1, 2], ...[3]] vaut [1, 2, 3], et non un dictionnaire avec des clés en double.
Le trio fonctionnel et ses cousins
array_map(), array_filter() et array_reduce() font ce que leur nom dit, avec chacun sa subtilité :
<?php
declare(strict_types=1);
$orders = [
['id' => 1, 'total' => 40, 'paid' => true],
['id' => 2, 'total' => 15, 'paid' => false],
['id' => 3, 'total' => 90, 'paid' => true],
];
$totals = array_map(fn(array $o) => $o['total'], $orders); // [40, 15, 90]
$paid = array_filter($orders, fn(array $o) => $o['paid']); // keys 0 and 2
$sum = array_reduce($totals, fn(int $carry, int $t) => $carry + $t, 0); // 145
echo json_encode($paid); // {"0":{...},"2":{...}} an object!
echo json_encode(array_values($paid)); // [{...},{...}] a list
array_filter() conserve les clĂ©s dâorigine. AprĂšs le filtrage dâune liste, les clĂ©s ont des trous, array_is_list() rĂ©pond false et json_encode() produit un objet ; array_values() renumĂ©rote et remet tout en ordre. Notez aussi lâordre des arguments, qui place le tableau en premier pour array_filter() et array_reduce() mais la fonction de rappel en premier pour array_map(). Cette incohĂ©rence a trente ans, et la complĂ©tion de votre Ă©diteur reste le meilleur remĂšde.
PHP 8.4 a ajouté les recherches que vous réécriviez à la main : array_find() renvoie le premier élément qui correspond, array_find_key() sa clé, array_any() et array_all() renvoient des booléens. PHP 8.5 a ajouté array_first() et array_last(), qui renvoient la premiÚre et la derniÚre valeur quelles que soient les clés, à cÎté des plus anciennes array_key_first() et array_key_last().
// PHP 8.4
$firstBig = array_find($orders, fn(array $o) => $o['total'] > 50);
$allPaid = array_all($orders, fn(array $o) => $o['paid']); // false
Le tri modifie sur place et, depuis PHP 8.0, est stable. usort() avec lâopĂ©rateur vaisseau spatial est lâidiome :
usort($orders, fn(array $a, array $b) => $b['total'] <=> $a['total']);
sort() et usort() renumĂ©rotent les clĂ©s ; asort() et uasort() les conservent ; ksort() trie par clĂ©. array_column($orders, 'total', 'id') extrait un champ dâune liste de lignes et, avec le troisiĂšme argument, indexe le rĂ©sultat par un autre. array_combine(), array_flip(), array_unique(), array_slice() et array_splice() sont lĂ aussi, avec count() pour la longueur.
Vous croiserez aussi compact() et extract(), qui transforment des variables locales en tableau et inversement. Il suffit de savoir les reconnaĂźtre, parce quâelles mettent en Ă©chec lâanalyse statique comme votre Ă©diteur et quâaucun code rĂ©cent ne les Ă©crit.
ItĂ©rer sur nâimporte quoi
foreach ne se limite pas aux tableaux. Tout ce qui est iterable fonctionne : tableaux, gĂ©nĂ©rateurs, et objets implĂ©mentant Iterator ou IteratorAggregate. Une fonction qui accepte iterable peut recevoir un gĂ©nĂ©rateur dâun million de lignes sans charger le million de lignes, ce que Fonctions et closures reprend.
<?php
declare(strict_types=1);
function total(iterable $amounts): int
{
$sum = 0;
foreach ($amounts as $amount) {
$sum += $amount;
}
return $sum;
}
echo total([1, 2, 3]); // 6
Quand un tableau ne suffit pas
Un tableau ne peut pas dire ce quâil contient : array $orders nâapprend rien au lecteur, et le langage nâa pas dâarray<Order>. La rĂ©ponse lĂ©gĂšre est un docblock, @param list<Order> $orders, que PHPStan et Psalm imposent comme un vrai type et que votre Ă©diteur utilise pour la complĂ©tion. La rĂ©ponse plus lourde est une petite classe, une final class Orders qui dĂ©tient un tableau privĂ©, expose exactement les opĂ©rations dont vous avez besoin, et implĂ©mente les interfaces qui lui permettent de se comporter comme un tableau lĂ oĂč câest utile : Countable pour count(), ArrayAccess pour $orders[0] et IteratorAggregate pour foreach.
Pour les cas quâun tableau ne couvre pas, la bibliothĂšque standard fournit SplObjectStorage, qui associe des objets Ă des donnĂ©es en utilisant lâobjet lui-mĂȘme comme clĂ©, et WeakMap (PHP 8.0), qui fait de mĂȘme sans garder lâobjet en vie, ce qui permet aux caches indexĂ©s par entitĂ© de ne pas fuir.
Les deux habitudes Ă perdre ici sont dâattendre dâune fonction quâelle modifie le tableau que vous lui passez, et dâoublier quâ
array_filter()laisse des trous. Renvoyez le nouveau tableau, et passez pararray_values()avant dâencoder.
Les tableaux sont ce que la plupart du code PHP fait circuler, et les fonctions sont ce à quoi il les confie. Comme les closures de PHP capturent leur contexte autrement que celles que vous connaissez, elles méritent leur propre chapitre, Fonctions et closures.
Fonctions et closures
Les fonctions PHP ressemblent Ă celles de TypeScript avec un $ devant chaque paramĂštre, et les closures capturent par valeur, pas par variable. La premiĂšre moitiĂ© de cette phrase se lit en vitesse, et câest dans la seconde que se cachent les surprises de ce chapitre.
<?php
declare(strict_types=1);
function greet(string $name, string $greeting = 'Hello', bool $shout = false): string
{
$text = "$greeting, $name!";
return $shout ? strtoupper($text) : $text;
}
echo greet('Ada'); // Hello, Ada!
echo greet('Ada', shout: true); // HELLO, ADA!
echo greet(greeting: 'Hi', name: 'Ada'); // Hi, Ada!
ParamĂštres et valeurs de retour portent des types, vĂ©rifiĂ©s Ă lâexĂ©cution comme lâexplique Le systĂšme de types. Les valeurs par dĂ©faut fonctionnent comme partout. Les arguments nommĂ©s (PHP 8.0) permettent de sauter les valeurs par dĂ©faut qui ne vous intĂ©ressent pas et rendent lisible un appel Ă quatre boolĂ©ens. Positionnels et nommĂ©s se mĂ©langent, les positionnels dâabord.
Variadiques, références et types de retour particuliers
... sur le dernier paramÚtre rassemble le reste dans un tableau ; ... dans un appel déplie un tableau en arguments, clés de chaßne comprises, ce qui transforme un tableau en arguments nommés :
<?php
declare(strict_types=1);
function sum(int ...$numbers): int
{
return array_sum($numbers);
}
echo sum(1, 2, 3); // 6
echo sum(...[4, 5]); // 9
$options = ['greeting' => 'Hey', 'name' => 'Ada'];
// greet(...$options) would call greet(name: 'Ada', greeting: 'Hey')
Un paramĂštre dĂ©clarĂ© &$x reçoit une rĂ©fĂ©rence, et la fonction Ă©crit alors directement dans la variable de lâappelant. La bibliothĂšque standard sâen sert pour sort(), pour le $matches de preg_match() et pour quelques autres. Dans votre propre code, prĂ©fĂ©rez renvoyer la valeur, parce quâune fonction qui modifie ses arguments oblige le lecteur Ă lâouvrir pour comprendre ce quâelle fait.
Les types de paramĂštres acceptent tout ce que le systĂšme de types propose : ?string $label = null pour une valeur optionnelle, int|string $id pour une union, Countable&Traversable $items pour une intersection. Ăcrivez le ? explicitement, car un simple string $x = null fonctionne encore mais est dĂ©prĂ©ciĂ© depuis PHP 8.4. Pour une fonction qui accepte une fonction, vous avez le choix entre callable, qui accepte les closures ainsi que les formes chaĂźne et tableau dĂ©crites plus bas, et Closure, qui nâaccepte que de vrais objets closure. Le code rĂ©cent tend Ă dĂ©clarer Closure et Ă laisser lâappelant convertir avec (...), parce quâune Closure se vĂ©rifie par le typage alors quâune chaĂźne Ă©chappe Ă toute vĂ©rification.
Les fonctions qui ne reviennent pas normalement disposent de leurs propres types de retour : void signifie que rien ne revient, et never (PHP 8.1) signifie que la fonction lÚve toujours une exception ou termine le script, de sorte que les analyseurs statiques savent que le code placé aprÚs un appel à fail() est inaccessible.
Les fonctions ne sont pas des valeurs, mais on peut en obtenir une référence
Un nom de fonction nâest pas une expression, et $f = strlen; est une erreur de syntaxe. Le contournement historique passait par une chaĂźne : $f = 'strlen'; fonctionne parce que le type callable accepte un nom de fonction, une paire [$object, 'method'] ou une chaĂźne 'Class::method'. Cette forme marche encore, mais rien ne la vĂ©rifie avant lâexĂ©cution.
La façon moderne est la syntaxe de callable de premiÚre classe (PHP 8.1) : le nom suivi de (...).
<?php
declare(strict_types=1);
final class Mailer
{
public function send(string $to): string
{
return "sent to $to";
}
}
$length = strlen(...); // Closure wrapping strlen()
$send = (new Mailer())->send(...); // Closure bound to that instance
echo $length('hello'); // 5
echo $send('ada@example.org');
var_dump(array_map(strtoupper(...), ['a', 'b'])); // ['A', 'B']
Le rĂ©sultat est un objet Closure, la seule valeur de type fonction en PHP. Il est sĂ»r pour le typage comme pour le refactoring, puisque votre Ă©diteur suit le renommage de la mĂ©thode, et câest la forme Ă passer Ă array_map() et consorts. Quand vous tenez une chaĂźne plutĂŽt quâun nom, Closure::fromCallable('strlen') produit le mĂȘme objet.
Le rĂ©flexe du dĂ©veloppeur venu dâailleurs est dâĂ©crire
$this->sendsans parenthĂšses, ce qui en PHP lit la propriĂ©tĂ©send, qui nâexiste pas. La mĂ©thode en tant que valeur sâĂ©crit$this->send(...).
Les closures capturent par valeur
Les fonctions anonymes existent, et vous devez dire ce quâelles capturent :
<?php
declare(strict_types=1);
$rate = 0.2;
$withTax = function (float $price) use ($rate): float {
return $price * (1 + $rate);
};
$rate = 0.5; // too late, the closure already copied 0.2
echo $withTax(100.0); // 120
La clause use copie les variables au moment oĂč la closure est créée. Rien de la portĂ©e englobante nâest visible sans ĂȘtre listĂ©, et les changements ultĂ©rieurs de la variable extĂ©rieure nâatteignent pas la closure. LĂ oĂč JavaScript et Python se referment sur la variable elle-mĂȘme et afficheraient 150 ici, PHP donne Ă la closure un instantanĂ© pris Ă sa crĂ©ation.
Pour capturer la variable plutĂŽt que sa valeur, ajoutez & dans la clause, use (&$rate), et la closure partage alors la variable avec la portĂ©e extĂ©rieure, dans les deux sens. On en a besoin pour un accumulateur, ou pour une closure rĂ©cursive qui doit se voir elle-mĂȘme :
$fact = function (int $n) use (&$fact): int {
return $n <= 1 ? 1 : $n * $fact($n - 1);
};
Les fonctions flĂ©chĂ©es (PHP 7.4) suppriment la cĂ©rĂ©monie. fn capture automatiquement toute la portĂ©e englobante, par valeur, et ne contient quâune seule expression :
$withTax = fn(float $price): float => $price * (1 + $rate);
Il nây a plus ni use, ni return, ni accolades, mais la sĂ©mantique dâinstantanĂ© reste la mĂȘme. La plupart des callbacks que vous Ă©crirez seront des fonctions flĂ©chĂ©es, et vous reviendrez Ă function () use () quand il vous faudra plusieurs instructions ou une capture par rĂ©fĂ©rence.
Dans une classe, une closure conserve $this automatiquement, comme vous vous y attendez. Marquez-la static fn ou static function quand elle nâa pas besoin de lâinstance, ce qui Ă©vite de maintenir lâobjet en vie depuis un callback de longue durĂ©e. Closure::bind() et $closure->call($object) rattachent $this Ă un autre objet ; câest ainsi que les frameworks atteignent un Ă©tat privĂ© depuis lâextĂ©rieur, et vous les Ă©crirez rarement vous-mĂȘme.
Générateurs
Une fonction qui contient yield renvoie un Generator sans exĂ©cuter son corps, et chaque tour de foreach la fait avancer jusquâau yield suivant. Si vous connaissez les gĂ©nĂ©rateurs de Python, vous les retrouvez ici presque ligne pour ligne :
<?php
declare(strict_types=1);
/** @return Generator<int, string> */
function lines(string $path): Generator
{
$handle = fopen($path, 'r');
try {
while (($line = fgets($handle)) !== false) {
yield rtrim($line, "\n");
}
} finally {
fclose($handle);
}
}
foreach (lines('/etc/hosts') as $number => $line) {
echo "$number: $line", PHP_EOL;
}
Le fichier est lu une ligne Ă la fois, quelle que soit sa taille, et fermĂ© quand la boucle se termine ou sâinterrompt. Un gĂ©nĂ©rateur est iterable, donc toute fonction qui accepte iterable le prend sans le savoir. yield $key => $value fixe des clĂ©s explicites, yield from dĂ©lĂšgue Ă un autre gĂ©nĂ©rateur ou Ă un tableau, et un return dans un gĂ©nĂ©rateur dĂ©finit une valeur lisible via getReturn() une fois lâitĂ©ration terminĂ©e. Un gĂ©nĂ©rateur ne sâexĂ©cute quâune fois, et pour itĂ©rer Ă nouveau il faut rappeler la fonction.
Pipelines
PHP 8.5 ajoute lâopĂ©rateur pipe. $x |> f(...) appelle f($x), et les chaĂźnes se lisent de haut en bas au lieu de lâintĂ©rieur vers lâextĂ©rieur :
// PHP 8.5
$slug = ' Hello World '
|> trim(...)
|> strtolower(...)
|> (fn(string $s) => str_replace(' ', '-', $s));
echo $slug; // hello-world
Chaque Ă©tape est nâimporte quel callable Ă un argument, ce qui correspond exactement Ă ce que produisent la syntaxe de callable de premiĂšre classe et les fonctions flĂ©chĂ©es. Avant 8.5, le mĂȘme code tenait en trois appels imbriquĂ©s ou en trois variables temporaires, deux formes qui fonctionnent toujours et restent courantes.
PHP 8.5 apporte aussi #[\NoDiscard], un attribut pour les fonctions dont la valeur de retour ne doit pas ĂȘtre ignorĂ©e. Si vous appelez une telle fonction comme simple instruction, PHP Ă©met un avertissement, et vous transtypez lâappel en (void) pour dire que câest voulu. Les bibliothĂšques lâutilisent sur les mĂ©thodes qui renvoient un nouvel objet immuable, pour Ă©viter le bug classique du $date->modify() dont on jette le rĂ©sultat.
Les formes anciennes que vous reconnaĂźtrez
func_get_args() et func_num_args() lisent les arguments dâune fonction dĂ©clarĂ©e sans paramĂštres ; elles prĂ©cĂšdent ...$args et survivent dans le vieux code. call_user_func() et call_user_func_array() invoquent un callable, ce que $callable(...$args) fait aujourdâhui. create_function() fabriquait des closures Ă partir de chaĂźnes et a Ă©tĂ© supprimĂ©e en 8.0. Quand vous croisez ces formes, le remplacement moderne se trouve Ă une ligne de lĂ , et Rector peut faire la modification pour vous.
Les closures photographient leurs variables
use, les fonctions flĂ©chĂ©es photographient toute la portĂ©e, et une mĂ©thode devient une valeur avec(...). Avec ces trois points en tĂȘte, les callbacks PHP nâont plus de surprise en rĂ©serve.
Les fonctions portent le comportement, mais les donnĂ©es sur lesquelles elles agissent sont surtout des objets, et le modĂšle objet de PHP a plus changĂ© ces cinq derniĂšres annĂ©es que pendant les quinze prĂ©cĂ©dentes. Le chapitre Les classes montre Ă quoi il ressemble aujourdâhui.
Les classes
Une classe PHP moderne est courte, typĂ©e et presque entiĂšrement immuable. Le constructeur dĂ©clare les propriĂ©tĂ©s, les types sont vĂ©rifiĂ©s Ă lâexĂ©cution, et lâessentiel du code rĂ©pĂ©titif que vous connaissez de Java, ou de PHP 5, a disparu. Lâexemple ci-dessous montre une classe complĂšte :
<?php
declare(strict_types=1);
namespace App\Billing;
final class Invoice
{
private array $lines = [];
public function __construct(
public readonly string $number,
public readonly \DateTimeImmutable $issuedAt,
) {
}
public function addLine(string $label, int $cents): static
{
$this->lines[] = ['label' => $label, 'cents' => $cents];
return $this;
}
public function total(): int
{
return array_sum(array_column($this->lines, 'cents'));
}
}
$invoice = new Invoice('2026-0042', new \DateTimeImmutable('2026-09-14'));
$invoice->addLine('Hosting', 1200)->addLine('Support', 800);
echo $invoice->number, ': ', $invoice->total(), PHP_EOL; // 2026-0042: 2000
En la lisant de haut en bas, vous rencontrez dâabord namespace, qui place la classe dans App\Billing, de sorte que son nom complet est App\Billing\Invoice (le chapitre Namespaces, Composer et autoloading explique comment ce nom correspond Ă un fichier). final interdit lâhĂ©ritage, et le code PHP moderne rend les classes final par dĂ©faut pour ne les ouvrir quâĂ dessein. Le constructeur nâa pas de corps, parce quâĂ©crire public readonly string $number dans la liste des paramĂštres dĂ©clare la propriĂ©tĂ©, la type et lâaffecte en une seule ligne. Cette promotion de propriĂ©tĂ©s dans le constructeur (PHP 8.0) a retirĂ© lâessentiel de la cĂ©rĂ©monie des classes PHP. readonly (PHP 8.1) rend la propriĂ©tĂ© affectable une seule fois, dans le constructeur, et plus jamais ensuite. Enfin, static comme type de retour signifie « la classe de lâobjet sur lequel la mĂ©thode a Ă©tĂ© appelĂ©e », ce qui est exactement ce quâune mĂ©thode fluide veut promettre.
LâaccĂšs aux membres passe par deux symboles : -> atteint un membre dâinstance, :: atteint un membre statique ou une constante. $this est lâobjet courant, et vous lâĂ©crivez toujours puisquâil nây a pas de this implicite.
Les objets circulent par identifiant
Les tableaux se copient quand vous les affectez, comme le chapitre Les tableaux lâexplique, mais les objets suivent la rĂšgle inverse. Affecter ou passer un objet copie un identifiant qui dĂ©signe le mĂȘme objet, comme le font Java, Python et JavaScript :
<?php
declare(strict_types=1);
final class Cart
{
public array $items = [];
}
function addApple(Cart $cart): void
{
$cart->items[] = 'apple';
}
$cart = new Cart();
addApple($cart);
echo count($cart->items), PHP_EOL; // 1, the caller sees the change
Ce comportement ne doit rien au & des paramĂštres par rĂ©fĂ©rence, qui sont un mĂ©canisme diffĂ©rent, rĂ©servĂ© aux variables, et dont vous nâavez presque jamais besoin avec des objets.
clone $cart fait une copie superficielle, câest-Ă -dire un nouvel objet dont les propriĂ©tĂ©s contiennent les mĂȘmes valeurs, de sorte quâun objet rangĂ© Ă lâintĂ©rieur est partagĂ© entre les deux copies. DĂ©finissez __clone() si la copie a besoin de ses propres objets internes.
Pour comparer deux objets, == compare lâĂ©tat propriĂ©tĂ© par propriĂ©tĂ©, tandis que === demande si les deux cĂŽtĂ©s sont le mĂȘme objet, ce qui explique que $a === clone $a vaille false.
LâimmuabilitĂ© sans accesseurs
La classe PHP 5 classique avait une propriété privée, un getter, et parfois un setter. Ce motif a été remplacé par des fonctionnalités récentes que vous verrez dans toutes les bases de code écrites aprÚs 2024.
La premiĂšre est readonly, que vous avez dĂ©jĂ rencontrĂ©. Quand toutes les propriĂ©tĂ©s dâune classe sont readonly, marquez plutĂŽt la classe elle-mĂȘme (PHP 8.2) :
<?php
declare(strict_types=1);
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {
}
public function add(Money $other): self
{
return new self($this->amount + $other->amount, $this->currency);
}
}
Chaque propriété est publique et personne ne peut la modifier, ce qui vous donne un objet valeur sans le moindre getter.
La visibilitĂ© asymĂ©trique (PHP 8.4) laisse lâextĂ©rieur lire une propriĂ©tĂ© que seule la classe peut Ă©crire :
// PHP 8.4
final class Counter
{
public private(set) int $count = 0;
public function increment(): void
{
$this->count++;
}
}
$c = new Counter();
$c->increment();
echo $c->count; // 1
$c->count = 5; // Error: Cannot modify private(set) property Counter::$count
Le getter nâa plus de raison dâĂȘtre. LĂ oĂč il vous faut de la logique Ă la lecture ou Ă lâĂ©criture, les hooks de propriĂ©tĂ© (PHP 8.4) lâattachent Ă la propriĂ©tĂ© elle-mĂȘme, comme une propriĂ©tĂ© C# ou le @property de Python :
// PHP 8.4
final class User
{
public string $email {
set(string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Invalid email: $value");
}
$this->email = strtolower($value);
}
}
public string $domain {
get => substr($this->email, strpos($this->email, '@') + 1);
}
}
$u = new User();
$u->email = 'Ada@Example.org';
echo $u->domain; // example.org
Pour lâappelant, ce sont des propriĂ©tĂ©s ordinaires, alors quâĂ lâintĂ©rieur set valide et normalise pendant que get calcule. Une propriĂ©tĂ© avec un seul hook get et sans stockage est simplement une propriĂ©tĂ© calculĂ©e.
Interfaces, classes abstraites, traits
Les interfaces et les classes abstraites fonctionnent comme en Java et en C# : une interface liste des signatures de mĂ©thodes et des constantes, une classe abstraite peut porter de lâimplĂ©mentation, une classe implĂ©mente plusieurs interfaces et Ă©tend un seul parent. #[\Override] (PHP 8.3) sur une mĂ©thode fait vĂ©rifier Ă PHP que le parent la possĂšde vraiment, ce qui attrape les fautes de frappe dans les redĂ©finitions.
Les traits, eux, nâont pas dâĂ©quivalent exact dans la plupart des langages. Un trait est un bloc de mĂ©thodes et de propriĂ©tĂ©s que le compilateur copie dans chaque classe qui lâutilise avec use. Les mixins de Ruby en sont le repĂšre le plus proche, alors que les traits de Rust, malgrĂ© le nom, dĂ©crivent tout autre chose.
<?php
declare(strict_types=1);
trait HasTimestamps
{
private ?\DateTimeImmutable $createdAt = null;
public function touch(): void
{
$this->createdAt ??= new \DateTimeImmutable();
}
}
final class Article
{
use HasTimestamps;
}
$a = new Article();
$a->touch();
Les traits sont pratiques pour les utilitaires transversaux et faciles Ă surutiliser, parce quâune classe qui en utilise cinq vous impose cinq fichiers Ă lire pour savoir ce quâelle fait. PrĂ©fĂ©rez la composition quand vous le pouvez.
La diffĂ©rence entre static et self tient en quelques lignes : self nomme la classe oĂč le code est Ă©crit, tandis que static nomme la classe de lâobjet Ă lâexĂ©cution. Dans une fabrique statique dĂ©finie dans une classe parente, new static() construit donc la sous-classe rĂ©ellement appelĂ©e, alors que new self() construit toujours le parent.
Construction
PHP nâa quâun constructeur par classe et ne connaĂźt pas la surcharge de mĂ©thodes. Le manque est comblĂ© par les arguments nommĂ©s (PHP 8.0) pour les paramĂštres optionnels, et par les fabriques statiques pour les autres façons de construire :
<?php
declare(strict_types=1);
final readonly class Period
{
private function __construct(
public \DateTimeImmutable $start,
public \DateTimeImmutable $end,
) {
}
public static function fromStrings(string $start, string $end): self
{
return new self(new \DateTimeImmutable($start), new \DateTimeImmutable($end));
}
public static function year(int $year): self
{
return self::fromStrings("$year-01-01", "$year-12-31");
}
}
$fy = Period::year(2026);
echo $fy->end->format('Y-m-d'), PHP_EOL; // 2026-12-31
Un constructeur privĂ© plus des fabriques publiques se lit aussi bien quâun jeu de constructeurs surchargĂ©s, et chaque variante porte un nom.
Depuis PHP 8.4, vous pouvez chaĂźner un appel sur un objet fraĂźchement créé sans lâentourer de parenthĂšses : new Period(...)->start exigeait auparavant (new Period(...))->start.
Pour les objets immuables, « modifier » signifie « fabriquer une copie modifiĂ©e ». PHP 8.5 lui donne sa propre syntaxe : clone($money, ['amount' => 500]) renvoie une copie avec les propriĂ©tĂ©s listĂ©es remplacĂ©es. Les rĂšgles de visibilitĂ© habituelles sâappliquent, et une propriĂ©tĂ© readonly ne sâĂ©crit que depuis lâintĂ©rieur de sa classe, donc lâappel vit dans une mĂ©thode withAmount() plutĂŽt que chez lâappelant. Avant 8.5, cette mĂȘme mĂ©thode clonait puis affectait dans __clone(), ce que PHP 8.3 a autorisĂ© pour les propriĂ©tĂ©s readonly.
Chaßnes, magie et métadonnées
Tout objet peut sâafficher lui-mĂȘme en implĂ©mentant __toString(). Le dĂ©clarer fait automatiquement implĂ©menter Stringable (PHP 8.0) Ă la classe, de sorte que vous pouvez typer un paramĂštre string|Stringable et accepter les deux.
Les autres mĂ©thodes Ă double tiret bas sont des crochets que le moteur appelle : __get et __set sâexĂ©cutent quand le code touche une propriĂ©tĂ© qui nâexiste pas, __call quand il appelle une mĂ©thode qui nâexiste pas. Les frameworks et les ORM sâen servent pour construire des API fluides et des modĂšles paresseux, et il suffit de savoir les reconnaĂźtre, sans les utiliser dans le code applicatif. Dans le mĂȘme esprit, crĂ©er une propriĂ©tĂ© jamais dĂ©clarĂ©e ($obj->foo = 1 sans $foo dans la classe) est dĂ©prĂ©ciĂ© depuis PHP 8.2 et doit devenir une erreur dans la prochaine version majeure, ce qui est une raison de plus de dĂ©clarer toutes vos propriĂ©tĂ©s.
Les attributs (PHP 8.0) sont des métadonnées structurées sur une classe, une méthode, une propriété ou un paramÚtre, lues par réflexion. Les annotations Java et les attributs C# sont le repÚre direct :
<?php
declare(strict_types=1);
#[\Attribute(\Attribute::TARGET_METHOD)]
final readonly class Route
{
public function __construct(public string $path) {}
}
final class HomeController
{
#[Route('/')]
public function index(): string
{
return 'Hello';
}
}
$method = new \ReflectionMethod(HomeController::class, 'index');
foreach ($method->getAttributes(Route::class) as $attribute) {
echo $attribute->newInstance()->path, PHP_EOL; // /
}
Un attribut est lui-mĂȘme une classe marquĂ©e #[\Attribute], et rien ne se passe Ă lâexĂ©cution tant que personne nâappelle getAttributes(), la mĂ©tadonnĂ©e restant inerte jusquâĂ sa lecture. Le routage, la validation, la sĂ©rialisation et les frameworks de test reposent tous sur ce mĂ©canisme.
Invoice::class donne le nom pleinement qualifiĂ© sous forme de chaĂźne, ce que vous faites circuler au lieu dâĂ©crire 'App\Billing\Invoice' en dur. $x instanceof Invoice vĂ©rifie le type Ă lâexĂ©cution et vaut false, sans erreur, quand $x nâest pas un objet. Les objets paresseux (PHP 8.4) permettent dâinstancier une classe sans exĂ©cuter son constructeur tant quâune propriĂ©tĂ© nâest pas touchĂ©e, un outil pour les conteneurs dâinjection de dĂ©pendances et les ORM plutĂŽt que pour le code de tous les jours.
Le langage nâa pas de gĂ©nĂ©riques, et une classe Collection contient du mixed aux yeux du moteur ; les docblocks @template dĂ©crits dans Le systĂšme de types donnent Ă PHPStan et Psalm ce qui manque au moteur.
Le piĂšge
Une fonction qui reçoit un objet et « lâajuste juste un peu » ajuste aussi lâobjet de lâappelant. Si vous vouliez une modification locale, faites dâabord un clone, ou concevez la classe en readonly et renvoyez une nouvelle instance.
Gardez aussi en tĂȘte que readonly est superficiel : il gĂšle la case de la propriĂ©tĂ©, pas ce vers quoi elle pointe. Un readonly array $items ne peut pas ĂȘtre rĂ©affectĂ©, mais un readonly Cart $cart laisse quiconque tient $cart y pousser des articles. LâimmuabilitĂ© de tout le graphe reste une dĂ©cision de conception, que le mot-clĂ© ne prend pas Ă votre place.
Une fois les classes en main, reste Ă voir comment PHP reprĂ©sente un ensemble fermĂ© de choix, ce qui est le sujet du chapitre ĂnumĂ©rations et match.
ĂnumĂ©rations et match
Une Ă©numĂ©ration PHP est une classe dotĂ©e dâun ensemble fixe dâinstances, et match est lâexpression qui choisit une branche par instance. Ensemble, ils remplacent le motif constantes-de-classe-plus-switch dont le vieux code PHP est rempli. Si vous connaissez les enums de Java, vous savez dĂ©jĂ presque tout ; si vous venez de lâEnum de Python ou des unions de littĂ©raux de chaĂźne de TypeScript, la forme vous sera familiĂšre et les vĂ©rifications plus strictes.
<?php
declare(strict_types=1);
enum Status: string
{
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
public function label(): string
{
return match ($this) {
self::Draft => 'Draft',
self::Published => 'Live',
self::Archived => 'Archived',
};
}
public function canEdit(): bool
{
return $this !== self::Archived;
}
}
$status = Status::from('published'); // Status::Published
echo $status->label(), PHP_EOL; // Live
echo $status->value, PHP_EOL; // published
var_dump($status->canEdit()); // bool(true)
var_dump(Status::tryFrom('deleted')); // NULL
Cas, valeurs et méthodes
Une Ă©numĂ©ration (PHP 8.1) dĂ©clare ses case et rien dâautre ne peut en ĂȘtre un. Status::Draft est un objet, le seul de son espĂšce, donc deux rĂ©fĂ©rences au mĂȘme cas sont le mĂȘme objet, et === est la comparaison Ă utiliser. Les Ă©numĂ©rations ne sâinstancient pas avec new, ne sâĂ©tendent pas, et ne portent aucun Ă©tat, ni propriĂ©tĂ©s ni donnĂ©es par instance. En revanche, elles peuvent avoir des mĂ©thodes, des constantes, des mĂ©thodes statiques et des interfaces Ă implĂ©menter.
Le : string aprĂšs le nom en fait une Ă©numĂ©ration adossĂ©e, dont chaque cas porte un scalaire (string ou int) accessible via ->value. from() retransforme un scalaire en cas et lĂšve une ValueError quand rien ne correspond, tandis que tryFrom() renvoie null Ă la place. Câest par ce duo quâune Ă©numĂ©ration franchit une frontiĂšre, quâil sâagisse dâune colonne en base, dâun champ JSON ou dâun paramĂštre dâURL. Une Ă©numĂ©ration pure, dĂ©clarĂ©e sans type adossĂ©, a des cas mais pas de ->value, et convient quand le choix ne quitte jamais votre code.
cases() renvoie tous les cas dans lâordre de dĂ©claration, ce que veulent une liste dĂ©roulante ou une rĂšgle de validation :
$allowed = array_map(fn (Status $s) => $s->value, Status::cases());
// ['draft', 'published', 'archived']
Chaque cas a aussi un ->name ('Draft'), utile pour les journaux.
Comme $this dans une mĂ©thode dâĂ©numĂ©ration est un cas, le comportement qui dĂ©pend du cas appartient Ă lâĂ©numĂ©ration plutĂŽt quâĂ des chaĂźnes de if Ă©parpillĂ©es dans le code. label() et canEdit() ci-dessus illustrent ce motif, oĂč lâĂ©numĂ©ration sait ce que chacun de ses cas signifie.
Depuis PHP 8.2, les cas dâĂ©numĂ©ration sont autorisĂ©s dans les expressions constantes, donc comme valeurs par dĂ©faut de paramĂštres, constantes de classe et arguments dâattributs : public function __construct(private Status $status = Status::Draft).
match
match (PHP 8.0) ressemble à switch et se comporte comme une expression en Rust ou un when en Kotlin, avec un comportement qui se résume à ce qui suit.
Il renvoie une valeur. $label = match ($status) { ... }; et return match (...) sont les usages normaux. Il nây a pas de break, parce quâil nây a pas de passage Ă la branche suivante, et exactement une branche sâexĂ©cute.
Il compare avec ===. match ('1') { 1 => 'int', '1' => 'string' } choisit la seconde branche, lĂ oĂč switch aurait choisi la premiĂšre.
Il doit ĂȘtre exhaustif. Si aucune branche ne correspond et quâil nây a pas de default, PHP lĂšve UnhandledMatchError. Si vous ajoutez un cas Ă une Ă©numĂ©ration et oubliez de mettre Ă jour un match qui la lit, la premiĂšre fois que ce cas atteint le match vous obtenez une exception qui nomme la ligne exacte, plutĂŽt quâun null silencieux.
Une branche peut lister plusieurs valeurs, séparées par des virgules :
<?php
declare(strict_types=1);
enum Status: string
{
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
}
function isVisible(Status $status): bool
{
return match ($status) {
Status::Published => true,
Status::Draft, Status::Archived => false,
};
}
Les analyseurs statiques comprennent cette rĂšgle, et PHPStan comme Psalm signalent un match sur une Ă©numĂ©ration qui laisse un cas non traitĂ©, avant mĂȘme quâil sâexĂ©cute.
Le sujet dâun match nâest pas forcĂ©ment une Ă©numĂ©ration. Toute valeur convient, et lâidiome match (true) en fait une Ă©chelle de conditions qui produit une valeur :
$size = match (true) {
$bytes < 1024 => 'small',
$bytes < 1024 * 1024 => 'medium',
default => 'large',
};
Chaque branche est comparĂ©e avec === Ă true, donc chaque branche est une expression boolĂ©enne, ce qui se lit mieux quâun ternaire imbriquĂ© et ne peut pas passer Ă la branche suivante.
switch existe toujours, avec sa comparaison lĂąche, son passage Ă la branche suivante et ses break, et vous le croiserez dans du code plus ancien. Dans du code neuf, match est le bon choix par dĂ©faut, et switch sert au cas rare oĂč vous voulez vraiment que plusieurs Ă©tiquettes partagent un bloc dâinstructions.
Null et throw comme expressions
Deux opĂ©rateurs se marient naturellement avec match et les Ă©numĂ©rations. LâopĂ©rateur nullsafe ?-> (PHP 8.0) court-circuite une chaĂźne quand le cĂŽtĂ© gauche vaut null, de sorte que $order?->customer?->email vaut null si un maillon est null, au lieu de provoquer une erreur. CombinĂ© Ă ??, il donne une valeur par dĂ©faut en une ligne : $email = $order?->customer?->email ?? 'nobody@example.org';.
throw est une expression (PHP 8.0), donc il peut se placer Ă droite dâun ??, dans un ternaire ou dans une branche de match :
$status = Status::tryFrom($input) ?? throw new \InvalidArgumentException("Unknown status: $input");
$handler = match ($status) {
Status::Draft => $this->saveDraft(...),
Status::Published => $this->publish(...),
Status::Archived => throw new \LogicException('Archived items are read-only'),
};
Le second exemple montre aussi le motif du dispatch, un match qui renvoie un callable, suivi de $handler($item).
Persistance et comportement au mĂȘme endroit
Dans une application, les trois idĂ©es se retrouvent dans une mĂȘme dĂ©claration : une Ă©numĂ©ration adossĂ©e pour le stockage, des mĂ©thodes pour le comportement, et match lĂ oĂč vivent les branches.
<?php
declare(strict_types=1);
interface HasColor
{
public function color(): string;
}
enum Priority: int implements HasColor
{
case Low = 1;
case Normal = 2;
case High = 3;
public const DEFAULT = self::Normal;
public static function fromLabel(string $label): self
{
return match (strtolower($label)) {
'low' => self::Low,
'normal', 'medium' => self::Normal,
'high', 'urgent' => self::High,
default => throw new \ValueError("Unknown priority: $label"),
};
}
public function color(): string
{
return match ($this) {
self::Low => 'grey',
self::Normal => 'blue',
self::High => 'red',
};
}
public function escalate(): self
{
return match ($this) {
self::Low => self::Normal,
self::Normal, self::High => self::High,
};
}
}
$p = Priority::fromLabel('medium');
echo $p->color(), PHP_EOL; // blue
echo $p->escalate()->name, PHP_EOL; // High
echo Priority::DEFAULT->value, PHP_EOL; // 2
Lâentier part en base et lâĂ©numĂ©ration circule partout ailleurs. Il nây a pas de constante PRIORITY_HIGH = 3 Ă garder synchronisĂ©e avec une table de couleurs quelque part, parce que le cas et son comportement vivent dans un seul fichier.
Face au motif ancien, constantes de classe plus drapeaux de chaĂźne plus switch, une Ă©numĂ©ration vous donne un vrai type Ă mettre dans une signature (function assign(Priority $p)), vĂ©rifiĂ© par le moteur, et un ensemble fermĂ© sur lequel lâanalyseur peut raisonner.
Le piĂšge
Deux habitudes venues dâautres langages, et du vieux PHP, provoquent le mĂȘme bug.
La premiĂšre consiste Ă reprendre switch, qui compare de façon lĂąche : switch (Status::Draft) avec case 'draft': ne correspond jamais, puisquâune Ă©numĂ©ration nâest pas Ă©gale Ă sa valeur quelle que soit la comparaison, et avec un sujet de type chaĂźne un case 0: correspond Ă plus de choses que vous ne lâimaginez.
La seconde consiste Ă comparer une Ă©numĂ©ration Ă sa valeur adossĂ©e, alors que $status == 'published' vaut toujours false, lâĂ©numĂ©ration Ă©tant un objet et la chaĂźne ce quâelle stocke. Comparez des cas Ă des cas ($status === Status::Published), ou convertissez dâabord ($status->value === 'published').
La question suivante, pour un dĂ©veloppeur venu dâailleurs, est ce qui se passe quand quelque chose tourne mal, et le chapitre Erreurs et exceptions dĂ©crit un modĂšle qui mĂȘle deux mĂ©canismes.
Erreurs et exceptions
PHP a des exceptions, et elles fonctionnent comme celles de Python ou de Java. Il a aussi un mĂ©canisme plus ancien, les erreurs moteur, antĂ©rieur aux exceptions et toujours prĂ©sent. La pratique moderne consiste Ă tout faire passer par les exceptions, et ce chapitre montre dâabord ce modĂšle, puis la poignĂ©e de lignes de configuration qui ramĂšnent lâancien mĂ©canisme dans le mĂȘme circuit.
Deux hiérarchies, une racine
Tout ce qui peut ĂȘtre levĂ© avec throw et rattrapĂ© avec catch implĂ©mente Throwable, et cette interface se divise en deux familles.
Error est ce que le moteur lĂšve quand votre code est faux : TypeError pour un mauvais argument, ValueError pour un bon type avec une valeur impossible, ArgumentCountError, DivisionByZeroError, UnhandledMatchError quand un match ne trouve aucune branche. Vous ne les levez pas vous-mĂȘme, et vous les rattrapez rarement, parce quâelles signalent un bug plutĂŽt quâune situation.
Exception est la famille de vos propres erreurs. PHP en livre un petit jeu dans la SPL, dont les noms tiennent lieu de documentation : InvalidArgumentException, RuntimeException, LogicException, DomainException, OutOfRangeException, UnexpectedValueException. Ătendez lâune dâelles plutĂŽt quâException directement, et vos appelants disposent dâune famille parlante Ă rattraper.
La syntaxe ne réserve aucune surprise :
<?php
declare(strict_types=1);
function parsePort(string $raw): int
{
if (!ctype_digit($raw)) {
throw new InvalidArgumentException("Not a port: $raw");
}
return (int) $raw;
}
try {
$port = parsePort('80a');
} catch (InvalidArgumentException|ValueError $e) {
echo 'Bad input: ', $e->getMessage(), PHP_EOL;
} finally {
echo 'Done.', PHP_EOL;
}
| rattrape plusieurs types dans un mĂȘme bloc (PHP 7.1). finally sâexĂ©cute quâune exception ait Ă©tĂ© levĂ©e ou non. Rattrapez Throwable quand vous voulez vraiment tout, par exemple en tĂȘte de la boucle dâun worker.
Exceptions vĂ©rifiĂ©es et valeurs dâerreur
Une fonction PHP signale un Ă©chec en levant une exception ou en renvoyant null. Rien nâoblige lâappelant Ă gĂ©rer lâun ou lâautre, et rien dans la signature ne liste ce qui peut ĂȘtre levĂ©. Si vous venez de Java, il nây a pas de clause throws ni de vĂ©rification Ă la compilation ; un docblock @throws est une courtoisie lue par votre IDE et par PHPStan ou Psalm, pas par le moteur. Si vous venez de Go ou de Rust, il nây a ni valeur de retour dâerreur ni type Result dans le langage. Quelques bibliothĂšques en proposent un, mais le PHP idiomatique ne sâen sert pas.
Le type de retour nullable plus ?? est lâidiome du « peut-ĂȘtre » :
<?php
declare(strict_types=1);
function findUser(int $id): ?array
{
return $id === 1 ? ['name' => 'Ada'] : null;
}
$name = findUser(2)['name'] ?? 'anonymous';
echo $name, PHP_EOL; // anonymous
La rĂšgle courante est de renvoyer null quand lâabsence est normale et de lever une exception quand elle ne lâest pas. Comme throw est une expression (PHP 8.0), les deux se combinent sur une ligne :
$user = findUser($id) ?? throw new RuntimeException("No user $id");
Les exceptions personnalisées transportent des données
Une exception personnalisée est une classe, donc elle peut porter un contexte typé et offrir un constructeur nommé qui compose le message pour vous :
<?php
declare(strict_types=1);
final class InsufficientFunds extends DomainException
{
public function __construct(
public readonly int $requested,
public readonly int $available,
?Throwable $previous = null,
) {
parent::__construct(
"Requested $requested, only $available available",
previous: $previous,
);
}
public static function forWithdrawal(int $requested, int $available): self
{
return new self($requested, $available);
}
}
try {
throw InsufficientFunds::forWithdrawal(100, 40);
} catch (InsufficientFunds $e) {
echo $e->available, PHP_EOL; // 40
}
Lâargument previous sert Ă chaĂźner. Rattrapez une exception de bas niveau, enveloppez-la dans une exception qui a un sens pour votre appelant, et passez lâoriginale en previous. getPrevious() remonte la chaĂźne, et tous les loggers lâaffichent, donc rien ne se perd.
try {
$pdo->query($sql);
} catch (PDOException $e) {
throw new RuntimeException('Order lookup failed', previous: $e);
}
Lâautre mĂ©canisme
Avant lâexistence des exceptions, PHP signalait les problĂšmes en Ă©mettant une erreur dâun niveau donnĂ© (notice, warning, fatal) et, pour tout ce qui nâĂ©tait pas fatal, continuait. Lâessentiel de cette machinerie a Ă©tĂ© repliĂ© dans des exceptions Error au fil des ans : une division par zĂ©ro lĂšve, un argument du mauvais type lĂšve, un appel de mĂ©thode sur null lĂšve. Quelques situations Ă©mettent encore un warning et continuent : lire une variable non dĂ©finie, une clĂ© de tableau absente, une propriĂ©tĂ© non dĂ©finie, et toute notice de dĂ©prĂ©ciation.
<?php
declare(strict_types=1);
$config = [];
echo $config['debug']; // Warning: Undefined array key "debug"
echo 'still running', PHP_EOL;
Ce « still running » est prĂ©cisĂ©ment ce quâun dĂ©veloppeur venu dâun autre langage nâattend pas. Le remĂšde est un gestionnaire dâerreurs, installĂ© Ă lâamorçage, qui transforme chaque erreur moteur en exception, et câest ce que font tous les frameworks :
<?php
declare(strict_types=1);
error_reporting(E_ALL);
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
throw new ErrorException($message, 0, $severity, $file, $line);
});
$config = [];
echo $config['debug']; // ErrorException: Undefined array key "debug"
ErrorException est une exception intĂ©grĂ©e qui se souvient de la sĂ©vĂ©ritĂ©. Ă partir de lĂ , il nây a plus quâun chemin dâĂ©chec, et try le rattrape en entier.
Quelques rĂ©glages accompagnent ce gestionnaire. error_reporting(E_ALL) garantit que rien nâest filtrĂ©. display_errors vaut On en dĂ©veloppement et Off en production, oĂč les erreurs partent dans le journal : une exception non rattrapĂ©e sur une page publique ne doit jamais afficher une trace. set_exception_handler() reçoit tout ce qui atteint le sommet sans avoir Ă©tĂ© rattrapĂ©, et câest lĂ que vous journalisez et affichez une page dâerreur gĂ©nĂ©rique. PHP 8.5 ajoute get_error_handler() et get_exception_handler() pour quâune bibliothĂšque puisse inspecter ce qui est installĂ© avant de lâenvelopper.
Ce qui ne se rattrape pas
Les erreurs fatales terminent la requĂȘte sans quâaucun catch ne les voie : mĂ©moire Ă©puisĂ©e, max_execution_time dĂ©passĂ©, mĂȘme classe dĂ©clarĂ©e deux fois. Une erreur de syntaxe dans un fichier inclus, en revanche, est une ParseError que vous pouvez rattraper depuis PHP 7. Sâil faut rĂ©agir, register_shutdown_function() sâexĂ©cute aprĂšs lâarrĂȘt du script, et error_get_last() vous dit si lâarrĂȘt Ă©tait propre. Depuis PHP 8.5, une erreur fatale affiche une trace dâappels, et une mĂ©moire Ă©puisĂ©e en production pointe enfin vers une ligne.
LâopĂ©rateur @ et assert()
Vous croiserez @ dans du code ancien : @file_get_contents($url). Il rĂ©duit au silence tout warning Ă©mis par lâexpression. Votre gestionnaire dâerreurs est toujours appelĂ©, mais error_reporting() y renvoie un masque rĂ©duit, ce qui permet au gestionnaire de savoir que @ a Ă©tĂ© utilisĂ©. ConsidĂ©rez cet opĂ©rateur comme un signal dâalerte quand vous le croisez. Le seul usage dĂ©fendable entoure une fonction qui Ă©met un warning et renvoie false en cas dâĂ©chec, immĂ©diatement suivi dâun test sur cette valeur de retour. MĂȘme lĂ , un try autour dâune alternative qui lĂšve une exception se lit mieux.
assert() mĂ©rite aussi dâĂȘtre reconnu. Câest une vĂ©rification de dĂ©veloppement, retirĂ©e en production quand zend.assertions vaut -1 dans php.ini, la valeur recommandĂ©e pour la production. Servez-vous-en pour des invariants qui documentent une intention, jamais pour valider une entrĂ©e.
Le piĂšge
Les deux erreurs classiques sont silencieuses. La premiĂšre consiste Ă rattraper Exception dans un gestionnaire de haut niveau et Ă croire que lâon a tout rattrapĂ©, alors quâune TypeError est une Error et non une Exception, et quâelle passe donc tout droit devant ce bloc. Ă la frontiĂšre de lâapplication, câest Throwable quâil faut rattraper.
La seconde est le catch vide :
try {
$cache->delete($key);
} catch (Throwable) {
}
La variable peut ĂȘtre omise (PHP 8.0), ce qui rend le bloc honnĂȘte sur le fait quâil ignore lâexception, et il y a des cas oĂč ignorer est le bon choix. Mais un catch vide autour de quoi que ce soit dâimportant est la façon la plus sĂ»re de cacher un bug pendant un an, alors journalisez au minimum ce que vous ignorez.
Tout ce qui tourne mal doit vous parvenir sous la forme dâune exception, en un seul endroit. PHP le fait, Ă condition de le lui demander.
Ce gestionnaire, et chaque classe que vous levez, vivent dans des fichiers que PHP doit trouver. Namespaces, Composer et autoloading explique comment il les trouve.
Namespaces, Composer et autoloading
PHP nâa pas de systĂšme de modules, mais trois mĂ©canismes plus modestes, require, les espaces de noms et un crochet dâautoloading, que Composer assemble en un gestionnaire de paquets comparable Ă npm, pip, Maven ou Cargo. Vous nâĂ©crirez plus jamais de require pour lâune de vos propres classes, et il reste utile de savoir ce que Composer fait Ă votre place.
Les trois primitives
require 'file.php'; lit et exĂ©cute un fichier, une fois par appel. Câest ainsi que le code se partageait en 2005, et câest encore ainsi que tout sâamorce aujourdâhui, avec un unique require de vendor/autoload.php en tĂȘte de votre point dâentrĂ©e.
Un espace de noms est un prĂ©fixe sur un nom de classe. namespace App\Billing; en tĂȘte dâun fichier fait de chaque classe qui y est dĂ©clarĂ©e une App\Billing\Something, et nâapporte rien dâautre, aucune hiĂ©rarchie, aucune visibilitĂ©, aucun lien avec un dossier. App\Billing nâest pas « dans » App, ce sont simplement deux chaĂźnes qui partagent un prĂ©fixe.
Le crochet dâautoloading est la piĂšce qui rend les deux premiĂšres utiles. Quand PHP rencontre une classe quâil nâa jamais vue, il appelle une fonction que vous avez enregistrĂ©e, en lui passant le nom de la classe, et cette fonction est censĂ©e faire un require du bon fichier, aprĂšs quoi PHP rĂ©essaie.
<?php
declare(strict_types=1);
spl_autoload_register(function (string $class): void {
$file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
if (is_file($file)) {
require $file;
}
});
$invoice = new App\Billing\Invoice(); // loads src/App/Billing/Invoice.php
Le mĂ©canisme se rĂ©sume Ă cette closure, et Composer en Ă©crit une meilleure version, avec un cache, quâil vous remet.
PSR-4 : du nom au chemin
La rĂšgle que suit la closure ci-dessus porte un nom, PSR-4, et Composer lâapplique Ă partir dâun bloc de composer.json :
{
"name": "acme/shop",
"type": "project",
"require": {
"php": "^8.4",
"ext-intl": "*"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
},
"autoload": {
"psr-4": { "App\\": "src/" }
},
"autoload-dev": {
"psr-4": { "App\\Tests\\": "tests/" }
}
}
App\Billing\Invoice vit dans src/Billing/Invoice.php : une classe par fichier, un nom de fichier Ă©gal au nom de la classe, et des dossiers qui reprennent les segments de lâespace de noms aprĂšs le prĂ©fixe. Lâarborescence qui en rĂ©sulte est la mĂȘme sur tous les projets PHP modernes que vous ouvrirez :
shop/
âââ composer.json
âââ composer.lock
âââ public/
â âââ index.php # require __DIR__ . '/../vendor/autoload.php';
âââ src/
â âââ Billing/
â âââ Invoice.php # namespace App\Billing;
âââ tests/
â âââ Billing/
â âââ InvoiceTest.php
âââ vendor/ # generated, not committed
Ajoutez une classe sous src/, et elle est trouvĂ©e Ă la requĂȘte suivante, sans commande Ă lancer, puisque PSR-4 rĂ©sout par chemin au moment de lâappel. La stratĂ©gie plus ancienne classmap parcourt les dossiers pour en faire un tableau gĂ©nĂ©rĂ©, et rĂ©clame un composer dump-autoload aprĂšs chaque nouveau fichier ; vous la rencontrerez dans les projets anciens.
Composer, dans le vocabulaire que vous connaissez
composer.json joue le rÎle de votre package.json, composer.lock celui du fichier de verrouillage, vendor/ celui de node_modules, généré et ignoré par Git, et Packagist celui du registre public.
composer init # interactive composer.json
composer require monolog/monolog # add and install, updates the lock
composer require --dev phpstan/phpstan # development-only dependency
composer install # reproduce exactly what the lock says
composer update # resolve anew, rewrite the lock
composer update monolog/monolog # ... for one package only
composer show # what is installed, with versions
composer outdated # what has a newer release
composer audit # known vulnerabilities in the lock
composer dump-autoload -o # regenerate the autoloader, optimised
install obĂ©it Ă composer.lock, tandis quâupdate le réécrit. En CI et en production vous lancez install, et vous obtenez exactement les versions que votre collĂšgue a testĂ©es. Committez composer.lock pour une application ; pour une bibliothĂšque, la plupart des auteurs ne le font pas, afin quâelle soit testĂ©e avec ce que ses utilisateurs rĂ©solvent, et câest le mĂȘme dĂ©bat que dans tous les autres Ă©cosystĂšmes.
Les contraintes de version suivent semver, et ^8.4 signifie « 8.4 ou toute 8.x ultĂ©rieure ». LâentrĂ©e php de require est une contrainte elle aussi ; avec config.platform.php vous figez la version contre laquelle Composer rĂ©sout, pour quâun dĂ©veloppeur sous PHP 8.5 ne tire pas un paquet que votre production en 8.4 ne peut pas exĂ©cuter.
Les scripts vivent sous une clĂ© scripts et se lancent avec composer run nom ou comme crochets de cycle de vie (post-install-cmd), Ă la maniĂšre des scripts npm. Les extensions ne sont pas des paquets : ext-intl dans require vĂ©rifie seulement que lâextension est prĂ©sente, et lâinstaller est le travail de votre gestionnaire de paquets systĂšme, de PECL ou de PIE. Les outils que vous installeriez globalement ailleurs (linters, analyseurs) vont dans require-dev, pour que chaque dĂ©veloppeur et la CI utilisent la mĂȘme version ; composer global require existe et il vaut mieux le laisser de cĂŽtĂ©. Un monorepo dĂ©clare ses paquets internes comme dĂ©pĂŽts path, et Composer crĂ©e des liens symboliques.
Vivre avec les espaces de noms
Dans un fichier, une instruction use importe un nom pour que vous puissiez lâĂ©crire court. Les alias rĂ©solvent les collisions, et les fonctions comme les constantes peuvent aussi ĂȘtre importĂ©es :
<?php
declare(strict_types=1);
namespace App\Billing;
use App\Customer\Customer;
use DateTimeImmutable as Date;
use function App\Support\money;
use const App\Support\CURRENCY;
final class Invoice
{
public function __construct(
public readonly Customer $customer,
public readonly Date $issuedOn,
) {
}
public function total(): string
{
return money(1999, CURRENCY);
}
}
echo Invoice::class, PHP_EOL; // App\Billing\Invoice
Invoice::class donne le nom pleinement qualifiĂ© sous forme de chaĂźne, ce qui est ce que vous passez Ă un conteneur, Ă un mock ou aux vĂ©rifications instanceof qui prennent une chaĂźne. Un antislash initial, comme dans \DateTimeImmutable, nomme une classe de lâespace de noms global sans passer par use.
Les fonctions se rabattent sur lâespace de noms global, alors que les classes ne le font pas. Appeler strlen() dans App\Billing cherche dâabord App\Billing\strlen, puis strlen, tandis quâappeler new DateTimeImmutable() sans use ni antislash initial Ă©choue. Vous verrez \strlen() dans certaines bibliothĂšques, parce que lâantislash saute la recherche et laisse OPcache inliner quelques fonctions intĂ©grĂ©es ; câest une micro-optimisation, pas une convention que vous ayez Ă adopter.
Les standards Ă connaĂźtre
Le PHP-FIG est le groupe oĂč les auteurs de frameworks et de bibliothĂšques sâaccordent sur des interfaces, publiĂ©es sous forme de PSR. Une PSR est un contrat plutĂŽt quâune bibliothĂšque : les implĂ©mentations viennent de nombreux Ă©diteurs, et vous pouvez en remplacer une par une autre parce que votre code ne voit que lâinterface. Voici celles que vous croiserez dĂšs la premiĂšre semaine :
- PSR-4, lâautoloading, ci-dessus.
- PER Coding Style, successeur de PSR-12 : placement des accolades, indentation, nommage. PHP-CS-Fixer ou PHP_CodeSniffer le font respecter.
- PSR-3,
LoggerInterface. Toutes les bibliothĂšques journalisent Ă travers elle ; vous branchez le logger de votre choix. - PSR-7, PSR-15 et PSR-17 : objets requĂȘte et rĂ©ponse HTTP, middlewares, et leurs fabriques. Les
$_GETetheader()du langage sont traitĂ©s dans Une requĂȘte web, sans framework ; PSR-7 est le modĂšle objet que les bibliothĂšques partagent par-dessus. - PSR-11,
ContainerInterface, pour quâune bibliothĂšque puisse demander un service Ă nâimporte quel conteneur. - PSR-14, la distribution dâĂ©vĂ©nements, PSR-6 et PSR-16, le cache.
Une bibliothĂšque qui type son constructeur contre Psr\Log\LoggerInterface fonctionne dans tous les frameworks citĂ©s dans ce livre. Cette interopĂ©rabilitĂ© explique pourquoi lâĂ©cosystĂšme compte moins de forks et de réécritures que sa taille ne le laisserait penser.
Le piĂšge
Ne modifiez jamais un fichier de vendor/. Le prochain composer install sur nâimporte quelle machine efface la modification, et personne ne saura pourquoi la production diffĂšre de votre portable. Forkez le paquet, ou surchargez la classe par votre propre entrĂ©e dâautoloading, ou envoyez le correctif en amont.
Le second piĂšge est plus discret : vous ajoutez une classe, PHP dit quâelle nâexiste pas, et vous passez vingt minutes Ă chercher une faute de frappe qui nâexiste pas. Comparez lâespace de noms au dossier : App\Billing\Invoice doit ĂȘtre src/Billing/Invoice.php, lettre pour lettre, casse comprise sous Linux. Si le projet utilise classmap plutĂŽt que psr-4, lancez composer dump-autoload et passez Ă autre chose.
Composer nâest pas une Ă©tape de build, puisquâil nây a rien Ă compiler, Ă empaqueter ni Ă transpiler : aprĂšs
composer install, le code sâexĂ©cute tel quel.
Une fois les classes trouvées et les paquets installés, il reste la bibliothÚque standard sur laquelle vous vous appuierez chaque jour, et Chaßnes, nombres, dates et JSON en fait le tour.
ChaĂźnes, nombres, dates et JSON
PHP est livrĂ© avec une grande bibliothĂšque standard, faite de fonctions bien plus que de mĂ©thodes. Vous nâappelez pas $s.upper() mais strtoupper($s), parce quâil nây a ni classe String, ni classe Number, ni receveur : les valeurs entrent en arguments et les rĂ©sultats sortent en valeurs de retour. Une fois cette convention acceptĂ©e, la bibliothĂšque est vaste, rapide, et documentĂ©e fonction par fonction sur php.net, oĂč chaque page donne la signature, le journal des changements par version, et des exemples qui valent la lecture.
Les noms, en revanche, sont incohĂ©rents : strlen voisine avec str_replace, array_key_exists prend la clĂ© en premier lĂ oĂč in_array prend lâaiguille en premier, strpos et array_search renvoient false en cas dâĂ©chec alors que preg_match renvoie 0. Câest le rĂ©sidu de trente ans de croissance, et il ne disparaĂźtra pas. LâautocomplĂ©tion de votre Ă©diteur et la page php.net sont le remĂšde, bien plus que la mĂ©morisation, et au bout dâune semaine vous ne remarquez plus ces Ă©carts.
Les chaĂźnes sont des octets
Une chaĂźne PHP est une suite dâoctets, et non de points de code ou de graphĂšmes. Il en dĂ©coule que strlen('Ă©') vaut 2, que strrev('hĂ©llo') massacre lâaccent, et que substr peut couper un caractĂšre multi-octets en deux.
<?php
declare(strict_types=1);
$word = 'café';
echo strlen($word), PHP_EOL; // 5
echo mb_strlen($word), PHP_EOL; // 4
echo strtoupper($word), PHP_EOL; // CAFĂ©
echo mb_strtoupper($word), PHP_EOL; // CAFĂ
Pour tout texte quâun humain lira, utilisez la famille mb_ : mb_strlen, mb_substr, mb_strtoupper, mb_str_pad, et depuis PHP 8.4 mb_trim, mb_ucfirst et mb_lcfirst. Elles supposent UTF-8 par dĂ©faut, et les fonctions nues restent utiles pour ce pour quoi elles ont Ă©tĂ© conçues : donnĂ©es binaires, protocoles ASCII, hachages, et tout ce qui se compte vraiment en octets.
Pour tout ce qui dĂ©pend de la langue de lâutilisateur, lâextension intl enveloppe ICU : Normalizer pour ramener formes composĂ©es et dĂ©composĂ©es Ă une forme canonique, Collator pour trier « Ă© » Ă cĂŽtĂ© de « e » selon la langue du lecteur, NumberFormatter pour les devises et les pluriels, IntlDateFormatter pour des dates Ă©crites comme un lecteur français ou japonais les attend.
Les utilitaires du quotidien sont ceux de nâimporte quel langage, avec des noms PHP :
<?php
declare(strict_types=1);
$path = '/var/log/app.log';
var_dump(str_contains($path, 'log')); // true (PHP 8.0)
var_dump(str_starts_with($path, '/var')); // true
var_dump(str_ends_with($path, '.log')); // true
echo implode(', ', ['a', 'b', 'c']), PHP_EOL; // a, b, c
print_r(explode('/', trim($path, '/'))); // ['var', 'log', 'app.log']
echo str_pad('7', 3, '0', STR_PAD_LEFT), PHP_EOL; // 007
echo ucfirst('php'), PHP_EOL; // Php
echo sprintf('%05.2f|%-6s|%03d', 3.14159, 'ok', 7), PHP_EOL; // 03.14|ok |007
echo number_format(1234567.891, 2), PHP_EOL; // 1,234,567.89
sprintf est celui du C, printf affiche au lieu de renvoyer, et number_format est ce que vous utilisez avant de découvrir NumberFormatter.
Deux syntaxes gĂšrent le texte sur plusieurs lignes. Le heredoc interpole, le nowdoc non, et les deux retirent lâindentation du marqueur de fin :
<?php
declare(strict_types=1);
$name = 'Ada';
$greeting = <<<TXT
Hello, {$name}.
Welcome back.
TXT;
$raw = <<<'TXT'
Hello, {$name}. This stays literal.
TXT;
echo $greeting, PHP_EOL, $raw, PHP_EOL;
Expressions réguliÚres
PHP utilise PCRE, le mĂȘme dialecte que Perl et proche de ce quâacceptent le module re de Python et JavaScript. Le motif est une chaĂźne entre dĂ©limiteurs, en gĂ©nĂ©ral / ou ~, suivis de modificateurs. Ajoutez le modificateur u dĂšs que le texte est en UTF-8, sans quoi . correspond Ă un seul octet et \w ignore les lettres accentuĂ©es.
<?php
declare(strict_types=1);
$line = 'Order #4521 shipped to Zoé on 2026-03-14';
if (preg_match('/#(?<id>\d+).*on (?<date>\d{4}-\d{2}-\d{2})/u', $line, $m)) {
echo $m['id'], ' ', $m['date'], PHP_EOL; // 4521 2026-03-14
}
echo preg_replace('/\s+/u', ' ', "too many\n\nspaces"), PHP_EOL;
echo preg_replace_callback(
'/\d+/',
fn (array $m): string => (string) ($m[0] * 2),
'a1 b22 c333',
), PHP_EOL; // a2 b44 c666
preg_match renvoie 1, 0, ou false si le motif est mal formé. Les groupes nommés se retrouvent dans le tableau de résultat sous leur nom. preg_split, preg_quote et preg_match_all complÚtent la famille.
Les nombres
int est un entier signé sur 64 bits, et quand il déborde, PHP bascule silencieusement en float, sans exception ni retour à zéro. Ce comportement convient à un compteur et devient faux pour un identifiant ou un montant.
<?php
declare(strict_types=1);
var_dump(PHP_INT_MAX + 1); // float(9.223372036854776E+18)
var_dump(intdiv(7, 2)); // int(3)
var_dump(7 / 2); // float(3.5), division always yields float unless exact
var_dump(7 % 2); // int(1)
var_dump(2 ** 10); // int(1024)
var_dump(fdiv(1, 0)); // float(INF), where 1 / 0 throws DivisionByZeroError
var_dump(0.1 + 0.2 === 0.3); // false
var_dump(abs(0.1 + 0.2 - 0.3) < PHP_FLOAT_EPSILON); // true
var_dump(round(2.5), round(3.5), round(-2.5)); // 3, 4, -3: half away from zero
Les flottants sont des doubles IEEE 754, avec les rĂ©serves habituelles. round arrondit par dĂ©faut la moitiĂ© en sâĂ©loignant de zĂ©ro et accepte une constante de mode pour lâarrondi bancaire. Lâargent, câest soit des entiers dans la plus petite unitĂ© (les centimes), soit de la prĂ©cision arbitraire : bcadd, bcmul et consorts travaillent sur des chaĂźnes, et PHP 8.4 les enveloppe dans BcMath\Number, un objet immuable qui accepte les opĂ©rateurs.
// PHP 8.4
$price = new BcMath\Number('19.99');
$total = $price * 3;
echo $total, PHP_EOL; // 59.97
Pour lâalĂ©atoire, rand() et mt_rand() sont rapides mais prĂ©visibles. Tout ce qui touche Ă la sĂ©curitĂ© passe par random_int, random_bytes ou la classe Random\Randomizer (PHP 8.2), adossĂ©s au gĂ©nĂ©rateur cryptographique du systĂšme dâexploitation.
<?php
declare(strict_types=1);
echo random_int(1, 6), PHP_EOL; // a fair die
echo bin2hex(random_bytes(16)), PHP_EOL; // 32 hex chars, a fine token
$r = new Random\Randomizer();
echo $r->getInt(1, 100), PHP_EOL;
print_r($r->shuffleArray([1, 2, 3, 4]));
echo $r->getBytesFromString('abcdef0123456789', 8), PHP_EOL; // PHP 8.3
Les dates
Utilisez DateTimeImmutable plutĂŽt que DateTime. Les deux existent et partagent une interface, mais la version mutable est un piĂšge : $date->modify('+1 day') modifie $date sur place et le renvoie, donc chaque rĂ©fĂ©rence Ă cet objet se dĂ©cale avec lui. La version immuable renvoie un nouvel objet et laisse lâoriginal tranquille, comme vous lâattendez de java.time en Java ou de datetime en Python.
<?php
declare(strict_types=1);
date_default_timezone_set('UTC');
$start = new DateTimeImmutable('2026-03-14 09:30', new DateTimeZone('Europe/Paris'));
$end = $start->modify('+2 weeks')->setTime(18, 0);
echo $start->format(DateTimeInterface::ATOM), PHP_EOL; // 2026-03-14T09:30:00+01:00
echo $end->format('D, d M Y H:i'), PHP_EOL; // Sat, 28 Mar 2026 18:00
$diff = $start->diff($end);
echo $diff->days, ' days, ', $diff->h, ' hours', PHP_EOL; // 14 days, 8 hours
$parsed = DateTimeImmutable::createFromFormat('d/m/Y', '01/07/2026');
echo $parsed->getTimestamp(), PHP_EOL;
$inTokyo = $start->setTimezone(new DateTimeZone('Asia/Tokyo'));
echo $inTokyo->format('H:i T'), PHP_EOL; // 17:30 JST
foreach (new DatePeriod($start, new DateInterval('P1D'), 3) as $day) {
echo $day->format('l'), PHP_EOL; // Saturday, Sunday, Monday, Tuesday
}
Lâanalyseur derriĂšre new DateTimeImmutable('...') et modify() accepte des expressions en anglais ('next monday', 'first day of last month') et toutes les formes ISO courantes. DateInterval utilise les durĂ©es ISO 8601 (P1Y2M3DT4H). Le fuseau par dĂ©faut vient de php.ini ; le rĂ©gler sur UTC au dĂ©but du point dâentrĂ©e et convertir aux frontiĂšres est la pratique habituelle. Les codes de format sont propres Ă PHP (Y-m-d H:i:s), pas ceux de strftime, dĂ©prĂ©ciĂ©e en 8.1.
JSON
json_encode et json_decode sont intégrés et rapides. Passez JSON_THROW_ON_ERROR à chaque fois, sans quoi un échec renvoie false ou null et vous le découvrez trois fonctions plus loin.
<?php
declare(strict_types=1);
$payload = ['id' => 4521, 'tags' => ['php', 'json'], 'total' => 59.97, 'note' => null];
$json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
echo $json, PHP_EOL;
$asArray = json_decode($json, true, flags: JSON_THROW_ON_ERROR); // nested arrays
$asObject = json_decode($json, flags: JSON_THROW_ON_ERROR); // stdClass objects
echo $asArray['tags'][0], ' ', $asObject->tags[1], PHP_EOL; // php json
var_dump(json_validate('{"ok": true}')); // true (PHP 8.3), without building the tree
Le deuxiĂšme argument de json_decode choisit entre tableaux associatifs et stdClass. Les tableaux sont ce que la plupart du code attend. Les objets de vos propres classes se sĂ©rialisent via lâinterface JsonSerializable : implĂ©mentez jsonSerialize(): mixed et renvoyez la forme de tableau voulue. Le sens inverse, JSON vers objet typĂ©, nâest pas dans le langage ; bibliothĂšques et frameworks sâen chargent.
Un Ă©cueil mĂ©rite sa propre phrase, et Les tableaux explique pourquoi : un tableau dont les clĂ©s ne sont pas 0, 1, 2... sâencode en objet JSON et non en liste. Comme array_filter laisse des trous et que json_encode les voit, votre API renvoie {"0": ..., "2": ...} lĂ oĂč vous attendiez une liste, Ă moins de passer le tableau dans array_values() avant de lâencoder.
Les grands entiers survivent Ă lâaller-retour tant quâils tiennent sur 64 bits ; au-delĂ , dĂ©codez avec JSON_BIGINT_AS_STRING. Les flottants sâĂ©crivent avec jusquâĂ 17 chiffres significatifs par dĂ©faut (serialize_precision), donc 0.1 reste 0.1.
Fichiers et flux
file_get_contents et file_put_contents lisent ou écrivent un fichier entier en un appel, et acceptent des URL et des enveloppes de flux aussi bien que des chemins. Pour travailler ligne par ligne, il y a le trio à la C fopen/fgets/fclose, ou SplFileObject, qui est itérable :
<?php
declare(strict_types=1);
$path = __DIR__ . '/notes.txt';
file_put_contents($path, "one\ntwo\nthree\n");
foreach (new SplFileObject($path) as $n => $line) {
if ($line !== '') {
echo $n, ': ', rtrim($line), PHP_EOL;
}
}
$stdin = fopen('php://stdin', 'r');
$buffer = fopen('php://memory', 'r+');
fwrite($buffer, 'scratch');
rewind($buffer);
echo fread($buffer, 100), PHP_EOL;
__DIR__ est le dossier du fichier courant ; sans lui, les chemins relatifs se rĂ©solvent par rapport au rĂ©pertoire de travail, qui sous un serveur web est rarement celui que vous croyez. Les enveloppes php:// exposent les flux standard, des tampons mĂ©moire et des fichiers temporaires Ă travers les mĂȘmes fonctions que les vrais fichiers, et file_get_contents('https://...') fonctionne quand allow_url_fopen est activĂ©, mĂȘme si pour du vrai HTTP vous voudrez curl ou un client PSR-18.
Validation, hachage, URI
filter_var valide et assainit des scalaires avec un jeu de filtres intégrés, sans dépendance :
<?php
declare(strict_types=1);
var_dump(filter_var('ada@example.org', FILTER_VALIDATE_EMAIL)); // the string, or false
var_dump(filter_var('42', FILTER_VALIDATE_INT)); // int(42)
var_dump(filter_var('yes', FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE)); // true
$hash = password_hash('correct horse battery staple', PASSWORD_DEFAULT);
var_dump(password_verify('correct horse battery staple', $hash)); // true
echo hash('sha256', 'payload'), PHP_EOL;
password_hash choisit lâalgorithme, gĂ©nĂšre le sel et encode le tout dans une seule chaĂźne, que password_verify relit. Ces deux fonctions couvrent lâensemble de la question des mots de passe, et PASSWORD_DEFAULT migre vers des algorithmes plus solides au fil des versions sans que vous changiez une ligne. hash couvre le reste, de sha256 Ă xxh3, et hash_hmac signe.
Depuis PHP 8.5, les URL passent par un vrai analyseur plutĂŽt que par lâapproximatif parse_url :
// PHP 8.5
$uri = new Uri\Rfc3986\Uri('https://example.org:8443/docs/intro?lang=fr#top');
echo $uri->getHost(), ' ', $uri->getPort(), ' ', $uri->getPath(), PHP_EOL;
// example.org 8443 /docs/intro
Uri\WhatWg\Url, dans la mĂȘme extension, applique les rĂšgles des navigateurs plutĂŽt que celles de la RFC, pour les cas oĂč vous devez tomber dâaccord avec ce que fera un <a href>.
Telle est la partie de la bibliothĂšque standard que vous toucherez au cours dâune semaine ordinaire. La question suivante est ce que PHP vous donne quand lâentrĂ©e nâest plus une chaĂźne dans une variable mais une requĂȘte HTTP, et Une requĂȘte web, sans framework y rĂ©pond sans la moindre bibliothĂšque.
Une requĂȘte web, sans framework
PHP peut servir une page web sans bibliothĂšque, sans code serveur et sans configuration, parce que traiter une requĂȘte HTTP est ce pour quoi le langage a Ă©tĂ© conçu. La requĂȘte est dĂ©jĂ analysĂ©e quand votre script dĂ©marre, et la rĂ©ponse est ce que vous affichez. Un framework ajoute de la structure par-dessus, mais il nâajoute pas la capacitĂ©.
Avoir vu une fois cette couche brute rend ensuite chaque framework lisible, parce quâils reposent tous sur exactement ces primitives.
Le contrĂŽleur frontal
Pointez le serveur de développement de PHP sur un seul fichier, et chaque URL passe par lui :
php -S localhost:8000 public/index.php
Ce fichier est le contrĂŽleur frontal. En production, le serveur web fait la mĂȘme chose avec une rĂšgle de réécriture (ou FrankenPHP et RoadRunner le font pour vous, comme le dĂ©crit Comment PHP sâexĂ©cute). Avec le serveur de dĂ©veloppement, un dĂ©tail compte : si le script renvoie false, le serveur sert le fichier demandĂ© depuis le disque, et câest ainsi que passent les ressources statiques.
<?php
declare(strict_types=1);
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($path !== '/' && is_file(__DIR__ . $path)) {
return false; // let the built-in server send the CSS or image
}
echo 'Every other URL lands here: ', htmlspecialchars($path, ENT_QUOTES);
Lire la requĂȘte
La requĂȘte vit dans les superglobales, des tableaux que PHP remplit avant la premiĂšre ligne de votre code. $_GET contient la chaĂźne de requĂȘte, $_POST les champs dâun formulaire soumis, $_COOKIE les cookies, $_FILES les fichiers envoyĂ©s, et $_SERVER tout le reste : REQUEST_METHOD, REQUEST_URI, et chaque en-tĂȘte HTTP sous la forme HTTP_ suivi de son nom en majuscules, de sorte que Accept-Language devient $_SERVER['HTTP_ACCEPT_LANGUAGE'].
<?php
declare(strict_types=1);
$method = $_SERVER['REQUEST_METHOD'];
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
$name = trim($_POST['name'] ?? '');
$lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
$body = json_decode(file_get_contents('php://input'), true, flags: JSON_THROW_ON_ERROR);
$_POST nâest rempli que pour les corps application/x-www-form-urlencoded et multipart/form-data envoyĂ©s en POST. Un corps JSON, quelle que soit la mĂ©thode, se lit brut depuis php://input. Un formulaire envoyĂ© en PUT ou PATCH nâest pas analysĂ© du tout, sauf si vous le demandez : request_parse_body() (PHP 8.4) renvoie les champs et les fichiers pour ces mĂ©thodes aussi.
Rien dans ces tableaux nâest digne de confiance. HTTP_HOST est ce que le client a envoyĂ©, REQUEST_URI peut contenir nâimporte quoi, et un champ attendu comme chaĂźne arrive sous forme de tableau si le client Ă©crit name[]=x. Traitez chaque valeur comme une entrĂ©e utilisateur non typĂ©e, validez-la avec filter_var ou vos propres vĂ©rifications, et seulement ensuite laissez-la approcher votre logique.
Ăcrire la rĂ©ponse
Tout ce que votre script affiche est le corps de la rĂ©ponse, quâil sâagisse dâun echo, dâun print ou de texte placĂ© hors des balises <?php ?>. Le statut et les en-tĂȘtes se rĂšglent avec deux fonctions, Ă appeler avant le premier octet de sortie, parce que les en-tĂȘtes voyagent en premier :
<?php
declare(strict_types=1);
http_response_code(201);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
setcookie('theme', 'dark', [
'expires' => time() + 86400 * 30,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
echo json_encode(['created' => true], JSON_THROW_ON_ERROR);
Affichez quoi que ce soit avant, mĂȘme un saut de ligne Ă©garĂ© devant <?php, et header() Ă©choue avec « headers already sent ». La mise en tampon de la sortie (ob_start() au dĂ©but, ob_end_flush() Ă la fin) retient le corps en mĂ©moire jusquâĂ la fin du script et rend lâordre indiffĂ©rent ; câest ce que font les frameworks.
Les gabarits sont du PHP
PHP a commencĂ© comme langage de gabarits, et il lâest toujours : un fichier HTML avec des <?= $expr ?> dedans est un gabarit, et un include suffit Ă lâafficher. La seule rĂšgle porte sur la sortie : Ă©chappez chaque valeur avec htmlspecialchars avant quâelle nâatterrisse dans le HTML, sinon le premier utilisateur nommĂ© <script> est propriĂ©taire de votre page.
<?php
declare(strict_types=1);
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$todos = ['Write the chapter', 'Escape <everything>'];
?>
<ul>
<?php foreach ($todos as $todo): ?>
<li><?= e($todo) ?></li>
<?php endforeach; ?>
</ul>
La forme foreach (...): ... endforeach; existe prĂ©cisĂ©ment pour cet entrelacement. Un utilitaire e() de deux lignes est toute lâhistoire de lâĂ©chappement pour le HTML ; les attributs, les URL et les contextes JavaScript demandent chacun leur propre encodage, et câest la partie que les moteurs de gabarits automatisent.
Une application complĂšte
Lâapplication ci-dessous fonctionne telle quelle, en un seul fichier : une liste de notes stockĂ©es dans SQLite, avec un formulaire pour en ajouter une. Elle tourne avec php -S localhost:8000 index.php et rien dâautre.
<?php
declare(strict_types=1);
$db = new PDO('sqlite:' . __DIR__ . '/notes.db', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)');
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$route = $_SERVER['REQUEST_METHOD'] . ' ' . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
match ($route) {
'GET /' => (function () use ($db): void {
$notes = $db->query('SELECT id, body FROM notes ORDER BY id DESC')->fetchAll();
echo '<h1>Notes</h1><form method="post" action="/notes">',
'<input name="body" required> <button>Add</button></form><ul>';
foreach ($notes as $note) {
echo '<li>', e($note['body']), '</li>';
}
echo '</ul>';
})(),
'POST /notes' => (function () use ($db): void {
$body = trim($_POST['body'] ?? '');
if ($body === '') {
http_response_code(422);
echo 'A note needs a body.';
return;
}
$stmt = $db->prepare('INSERT INTO notes (body) VALUES (:body)');
$stmt->execute(['body' => $body]);
http_response_code(303);
header('Location: /');
})(),
default => (function (): void {
http_response_code(404);
echo 'Not found';
})(),
};
Ce fichier illustre lâessentiel de ce quâil y a Ă savoir. Le routeur est un match sur la mĂ©thode et le chemin, ce qui tient jusquâĂ une dizaine de routes avant que vous nâen vouliez un vrai. PDO est lâAPI de base de donnĂ©es, une seule interface pour SQLite, MySQL, PostgreSQL et dâautres, et ERRMODE_EXCEPTION transforme chaque Ă©chec en PDOException levĂ©e plutĂŽt quâen false que vous oubliez de vĂ©rifier. La requĂȘte utilise un marqueur nommĂ© et execute() lie la valeur ; le texte SQL et les donnĂ©es ne se rencontrent jamais sous forme de chaĂźne, donc aucune injection SQL Ă craindre. PHP 8.4 ajoute des sous-classes par pilote (Pdo\Sqlite, Pdo\Mysql, Pdo\Pgsql) via Pdo::connect(), qui exposent les spĂ©cificitĂ©s de chaque pilote avec des types corrects.
Construire le SQL par interpolation, "WHERE id = $id", est la seule habitude des vieux tutoriels PHP que le langage vous laisse encore garder, et câest prĂ©cisĂ©ment celle quâil faut abandonner.
Sessions et mots de passe
Une session est un stockage cĂŽtĂ© serveur indexĂ© par un cookie. Appelez session_start() avant toute sortie, et $_SESSION devient un tableau qui survit dâune requĂȘte Ă lâautre pour ce visiteur. Par dĂ©faut, les donnĂ©es vivent dans des fichiers sur le serveur ; les frameworks y substituent une base de donnĂ©es ou un cache via session_set_save_handler(). Le cookie ne transporte que lâidentifiant de session.
<?php
declare(strict_types=1);
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ok = password_verify($_POST['password'] ?? '', $storedHash ?? '');
if ($ok) {
session_regenerate_id(true);
$_SESSION['user_id'] = 42;
}
}
$csrf = $_SESSION['csrf'] ??= bin2hex(random_bytes(32));
password_hash et password_verify, vus dans ChaĂźnes, nombres, dates et JSON, suffisent pour les mots de passe, Ă condition de rĂ©gĂ©nĂ©rer lâidentifiant de session Ă la connexion. Pour la protection CSRF, mettez un jeton alĂ©atoire dans la session, imprimez-le en champ cachĂ© dans chaque formulaire, et comparez-le Ă la soumission avec hash_equals() ; cela tient en quatre lignes, et chaque framework fait la mĂȘme chose sous un nom plus flatteur.
La couche des standards
Les superglobales et header() fonctionnent, mais câest de lâĂ©tat global, ce qui rend le code difficile Ă tester et impossible Ă composer. Le PHP-FIG a rĂ©pondu par des interfaces :
- PSR-7 définit des objets immuables
RequestInterfaceetResponseInterface, de sorte quâune requĂȘte est une valeur que vous passez et une rĂ©ponse une valeur que vous renvoyez. - PSR-15 dĂ©finit lâintergiciel (middleware) : un gestionnaire prend une requĂȘte et renvoie une rĂ©ponse, et un middleware enveloppe des gestionnaires. Authentification, CORS, journalisation, limitation de dĂ©bit sont chacun une classe.
- PSR-17 dĂ©finit les fabriques qui crĂ©ent ces objets, pour quâune bibliothĂšque ne dĂ©pende jamais dâune implĂ©mentation prĂ©cise.
- PSR-18 dĂ©finit un client HTTP, le cĂŽtĂ© sortant des mĂȘmes objets.
Une bibliothĂšque Ă©crite contre PSR-7 et PSR-15 tourne dans tout framework qui les parle, câest-Ă -dire aujourdâhui la plupart. CakePHP, Laminas, Laravel, Symfony et Yii, et les micro-frameworks Mezzio et Slim, ajoutent chacun routage, injection de dĂ©pendances, gabarits et couche de base de donnĂ©es par-dessus ces primitives ; les primitives en dessous sont celles que vous venez de voir.
En production, le contrĂŽleur frontal reste le mĂȘme, et seul change ce qui lâappelle : PHP-FPM derriĂšre nginx, Apache ou Caddy, ou un runtime persistant comme FrankenPHP ou RoadRunner. Rien dans ce chapitre nâa besoin dâĂȘtre adaptĂ© Ă lâun ou Ă lâautre.
Lâapplication ci-dessus nâa ni tests ni analyse statique, et Tests, analyse statique et outillage y remĂ©die.
Tests, analyse statique et outillage
Un projet PHP maintenu lance quatre outils Ă chaque commit : un lanceur de tests, un analyseur statique, un correcteur de style de code, et lâaudit intĂ©grĂ© Ă Composer. Aucun de ces outils nâest livrĂ© avec le langage, mais tous sâinstallent avec un seul composer require --dev et se lancent depuis vendor/bin/. Si vous avez dĂ©jĂ utilisĂ© pytest avec mypy, ou Jest avec tsc et Prettier, vous connaissez la forme de cette chaĂźne et il ne vous reste que les noms Ă apprendre.
Ce chapitre nomme deux outils pour chaque tùche, non par indécision, mais parce que les deux sont largement utilisés et bons, et que le projet que vous venez de rejoindre a de toute façon déjà choisi le sien.
Les tests
Deux lanceurs de tests dominent lâĂ©cosystĂšme. PHPUnit est le lanceur de style xUnit sur lequel tous les autres outils de test PHP sâappuient, et Pest est une couche describe-et-it posĂ©e sur le moteur de PHPUnit. Ils partagent les assertions, les mocks, la configuration et la machinerie de couverture, et ne diffĂšrent que par la façon dont un test se lit.
La fonction Ă tester :
<?php
declare(strict_types=1);
namespace App;
function slugify(string $title): string
{
$slug = strtolower(trim($title));
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
return trim($slug, '-');
}
Une fonction dans un espace de noms nâest pas une classe, donc PSR-4 ne peut pas la trouver : le fichier va dans une entrĂ©e files du bloc autoload de composer.json, et Composer le charge avec require Ă chaque exĂ©cution.
Le mĂȘme test, PHPUnit dâabord :
<?php
declare(strict_types=1);
namespace App\Tests;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use function App\slugify;
final class SlugifyTest extends TestCase
{
#[Test]
public function itLowercasesAndJoinsWithDashes(): void
{
self::assertSame('hello-world', slugify('Hello World'));
}
#[Test]
#[DataProvider('edgeCases')]
public function itHandlesEdgeCases(string $input, string $expected): void
{
self::assertSame($expected, slugify($input));
}
public static function edgeCases(): iterable
{
yield 'leading punctuation' => ['!!Hi', 'hi'];
yield 'empty' => ['', ''];
yield 'unicode is stripped' => ['café', 'caf'];
}
}
Puis Pest :
<?php
declare(strict_types=1);
use function App\slugify;
it('lowercases and joins with dashes', function () {
expect(slugify('Hello World'))->toBe('hello-world');
});
it('handles edge cases', function (string $input, string $expected) {
expect(slugify($input))->toBe($expected);
})->with([
'leading punctuation' => ['!!Hi', 'hi'],
'empty' => ['', ''],
'unicode is stripped' => ['café', 'caf'],
]);
Lancez-les avec vendor/bin/phpunit ou vendor/bin/pest. La configuration vit dans phpunit.xml Ă la racine du projet, et Pest lit le mĂȘme fichier : on y dĂ©clare quels dossiers contiennent les tests, sâil faut Ă©chouer sur un warning, et quelles variables dâenvironnement dĂ©finir.
Les conventions PHPUnit Ă connaĂźtre dĂšs le premier jour : une classe de test se termine par Test et Ă©tend TestCase, une mĂ©thode de test porte lâattribut #[Test] ou commence par test, setUp() sâexĂ©cute avant chaque test, self::assertSame() est lâassertion stricte (il existe un assertEquals(), qui jongle avec les types comme ==, donc prĂ©fĂ©rez assertSame()), et $this->createMock(SomeInterface::class) renvoie un double de test que vous configurez avec ->method('name')->willReturn($value).
La couverture nâest pas intĂ©grĂ©e au langage et demande une extension qui observe quelles lignes sâexĂ©cutent, soit Xdebug (avec xdebug.mode=coverage), soit PCOV, qui ne fait que de la couverture et le fait plus vite. Dans les deux cas, vendor/bin/phpunit --coverage-text affiche le rapport.
Le test unicode ci-dessus documente un bug :
slugify('cafĂ©')supprime leĂ©au lieu de le translittĂ©rer. Un test qui fige le comportement actuel reste un test utile, et vous corrigerez la fonction plus tard avec leTransliteratordâintl.
Lâanalyse statique
PHP vĂ©rifie les types Ă lâexĂ©cution, un appel Ă la fois, et il ne vous dira donc jamais quâune fonction situĂ©e trois fichiers plus loin peut renvoyer null sans que vous le gĂ©riez. PHPStan et Psalm jouent le rĂŽle du compilateur que PHP nâa pas : ils lisent toute la base de code, suivent chaque type Ă travers chaque appel, et signalent ce qui Ă©chouerait avant que quoi que ce soit ne sâexĂ©cute, comme mypy le fait pour Python ou le vĂ©rificateur de types de tsc pour TypeScript.
<?php
declare(strict_types=1);
function findUser(int $id): ?User
{
return $id === 1 ? new User('Ada') : null;
}
echo findUser(2)->name;
PHP exĂ©cute ce code et plante sur la seconde ligne avec « Attempt to read property on null », alors que les deux analyseurs le refusent avant mĂȘme de lâexĂ©cuter :
Cannot access property $name on User|null.
Les deux outils fonctionnent par niveaux : PHPStan va de 0 (erreurs Ă©videntes seulement) Ă 10 (chaque mixed doit ĂȘtre prĂ©cisĂ©), et Psalm compte dans lâautre sens, de 8 (permissif) Ă 1 (strict). Un nouveau projet dĂ©marre au niveau le plus strict quâil peut tenir et nâen redescend jamais, tandis quâun projet ancien gĂ©nĂšre une baseline, un fichier qui liste toutes les erreurs actuelles pour que seules les nouvelles fassent Ă©chouer le build, puis la rĂ©duit au fil du temps.
Un phpstan.neon minimal :
parameters:
level: 8
paths:
- src
- tests
Les deux outils lisent les docblocks pour ce que le langage ne sait pas exprimer : @param list<int> $ids, @return array<string, User>, et les gĂ©nĂ©riques @template prĂ©sentĂ©s dans Le systĂšme de types. Câest dans ces docblocks que vivent les gĂ©nĂ©riques en PHP : le moteur les ignore et lâanalyseur les fait respecter.
Le style de code
PER Coding Style, publiĂ© par le PHP-FIG, est le guide de style de rĂ©fĂ©rence. Il a succĂ©dĂ© Ă PSR-12, qui avait succĂ©dĂ© Ă PSR-2, et tous les frameworks et la plupart des bibliothĂšques le suivent : quatre espaces, accolades sur leur propre ligne pour les classes et les fonctions, sur la mĂȘme ligne pour les structures de contrĂŽle, une classe par fichier. Vous nâavez pas Ă lâapprendre par cĆur, puisquâun outil lâapplique pour vous.
PHP-CS-Fixer réécrit les fichiers pour respecter un jeu de rĂšgles. PHP_CodeSniffer signale les violations avec phpcs et corrige ce quâil peut avec phpcbf. Les deux acceptent PER en une ligne de configuration. Pour PHP-CS-Fixer, .php-cs-fixer.dist.php :
<?php
declare(strict_types=1);
$finder = PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests']);
return (new PhpCsFixer\Config())
->setRules(['@PER-CS' => true])
->setFinder($finder);
Pour PHP_CodeSniffer, phpcs.xml :
<?xml version="1.0"?>
<ruleset name="project">
<rule ref="PSR12"/>
<file>src</file>
<file>tests</file>
</ruleset>
Choisissez-en un et lancez-le en CI, ce qui met fin aux discussions sur le placement des accolades en revue de code.
Les migrations
Rector réécrit votre code vers une version plus rĂ©cente de PHP, ou dâun framework, automatiquement. Il sait que Foo $x = null doit devenir ?Foo $x = null, quâun switch qui renvoie une valeur est un match, quâun constructeur qui affecte des propriĂ©tĂ©s peut les promouvoir. Donnez-lui une version cible et il fait la partie mĂ©canique dâune migration :
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\ValueObject\PhpVersion;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
->withPhpVersion(PhpVersion::PHP_84)
->withPreparedSets(deadCode: true, codeQuality: true);
vendor/bin/rector --dry-run montre le diff. Sans lâoption, il lâapplique. Le chapitre Revenir Ă PHP aprĂšs des annĂ©es est, en pratique, la liste de ce que Rector fera au code que vous avez laissĂ© derriĂšre vous.
Le débogage
var_dump($value) affiche une valeur avec son type sans interrompre lâexĂ©cution, et var_dump($value); exit; reste le dĂ©bogueur le plus rapide qui soit. Les frameworks ajoutent un dump() et un dd() (dump and die) plus agrĂ©ables Ă lire, mais celui du langage a lâavantage de marcher partout.
Xdebug est le seul dĂ©bogueur pas Ă pas de lâĂ©cosystĂšme. Une fois lâextension installĂ©e et xdebug.mode=debug placĂ© dans php.ini, votre Ă©diteur sâarrĂȘte sur les points dâarrĂȘt, montre la pile et vous laisse inspecter les variables, dans les requĂȘtes web comme dans les scripts CLI. Câest une extension rĂ©servĂ©e au dĂ©veloppement, parce quâelle ralentit tout : les builds de production sâen passent, et quand il vous faut de la vitesse en local, php -d xdebug.mode=off script.php la dĂ©sactive le temps dâune exĂ©cution.
CĂŽtĂ© Ă©diteurs, PhpStorm intĂšgre le langage et VS Code a besoin dâune extension PHP. Les deux lisent les mĂȘmes docblocks que les analyseurs, donc les gĂ©nĂ©riques que vous Ă©crivez pour PHPStan ou Psalm alimentent aussi lâautocomplĂ©tion.
Relier le tout
Les scripts Composer donnent au projet un vocabulaire unique, quels que soient les outils choisis. Dans composer.json :
{
"scripts": {
"test": "phpunit",
"lint": "php-cs-fixer fix --dry-run --diff",
"lint:fix": "php-cs-fixer fix",
"analyse": "phpstan analyse",
"check": ["@lint", "@analyse", "@test"]
}
}
composer check lance les trois. Composer place vendor/bin dans le chemin des scripts, donc les noms dâoutils nâont pas besoin de prĂ©fixe. Un nouveau membre de lâĂ©quipe lit composer.json et sait comment le projet est vĂ©rifiĂ© sans ouvrir de README.
La CI lance les mĂȘmes commandes, sur chaque version de PHP que le projet prend en charge (la contrainte php de composer.json dit lesquelles), plus composer audit, qui confronte composer.lock Ă la base des vulnĂ©rabilitĂ©s connues et Ă©choue Ă la premiĂšre trouvĂ©e. composer outdated liste ce qui a une version plus rĂ©cente ; un robot de mise Ă jour des dĂ©pendances peut ouvrir les pull requests pour vous.
Pour un runtime local, php -S localhost:8000 -t public sert le projet comme lâa montrĂ© Une requĂȘte web, sans framework, et lâimage Docker officielle php:8.5-cli donne Ă tout le monde le mĂȘme interprĂ©teur. Nâimporte quel rĂ©glage de php.ini peut ĂȘtre surchargĂ© pour une commande avec php -d memory_limit=1G. Sachez enfin que les fichiers .env sont une convention de bibliothĂšques, que plusieurs paquets chargent dans lâenvironnement, et non quelque chose que PHP lit de lui-mĂȘme.
Le piĂšge
Les deux erreurs classiques tiennent au moment oĂč lâon fait les choses.
La premiĂšre consiste Ă Ă©crire des tests qui touchent la base de donnĂ©es par dĂ©faut : ils passent sur la machine de lâauteur, prennent des minutes en CI, et finissent par ne plus ĂȘtre lancĂ©s. Testez la fonction, passez la dĂ©pendance par le constructeur, et gardez les tests dâintĂ©gration dans leur propre dossier avec leur propre suite dans phpunit.xml, pour quâils sâexĂ©cutent quand on le dĂ©cide.
La seconde consiste Ă lancer lâanalyse statique Ă la fin du projet. Une base de code qui atteint le niveau 8 dĂšs le premier jour y reste sans y penser, alors quâune base de code qui rencontre PHPStan aprĂšs deux ans lâaccueille avec quatre mille erreurs et une baseline que personne ne rĂ©duit. Ajoutez lâanalyseur au premier commit, au niveau le plus Ă©levĂ© qui passe, et montez-le dĂšs que câest possible.
Une fois les outils en place, il reste la question que tout dĂ©veloppeur polyglotte pose la premiĂšre semaine, celle de lâasynchrone, et Concurrence et performance y rĂ©pond.
Concurrence et performance
PHP est synchrone : un processus exĂ©cute une requĂȘte, bloque Ă chaque appel dâentrĂ©e-sortie, et ce comportement est voulu. Il nây a pas de boucle dâĂ©vĂ©nements Ă alimenter, pas de mot-clĂ© async Ă Ă©crire, pas de goroutine Ă lancer, parce que la concurrence vient de lâextĂ©rieur du processus : le pool FPM fait tourner autant de copies de votre script que vous en avez configurĂ©es, chacune seule dans sa mĂ©moire, et le systĂšme dâexploitation les rĂ©partit sur les cĆurs.
Si vous venez de Node, cela ressemble Ă un retour en arriĂšre, mais la comparaison ne tient pas. Node a besoin dâune boucle dâĂ©vĂ©nements parce quâun seul processus sert toutes les connexions, si bien quâun seul appel bloquant les figerait toutes. PHP a donnĂ© Ă chaque requĂȘte son propre processus, donc un appel bloquant ne coĂ»te rien que quiconque puisse voir : une requĂȘte en base qui prend 40 millisecondes occupe un worker pendant 40 millisecondes, et les autres workers ne sâen aperçoivent pas.
Ce que vous nâavez pas
Il nây a pas de threads cĂŽtĂ© utilisateur. Une extension parallel existe pour les builds thread-safe (ZTS), et presque personne ne sâen sert. Le build standard est NTS, non thread-safe, parce que le modĂšle sans Ă©tat partagĂ© nâa jamais eu besoin de threads.
Il nây a pas non plus dâordonnanceur intĂ©grĂ©. Les Fibers (PHP 8.1) sont des coroutines Ă pile : une fonction peut se suspendre, et celui qui dĂ©tient la fiber peut la reprendre plus tard, sans que rien dans le langage ne dĂ©cide du moment de la reprise ni ne multiplexe les sockets. Une fiber est une brique de base, et les bibliothĂšques asynchrones sâen servent pour quâun code dâapparence ordinaire puisse cĂ©der la main au milieu dâun appel bloquant.
<?php
declare(strict_types=1);
$fiber = new Fiber(function (string $greeting): string {
$name = Fiber::suspend('who is there?');
return "$greeting, $name";
});
$question = $fiber->start('Hello'); // runs until suspend()
echo $question, PHP_EOL; // who is there?
$fiber->resume('Ada'); // runs to the return
echo $fiber->getReturn(), PHP_EOL; // Hello, Ada
Vous lirez du code comme celui-ci dans une bibliothĂšque, mais vous ne lâĂ©crirez pas dans une application. LâĂ©quivalent Python serait une coroutine Ă base de gĂ©nĂ©rateurs comme on en Ă©crivait avant asyncio, câest-Ă -dire le mĂ©canisme sans le runtime.
Quand vous avez vraiment besoin dâasynchrone
Certaines charges ne rentrent pas dans le modĂšle « une requĂȘte, un processus » : un serveur websocket qui tient dix mille connexions inactives, du long polling, un crawler qui fait cent appels HTTP sortants Ă la fois. Pour celles-lĂ , PHP a des runtimes asynchrones, et ce sont des bibliothĂšques, pas des fonctionnalitĂ©s du langage. Par ordre alphabĂ©tique : AMPHP, ReactPHP, et Swoole ou son fork OpenSwoole, qui est une extension. Les deux premiers sont du PHP pur bĂąti sur les fibers et la sĂ©lection de flux ; Swoole apporte sa propre boucle dâĂ©vĂ©nements en C.
Les runtimes worker de Comment PHP sâexĂ©cute, FrankenPHP et RoadRunner, sont une autre rĂ©ponse Ă une autre question : ils gardent votre application amorcĂ©e entre les requĂȘtes, toujours une requĂȘte Ă la fois par worker. Ils suppriment le coĂ»t de dĂ©marrage, mais ils ne rendent pas votre code concurrent.
Avant de vous tourner vers lâun dâeux, demandez-vous si le problĂšme est vraiment la concurrence, parce quâune application web classique nâen a jamais besoin et que dix workers FPM de plus ne coĂ»tent quâune ligne de configuration.
Le travail en arriĂšre-plan
Une requĂȘte dispose de trente secondes et doit envoyer une rĂ©ponse, donc tout ce qui prend plus longtemps, ou tout ce que lâutilisateur nâattend pas, doit sortir de la requĂȘte.
Lâidiome consiste Ă associer une file dâattente et un worker. La requĂȘte dĂ©pose un travail (une ligne dans une table, un message dans un broker) et rend la main. Un script CLI, lancĂ© par un superviseur de processus, boucle indĂ©finiment en tirant les travaux et en les exĂ©cutant. Il nâa ni limite de temps ni limite de mĂ©moire Ă moins que vous ne les fixiez, donc fixez-les : memory_limit dans lâini, et un compteur qui sort proprement aprĂšs quelques milliers de travaux pour que le superviseur relance un processus neuf. Une fuite mĂ©moire dans une boucle sans fin est le seul cas oĂč le nettoyage par requĂȘte de PHP ne vous sauve pas.
Cron couvre le cas planifiĂ©. La CLI a le reste de la boĂźte Ă outils : proc_open() lance un sous-processus avec des tubes, pcntl_fork() duplique le processus courant (CLI seulement, jamais sous FPM), et curl_multi_exec() effectue des requĂȘtes HTTP en parallĂšle sans la moindre bibliothĂšque :
<?php
declare(strict_types=1);
$urls = ['https://example.com/a', 'https://example.com/b', 'https://example.com/c'];
$multi = curl_multi_init();
$handles = [];
foreach ($urls as $url) {
$handle = curl_init($url);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($multi, $handle);
$handles[$url] = $handle;
}
do {
$status = curl_multi_exec($multi, $running);
if ($running) {
curl_multi_select($multi);
}
} while ($running && $status === CURLM_OK);
foreach ($handles as $url => $handle) {
echo $url, ': ', strlen((string) curl_multi_getcontent($handle)), " bytes\n";
curl_multi_remove_handle($multi, $handle);
}
Trois requĂȘtes partent en mĂȘme temps pour une seule attente, et câest tout le parallĂ©lisme dont la plupart des scripts auront jamais besoin.
OĂč passe le temps
LâinterprĂ©teur est rarement le goulot dâĂ©tranglement. Une requĂȘte passe son temps Ă attendre la base de donnĂ©es, le cache, le systĂšme de fichiers et dâautres services. Optimiser une boucle qui tourne en deux millisecondes pendant quâune requĂȘte SQL en prend quatre-vingts est lâerreur classique, et elle ne dĂ©pend pas du langage.
Le seul rĂ©glage qui compte est OPcache, dĂ©crit dans Comment PHP sâexĂ©cute. Assurez-vous quâil est actif et que opcache.memory_consumption est assez grand pour toute la base de code (opcache_get_status() vous le dit). Deux raffinements viennent par-dessus :
- Le préchargement (
opcache.preload=preload.php) compile une liste de fichiers une fois au dĂ©marrage de FPM et les garde liĂ©s en mĂ©moire, si bien que les classes nâont plus besoin dâautoloading du tout. Il faut un redĂ©marrage pour prendre en compte les changements, et câest pourquoi câest un rĂ©glage de production. - Le JIT compile les chemins chauds en code machine. Il accĂ©lĂšre le travail gourmand en CPU, parfois beaucoup, et accĂ©lĂšre trĂšs peu une requĂȘte web ordinaire. Activez-le (
opcache.jit=tracing,opcache.jit_buffer_size=64M), mesurez, gardez-le sâil a aidĂ©.
Lâautoloading a un coĂ»t, et Composer peut en supprimer lâessentiel. composer dump-autoload -o gĂ©nĂšre une carte de classes pour quâaucune recherche sur le systĂšme de fichiers nâait lieu par classe ; --classmap-authoritative va plus loin et ne touche jamais au systĂšme de fichiers pour une classe absente de la carte. Les deux ont leur place dans le script de dĂ©ploiement. realpath_cache_size dans lâini, quelques mĂ©gaoctets, Ă©vite Ă PHP de rĂ©soudre Ă nouveau les chemins Ă chaque requĂȘte.
Mesurer
Rien de ce qui précÚde ne vaut la peine avant une mesure. Le langage fournit les deux primitives :
<?php
declare(strict_types=1);
$numbers = range(1, 1_000_000);
$start = hrtime(true);
$doubled = array_map(fn (int $n): int => $n * 2, $numbers);
$mapTime = hrtime(true) - $start;
$start = hrtime(true);
$doubled = [];
foreach ($numbers as $n) {
$doubled[] = $n * 2;
}
$loopTime = hrtime(true) - $start;
printf("array_map: %.1f ms\n", $mapTime / 1e6);
printf("foreach: %.1f ms\n", $loopTime / 1e6);
printf("peak memory: %.1f MB\n", memory_get_peak_usage() / 1e6);
Les deux mesures tombent dans les dizaines de millisecondes pour un million dâĂ©lĂ©ments, avec un Ă©cart faible qui dĂ©pend de la version de PHP. La leçon nâest pas de savoir lequel gagne, mais quâun million dâitĂ©rations coĂ»te moins quâune seule requĂȘte SQL lente, ce qui vous laisse libre dâĂ©crire la version la plus lisible.
Pour un vrai profil, Xdebug a un mode profileur (xdebug.mode=profile) qui Ă©crit des graphes dâappels que votre Ă©diteur peut ouvrir, et des profileurs par Ă©chantillonnage existent sous forme dâextensions pour la production, oĂč le surcoĂ»t de Xdebug est inacceptable. Pointez lâun ou lâautre sur une requĂȘte lente et lisez le haut de la liste.
La mĂ©moire suit la mĂȘme rĂšgle. Une requĂȘte qui construit un tableau de cent mille lignes et meurt sur memory_limit a besoin dâun gĂ©nĂ©rateur, comme lâa montrĂ© Fonctions et closures, pas dâune limite plus haute. unset() libĂšre une variable, et le ramasse-miettes gĂšre seul les cycles de rĂ©fĂ©rences ; gc_collect_cycles() force une passe, quâun worker de longue durĂ©e peut appeler entre deux travaux.
Le piĂšge
La premiĂšre erreur consiste Ă importer un modĂšle de concurrence parce que le langage prĂ©cĂ©dent en avait besoin. Un runtime asynchrone sous une application CRUD ajoute une couche, un jeu de bibliothĂšques qui doivent ĂȘtre compatibles avec les fibers, et une classe de bugs (lâĂ©tat partagĂ© entre les requĂȘtes) que FPM rendait impossibles. Le gain est nul, parce que les requĂȘtes ne sâattendaient jamais les unes les autres.
La seconde consiste Ă optimiser sans chiffre. Lâoption JIT, la carte de classes ou la boucle réécrite sont autant dâhypothĂšses, et seule une mesure avec hrtime() sur le chemin lent, avant et aprĂšs, en fait des rĂ©sultats.
Le runtime, le langage et les outils sont maintenant couverts, et il reste un lecteur Ă servir : Revenir Ă PHP aprĂšs des annĂ©es sâadresse au dĂ©veloppeur dont le dernier PHP contenait encore mysql_query.
Revenir à PHP aprÚs des années
Vous avez Ă©crit du PHP il y a des annĂ©es, peut-ĂȘtre beaucoup, en 2008 ou en 2015 sur un projet qui Ă©tait dĂ©jĂ vieux Ă lâĂ©poque. Vous vous souvenez de mysql_query, de array(), du require_once en tĂȘte de chaque fichier, et dâun langage qui laissait tout passer. Maintenant que vous y revenez, la premiĂšre chose Ă savoir est que presque toutes les habitudes dont vous vous souvenez ont un remplaçant moderne, et que la plupart des anciennes formes sont dĂ©prĂ©ciĂ©es ou supprimĂ©es.
Ce chapitre est une liste de paires qui met en regard ce dont vous vous souvenez et ce que vous Ă©crivez aujourdâhui. Le reste du livre dĂ©taille chaque remplaçant, et ce chapitre sert de carte pour sây retrouver.
La base de données
Vous vous souvenez dâavoir assemblĂ© du SQL Ă la main pour le passer Ă mysql_query(). Les fonctions mysql_* ont Ă©tĂ© supprimĂ©es en PHP 7.0, si bien que le code qui les appelle ne tourne sur aucune version encore maintenue du langage. Le remplaçant est PDO avec des requĂȘtes prĂ©parĂ©es, ce qui referme au passage la faille dâinjection que lâancien style ouvrait :
<?php
declare(strict_types=1);
// then
// $result = mysql_query("SELECT * FROM users WHERE id = " . $_GET['id']);
// now
$pdo = new PDO('sqlite::memory:', options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => (int) ($_GET['id'] ?? 0)]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
mysqli existe toujours et fonctionne trĂšs bien, mais PDO parle Ă toutes les bases avec une seule API. Une requĂȘte web, sans framework le montre en situation.
Charger le code
Vous vous souvenez dâun mur de lignes require_once, ou dâune fonction __autoload() maison. Aujourdâhui, une seule ligne charge tout : require __DIR__ . '/vendor/autoload.php';. Composer gĂ©nĂšre ce fichier Ă partir dâune correspondance espace de noms vers dossier dĂ©clarĂ©e dans composer.json, et __autoload() elle-mĂȘme a Ă©tĂ© supprimĂ©e en 8.0. Les classes vivent Ă raison dâune par fichier, nommĂ© dâaprĂšs la classe, et sont trouvĂ©es Ă la premiĂšre utilisation. Namespaces, Composer et autoloading dĂ©crit la mise en place, qui prend cinq minutes.
Composer a aussi remplacĂ© lâhabitude de copier une bibliothĂšque dans son projet. Depuis 2012, composer require vendor/package la rĂ©cupĂšre sur Packagist, fige la version dans un fichier de verrouillage, et la met Ă jour quand vous le demandez.
Une syntaxe qui a raccourci
Ce sont de petits changements, mais vous les verrez sur chaque ligne.
// then
$list = array(1, 2, 3);
$name = isset($_GET['n']) ? $_GET['n'] : 'anon';
$double = function ($x) use ($factor) {
return $x * $factor;
};
$callback = array($obj, 'method');
call_user_func_array($callback, array(1));
// now
$list = [1, 2, 3];
$name = $_GET['n'] ?? 'anon';
$double = fn($x) => $x * $factor;
$callback = $obj->method(...);
$callback(1);
[] est arrivĂ© en 5.4, ?? en 7.0, les fonctions flĂ©chĂ©es fn en 7.4, et la syntaxe de callable de premiĂšre classe $obj->method(...) en 8.1. Les callables sous forme de chaĂźne comme 'Class::method' et call_user_func() fonctionnent toujours ; plus personne ne les Ă©crit, parce que la nouvelle forme est vĂ©rifiĂ©e par lâĂ©diteur et par lâanalyseur, et quâun callable sâappelle tout simplement avec des parenthĂšses.
Les types
Vous vous souvenez de fonctions qui acceptaient nâimporte quoi et renvoyaient ce qui venait. Aujourdâhui les fonctions dĂ©clarent leurs types, et une ligne par fichier oblige PHP Ă les faire respecter.
<?php
declare(strict_types=1);
// then
// function total($items, $rate) { ... }
// now
function total(array $items, float $rate): float
{
return array_sum($items) * $rate;
}
total([10, 20], '1.2'); // TypeError: must be of type float, string given
Les types scalaires sont arrivés en 7.0, les types de retour avec eux, le nullable ?int en 7.1, les types union en 8.0, et les propriétés typées en 7.4. declare(strict_types=1) désactive la conversion silencieuse pour les appels faits depuis ce fichier. Le systÚme de types donne les rÚgles précises.
Flux de contrĂŽle et constantes
Vous vous souvenez de switch, de son fallthrough et de sa comparaison lĂąche, et dâune classe remplie de const STATUS_ACTIVE = 'active';. match a remplacĂ© le premier, les Ă©numĂ©rations le second.
<?php
declare(strict_types=1);
enum Status: string
{
case Active = 'active';
case Archived = 'archived';
}
function label(Status $status): string
{
return match ($status) {
Status::Active => 'In use',
Status::Archived => 'Put away',
};
}
match (8.0) compare avec ===, ne traverse pas les branches, et lĂšve une exception si aucune ne convient. Les Ă©numĂ©rations (8.1) sont de vrais types : une fonction typĂ©e Status ne peut pas recevoir la chaĂźne 'deleted'. ĂnumĂ©rations et match va plus loin.
Les classes
Vous vous souvenez dâune propriĂ©tĂ© privĂ©e, dâun getter et dâun setter, multipliĂ©s par dix dans chaque classe. La promotion de propriĂ©tĂ©s dans le constructeur et readonly ramĂšnent tout cela Ă une ligne par propriĂ©tĂ©.
<?php
declare(strict_types=1);
final class Money
{
public function __construct(
public readonly int $cents,
public readonly string $currency,
) {
}
}
$price = new Money(1999, 'EUR');
echo $price->cents; // 1999
$price->cents = 0; // Error: Cannot modify readonly property
La promotion est arrivĂ©e en 8.0, readonly en 8.1. Les hooks de propriĂ©tĂ© (PHP 8.4) couvrent le cas oĂč un getter calculait vraiment quelque chose, en attachant des blocs get et set Ă la propriĂ©tĂ© elle-mĂȘme. Les classes montre tout cela.
Parmi ce dont vous vous souvenez, deux pratiques sont dĂ©sormais supprimĂ©es ou dĂ©prĂ©ciĂ©es. Les constructeurs Ă la PHP 4, oĂč la mĂ©thode portait le nom de la classe, ont Ă©tĂ© supprimĂ©s en 8.0. Les propriĂ©tĂ©s dynamiques, affecter $obj->whatever sans lâavoir dĂ©clarĂ©e, sont dĂ©prĂ©ciĂ©es depuis 8.2 et doivent devenir une erreur dans la prochaine version majeure.
Les erreurs
Vous vous souvenez de @mysql_connect(...) or die('no db'), et dâavertissements imprimĂ©s au beau milieu de la page. Aujourdâhui les erreurs sont des exceptions, et vous les rattrapez. Les fonctions internes lĂšvent TypeError et ValueError au lieu de renvoyer false avec un avertissement, la division par zĂ©ro lĂšve une exception, et les frameworks convertissent les avertissements restants en exceptions avec un gestionnaire dâerreurs. @ existe toujours, mais considĂ©rez-le comme le signe dâun problĂšme Ă corriger. Erreurs et exceptions explique la hiĂ©rarchie.
Les globales
Vous vous souvenez de global $db; en tĂȘte de chaque fonction. Aujourdâhui la dĂ©pendance entre par le constructeur.
<?php
declare(strict_types=1);
final class UserRepository
{
public function __construct(private readonly PDO $pdo)
{
}
}
Lâobjet qui a besoin dâune base de donnĂ©es en reçoit une, ce qui permet de le tester avec une autre. Les frameworks automatisent ce cĂąblage avec un conteneur, mais lâidĂ©e elle-mĂȘme nâen a pas besoin.
register_globals, qui transformait chaque paramĂštre de requĂȘte en variable, a Ă©tĂ© supprimĂ© en 5.4. $_REQUEST existe toujours et personne ne sâen sert : lisez $_GET ou $_POST et dites lequel vous voulez. extract() et les variables variables ($$name) restent lĂ©gales et nâapparaissent dans aucune base de code moderne.
Texte et dates
Vous vous souvenez de date('Y-m-d', $timestamp) et de utf8_encode(). Les dates sont des objets DateTimeImmutable, et le texte est en UTF-8 partout.
<?php
declare(strict_types=1);
$due = new DateTimeImmutable('2026-03-01', new DateTimeZone('UTC'));
echo $due->modify('+30 days')->format('Y-m-d'); // 2026-03-31
utf8_encode() et utf8_decode() nâont jamais gĂ©rĂ© que le Latin-1 et sont dĂ©prĂ©ciĂ©es depuis 8.2 ; mb_convert_encoding() fait le travail pour nâimporte quel encodage. ereg_* a Ă©tĂ© supprimĂ© en 7.0, preg_* est restĂ©. La forme ${var} dâinterpolation dans les chaĂźnes est dĂ©prĂ©ciĂ©e depuis 8.2 ; Ă©crivez {$var}. ChaĂźnes, nombres, dates et JSON a le reste.
Petites choses disparues
each()etcreate_function(), supprimées en 8.0. Utilisezforeachet les closures.- Le cast
(unset), supprimé en 8.0. - Les paramÚtres implicitement nullables,
Foo $x = nullsans le?, dĂ©prĂ©ciĂ©s en 8.4. Ăcrivez?Foo $x = null. split(), supprimĂ©e en 7.0. Utilisezexplode()oupreg_split().mb_internal_encoding('UTF-8')en tĂȘte des fichiers. Le dĂ©faut est UTF-8 depuis 5.6.
Faire passer une base de code PHP 5 sur PHP 8
Elle ne tournera pas telle quelle, les appels mysql_* Ă eux seuls le garantissent. La partie mĂ©canique, au moins, est automatisĂ©e : Rector réécrit lâancienne syntaxe en syntaxe nouvelle, version par version, Ă partir dâune configuration qui nomme votre cible. Pointez-le sur le code, relisez le diff, lancez les tests que vous avez, espĂ©rons-le, et recommencez. PHPStan ou Psalm trouvent ensuite ce que Rector nâa pas pu faire. Tests, analyse statique et outillage prĂ©sente les deux.
PrĂ©voyez ce travail dans votre planning, car un site qui tourne encore sous PHP 5 aujourdâhui utilise une version qui ne reçoit plus de correctifs de sĂ©curitĂ© depuis 2018, et il reprĂ©sente un risque avant mĂȘme dâĂȘtre une base de code.
Le rythme des versions
PHP publie dĂ©sormais une version mineure chaque novembre : 8.0 en 2020, 8.1 en 2021, et ainsi de suite jusquâĂ 8.5 en 2025. Chaque version reçoit deux ans de support actif puis deux ans de correctifs de sĂ©curitĂ© ; php.net/supported-versions donne les dates. Une version par an, câest une petite liste de dĂ©prĂ©ciations Ă lire chaque annĂ©e, et une base de code qui ne sâĂ©loigne jamais beaucoup du prĂ©sent.
Jugez le langage sur ses notes de version plutĂŽt que sur la base de code que vous retrouvez, parce que cette base de code est une photo de lâĂ©poque oĂč elle a Ă©tĂ© Ă©crite, alors que le langage a continuĂ© dâavancer.
Pour aller plus loin liste les endroits oĂč vous tenir Ă jour.
Pour aller plus loin
Le livre sâarrĂȘte ici, mais le langage continue dâĂ©voluer, et le projet quâon vous a confiĂ© aussi. Quelques sources vous permettent de rester Ă jour, et aucune nâappartient Ă un Ă©diteur commercial.
Le manuel
php.net est la référence, et une bonne référence : chaque fonction a sa page, avec sa signature, son historique par version, et des exemples qui tournent. Les notes contribuées par les utilisateurs sous chaque page sont inégales, alors que le texte officiel au-dessus est fiable. Mettez en favori les guides de migration, un par version (php.net/manual/fr/migration85.php et ses voisins) : ils listent chaque dépréciation et chaque nouveauté, et lire celui de la prochaine version de votre projet est la préparation de montée de version la moins chÚre que vous ferez.
Le wiki des RFC
Chaque changement du langage passe par une proposition publique, une discussion sur la liste de diffusion internals, et un vote des dĂ©veloppeurs du cĆur. Les propositions vivent sur wiki.php.net/rfc, acceptĂ©es, refusĂ©es et en cours. Lire les RFC acceptĂ©es dâune version vous dit non seulement ce qui a changĂ©, mais pourquoi, avec les alternatives Ă©cartĂ©es et les arguments contre. Quand une fonctionnalitĂ© paraĂźt Ă©trange, sa RFC explique en gĂ©nĂ©ral la contrainte qui lâa rendue ainsi.
La Fondation et le FIG
La PHP Foundation finance les dĂ©veloppeurs du cĆur qui maintiennent lâinterprĂ©teur et accompagne la direction du langage. Son blog rend compte des versions et des chantiers en cours. Le PHP-FIG (Framework Interoperability Group) publie les PSR et le PER Coding Style, les interfaces et conventions qui permettent Ă des bibliothĂšques dâauteurs diffĂ©rents de fonctionner ensemble. Quand une base de code mentionne PSR-quelque chose, le site du FIG a la spĂ©cification en deux pages.
Le framework de votre projet
La plupart des projets PHP reposent sur un framework, et la documentation du framework est lâendroit oĂč lâapprendre. Frameworks complets : CakePHP, Laminas, Laravel, Symfony, Yii. Micro-frameworks bĂątis autour des middlewares PSR-15 : Mezzio, Slim. Plateformes de contenu avec leurs propres conventions : Drupal, Joomla, TYPO3, WordPress.
Une habitude paie dĂšs le premier jour : quand vous lisez du code de framework, triez ce que vous voyez entre ce qui relĂšve du langage et ce qui relĂšve du framework. Un constructeur promu readonly, une Ă©numĂ©ration dans un match ou une chaĂźne de ?-> sont du PHP, et ils veulent dire la mĂȘme chose partout. Une façade, une liaison dans un conteneur de services ou un __call magique qui redirige vers un constructeur de requĂȘtes appartiennent au framework, et câest sa documentation qui les explique. Ceux qui confondent les deux finissent par croire que PHP est ce Ă quoi leur premier framework lâa fait ressembler.
Un chemin plus long
Ce livre a sautĂ© les bases volontairement. Si vous voulez la version qui part de zĂ©ro, avec un petit jeu, un outil en ligne de commande et une application web construite sans framework, le volume compagnon, The PHP Book, prend ce chemin Ă un rythme plus lent, et il est publiĂ© depuis le mĂȘme dĂ©pĂŽt que celui-ci.
Au-delĂ de lâĂ©crit, PHP a des groupes dâutilisateurs dans la plupart des grandes villes et des confĂ©rences sur la plupart des continents, et une salle pleine de gens qui ont dĂ©jĂ rĂ©solu le problĂšme que vous allez rencontrer vaut largement lâaprĂšs-midi quâon y passe.
Ce que vous savez maintenant
Il y a deux ou trois heures, vous avez ouvert une base de code et vu des $this->, des :: et un modĂšle dâexĂ©cution que vous ne reconnaissiez pas. Vous savez maintenant comment PHP sâexĂ©cute, comment il type, comment il organise le code en paquets, et oĂč il va vous surprendre, et lâancienne rĂ©putation du langage a retrouvĂ© sa place, dans le chapitre consacrĂ© au passĂ©. Cette base de code est devenue lisible, et vous pouvez retourner la lire.
Annexes
Ces trois annexes sont des tables de rĂ©fĂ©rence, pour les moments oĂč il vous faut un fait prĂ©cis plutĂŽt quâun chapitre. A - Venir de Python, JavaScript ou Java fait correspondre les constructions que vous connaissez dĂ©jĂ Ă leur Ă©criture PHP, une ligne chacune. B - PHP 8.0 Ă 8.5 en un coup dâĆil liste ce que chaque version a apportĂ©, pour que vous sachiez ce que le PHP de votre projet peut utiliser. C - Vocabulaire dĂ©finit les mots qui reviennent dans les conversations PHP et nulle part ailleurs.
A - Venir de Python, JavaScript ou Java
Une ligne par construction, lâĂ©criture que vous connaissez Ă gauche, lâĂ©criture PHP moderne Ă droite. Quand PHP propose une ancienne forme et une nouvelle, seule la nouvelle figure ici.
| Concept | Python | JavaScript | Java | PHP |
|---|---|---|---|---|
| Variable | x = 1 | let x = 1 | int x = 1; | $x = 1; |
| Constante | X = 1 (convention) | const X = 1 | static final int X = 1; | const X = 1; |
| Interpolation de chaĂźne | f"hi {name}" | `hi ${name}` | "hi " + name | "hi {$name}" |
| Concaténation | a + b | a + b | a + b | $a . $b |
| ChaĂźne multiligne | """...""" | `...` | """...""" | <<<TXT ... TXT; |
| Division entiĂšre | a // b | Math.trunc(a / b) | a / b | intdiv($a, $b) |
| Puissance | a ** b | a ** b | Math.pow(a, b) | $a ** $b |
| ĂgalitĂ© stricte | a == b | a === b | a.equals(b) | $a === $b |
| Coalescence de null | a if a is not None else b | a ?? b | Optional.ofNullable(a).orElse(b) | $a ?? $b |
| Ternaire | a if c else b | c ? a : b | c ? a : b | $c ? $a : $b |
| Littéral de liste | [1, 2] | [1, 2] | List.of(1, 2) | [1, 2] |
| Littéral de dictionnaire | {"k": 1} | {k: 1} | Map.of("k", 1) | ['k' => 1] |
| Ajout en fin de liste | xs.append(v) | xs.push(v) | xs.add(v) | $xs[] = $v; |
| Lecture avec valeur par défaut | d.get("k", 0) | d.k ?? 0 | d.getOrDefault("k", 0) | $d['k'] ?? 0 |
| Longueur | len(xs) | xs.length | xs.size() | count($xs) |
| Longueur dâune chaĂźne | len(s) | s.length | s.length() | mb_strlen($s) |
| Tranche | xs[1:3] | xs.slice(1, 3) | xs.subList(1, 3) | array_slice($xs, 1, 2) |
| Parcourir une liste | for v in xs: | for (const v of xs) | for (var v : xs) | foreach ($xs as $v) |
| Parcourir un dictionnaire | for k, v in d.items(): | for (const [k, v] of Object.entries(d)) | for (var e : d.entrySet()) | foreach ($d as $k => $v) |
| Map | [f(v) for v in xs] | xs.map(f) | xs.stream().map(f) | array_map($f, $xs) |
| Filtre | [v for v in xs if p(v)] | xs.filter(p) | xs.stream().filter(p) | array_filter($xs, $p) |
| Lambda | lambda x: x * 2 | x => x * 2 | x -> x * 2 | fn($x) => $x * 2 |
| Capture des closures | par référence | par référence | effectivement finale | par valeur (use ($x) ou fn) |
| Argument par défaut | def f(x=1): | function f(x = 1) | surcharge | function f(int $x = 1) |
| Argument nommé | f(x=1) | f({x: 1}) | aucun | f(x: 1) |
| Variadique | def f(*xs): | function f(...xs) | void f(int... xs) | function f(int ...$xs) |
| Classe | class A: | class A {} | class A {} | class A {} |
| Constructeur | def __init__(self, x): | constructor(x) {} | A(int x) {} | public function __construct(public int $x) {} |
| Membre dâinstance | self.x | this.x | this.x | $this->x |
| Membre statique | A.x | A.x | A.x | A::$x |
| Interface | Protocol | aucune (TS : interface) | interface A {} | interface A {} |
| ĂnumĂ©ration | class C(Enum): | aucune (TS : enum) | enum C { A, B } | enum C { case A; case B; } |
| Rattraper une exception | except E as e: | catch (e) | catch (E e) | catch (E $e) |
| Navigation sûre | aucune | a?.b | Optional.map | $a?->b |
| ChaĂźne vers entier | int(s) | parseInt(s) | Integer.parseInt(s) | (int) $s |
| Test de type | isinstance(x, A) | x instanceof A | x instanceof A | $x instanceof A |
| Afficher | print(x) | console.log(x) | System.out.println(x) | echo $x; |
| Import | from a import B | import { B } from 'a' | import a.B; | use A\B; |
| Gestionnaire de paquets | pip, pyproject.toml | npm, package.json | Maven, pom.xml | Composer, composer.json |
| Lanceur de tests | pytest | Jest, Vitest | JUnit | PHPUnit, Pest |
| Formateur | black, ruff | Prettier | google-java-format | PHP-CS-Fixer, PHP_CodeSniffer |
| Analyse statique | mypy, pyright | tsc | le compilateur | PHPStan, Psalm |
| Exécuter un script | python a.py | node a.js | java A.java | php a.php |
| REPL | python | node | jshell | php -a |
Les diffĂ©rences qui ne sont pas quâune question dâĂ©criture
- Une requĂȘte part de rien et finit sans rien, parce quâaucun processus ne reste en vie entre deux requĂȘtes. Voir Comment PHP sâexĂ©cute.
- Les tableaux sont des valeurs : affecter ou passer un tableau en produit une copie, alors que les objets circulent par identifiant. Voir Les tableaux.
- Les closures capturent par valeur, au moment de leur création, et une modification ultérieure de la variable extérieure ne leur parvient pas. Voir Fonctions et closures.
- Le typage strict est un interrupteur par fichier :
declare(strict_types=1)régit les appels faits depuis ce fichier, et seulement lui. Voir Le systÚme de types.
B - PHP 8.0 Ă 8.5 en un coup dâĆil
Cette annexe consacre une section à chaque version, avec les nouveautés principales seulement. Trouvez la version de votre projet, puis lisez vers le bas à partir de là pour voir ce que vous pouvez utiliser.
PHP 8.0, novembre 2020
- Arguments nommés :
str_pad(string: 'a', length: 3). - Attributs :
#[Route('/home')], des métadonnées structurées lues par réflexion. - Promotion de propriétés dans le constructeur :
public function __construct(private int $x) {}. - Types union :
int|string $id. - Expression
match: comparaison stricte, pas de fallthrough, exhaustive. - Opérateur nullsafe :
$user?->address?->city. mixedetstaticcomme types.throwcomme expression :$x = $y ?? throw new Exception();.str_contains(),str_starts_with(),str_ends_with().- Interface
Stringable, implĂ©mentĂ©e automatiquement par toute classe qui dĂ©finit__toString(). WeakMap.- Compilateur JIT, Ă lâintĂ©rieur dâOPcache.
- Comparaisons chaĂźne-nombre assainies :
0 == 'foo'vautfalse. - Virgule finale autorisée dans les listes de paramÚtres.
- Les fonctions internes lĂšvent
TypeErroretValueErrorau lieu dâĂ©mettre un avertissement et de renvoyernulloufalse.
PHP 8.1, novembre 2021
- ĂnumĂ©rations, pures et adossĂ©es :
enum Suit: string { case Hearts = 'H'; }. - Propriétés
readonly:public readonly int $x. - Syntaxe de callable de premiĂšre classe :
strlen(...). - Fibers : des coroutines Ă pile, la brique de base des bibliothĂšques asynchrones.
newdans les initialiseurs :public function __construct(private Logger $l = new NullLogger()) {}.- Types intersection purs :
Countable&Traversable. - Type de retour
never. - Constantes de classe
final. - Dépliage de tableaux avec clés textuelles :
[...$defaults, ...$options]. array_is_list().- Notation octale explicite :
0o16.
PHP 8.2, décembre 2022
- Classes
readonly:final readonly class Point {}. - Types en forme normale disjonctive :
(A&B)|null. - Types
true,falseetnullautonomes. - Propriétés dynamiques dépréciées ;
#[\AllowDynamicProperties]rĂ©autorise une classe. #[\SensitiveParameter]pour masquer un argument dans les traces dâappel.- Constantes dans les traits.
Random\Randomizeret lâextensionrandom.- Cas dâĂ©numĂ©ration utilisables dans les expressions constantes.
PHP 8.3, novembre 2023
- Constantes de classe typées :
const string NAME = 'x';. - Attribut
#[\Override]: le moteur vĂ©rifie quâune mĂ©thode parente existe. json_validate().- AccĂšs dynamique aux constantes de classe :
Foo::{$name}. - Les propriétés
readonlypeuvent ĂȘtre rĂ©initialisĂ©es dans__clone(). Randomizer::getBytesFromString(),Randomizer::getFloat().- Les indices nĂ©gatifs de tableau se comportent de façon cohĂ©rente.
mb_str_pad().
PHP 8.4, novembre 2024
- Hooks de propriété :
public string $name { get => ...; set => ...; }. - Visibilité asymétrique :
public private(set) int $x. newsans parenthĂšses dans un chaĂźnage :new Foo()->bar().- Objets paresseux :
ReflectionClass::newLazyGhost(),newLazyProxy(). - Attribut
#[\Deprecated]pour votre propre code. array_find(),array_find_key(),array_any(),array_all().mb_trim(),mb_ltrim(),mb_rtrim(),mb_ucfirst(),mb_lcfirst().- Nouvelle extension DOM avec un analyseur HTML5 :
Dom\HTMLDocument. - API objet pour BCMath :
BcMath\Number. - Sous-classes PDO par pilote :
Pdo\Sqlite,Pdo\Mysql,Pdo\Pgsql, viaPdo::connect(). request_parse_body()pour les corps de formulaire quelle que soit la méthode HTTP.- ParamÚtres implicitement nullables (
Foo $x = nullsans?) dépréciés. exitetdiesont désormais des fonctions.
PHP 8.5, novembre 2025
- Opérateur pipe :
$slug = $title |> trim(...) |> strtolower(...);. cloneavec mise à jour de propriétés :clone($point, ['x' => 3]).- Attribut
#[\NoDiscard], qui avertit quand une valeur de retour est ignorĂ©e ; cast(void)pour le faire taire volontairement. array_first(),array_last().- Closures et callables de premiĂšre classe dans les expressions constantes (arguments dâattributs, valeurs par dĂ©faut, constantes).
- Attributs sur les constantes.
- Nouvelle extension
uri:Uri\Rfc3986\Uri,Uri\WhatWg\Url. - Les erreurs fatales incluent dĂ©sormais une trace dâappel.
get_error_handler(),get_exception_handler().#[\DelayedTargetValidation].- Constante
PHP_BUILD_DATE. - LâopĂ©rateur dâexĂ©cution shell entre accents graves est dĂ©prĂ©ciĂ©.
Politique de support
Chaque version reçoit deux ans de support actif (corrections de bugs) suivis de deux ans de correctifs de sĂ©curitĂ© seulement. Une version publiĂ©e en novembre 2025 est donc en support actif jusquâĂ fin 2027 et corrigĂ©e jusquâĂ fin 2029. Les dates bougent Ă chaque version, et php.net/supported-versions tient la table Ă jour. Faire tourner une version au-delĂ de sa fenĂȘtre de sĂ©curitĂ© est un risque, et mieux vaut le prendre en connaissance de cause que le dĂ©couvrir aprĂšs coup.
Trouver la version de votre projet
php -v affiche lâinterprĂ©teur que vous exĂ©cutez en local. composer.json dĂ©clare ce que le projet prend en charge sous require.php ("php": "^8.3"), et config.platform.php fige la version contre laquelle Composer rĂ©sout, celle Ă croire quand les deux diffĂšrent. La production peut encore tourner sur autre chose, et seul un phpinfo() ou un php -v sur le serveur permet de trancher.
C - Vocabulaire
Cette annexe définit, en une ou deux phrases chacun, les mots qui reviennent dans les conversations PHP et rarement ailleurs.
SAPI. Server API : la couche qui relie lâinterprĂ©teur Ă ce qui lâexĂ©cute. La ligne de commande, PHP-FPM, mod_php et les runtimes embarquĂ©s sont tous des SAPI au-dessus du mĂȘme moteur.
CLI. La SAPI en ligne de commande, invoquĂ©e par php file.php. Elle nâa par dĂ©faut ni limite de temps dâexĂ©cution ni limite de mĂ©moire, contrairement aux SAPI web.
FPM. FastCGI Process Manager : un pool de processus PHP qui attendent les requĂȘtes derriĂšre un serveur web. La façon standard de servir PHP.
FastCGI. Le protocole par lequel le serveur web transmet une requĂȘte Ă FPM et relit la rĂ©ponse.
mod_php. Le module Apache qui embarque PHP dans le processus du serveur web lui-mĂȘme. Plus ancien que FPM, et toujours prĂ©sent.
OPcache. Le cache de bytecode. Les fichiers compilĂ©s restent en mĂ©moire partagĂ©e, et la requĂȘte suivante sâĂ©pargne lâanalyse. ActivĂ© dans toute installation sĂ©rieuse.
JIT. Compilation Ă la volĂ©e du code chaud en code machine, Ă lâintĂ©rieur dâOPcache, depuis 8.0. Aide les scripts gourmands en CPU ; change peu de chose pour le trafic web ordinaire.
Preloading (prĂ©chargement). Une option dâOPcache qui compile et lie un ensemble de fichiers une fois au dĂ©marrage du serveur, pour que chaque requĂȘte commence avec eux dĂ©jĂ chargĂ©s.
APCu. Un cache mĂ©moire partagĂ© entre les processus PHP dâune mĂȘme machine. Pour les valeurs calculĂ©es une fois et lues souvent, quand un stockage externe serait disproportionnĂ©.
Extension. Un module C compilé qui ajoute des fonctions ou des classes à PHP : pdo_mysql, intl, mbstring, xdebug. php -m liste celles qui sont chargées.
PECL. Le dépÎt historique des extensions non livrées avec PHP, avec son propre installeur.
PIE. Lâinstalleur dâextensions plus rĂ©cent, dans lâesprit de Composer, appelĂ© Ă succĂ©der au circuit PECL.
ZTS et NTS. Les builds thread-safe et non-thread-safe de lâinterprĂ©teur. NTS est le dĂ©faut et celui quâutilise FPM ; ZTS existe pour les rares SAPI multithreadĂ©es et lâextension parallel.
php.ini. Le fichier de configuration. La CLI et FPM en lisent des différents, ce qui explique la plupart des mystÚres du type « ça marche dans le terminal ».
Composer. Le gestionnaire de paquets. Lit composer.json, Ă©crit composer.lock, remplit vendor/, gĂ©nĂšre lâautoloader.
Packagist. Le registre public de paquets oĂč Composer sâapprovisionne.
vendor. Le dossier oĂč Composer installe les paquets, quâon ne modifie jamais Ă la main et quâon ne commite pas.
Autoload. Le mĂ©canisme qui charge le fichier dâune classe Ă sa premiĂšre utilisation, si bien quâon nâĂ©crit plus jamais une ligne require Ă la main.
PSR-4. La correspondance standard entre espace de noms et dossier que suivent les autoloaders : App\Billing\Invoice vit dans src/Billing/Invoice.php.
PHP-FIG. Framework Interoperability Group : lâorganisme qui publie les PSR et le style de code.
PSR. PHP Standards Recommendation : une interface ou une convention numĂ©rotĂ©e (PSR-3 pour les logs, PSR-7 pour les messages HTTP, PSR-15 pour les middlewares) sur laquelle les bibliothĂšques sâaccordent afin dâĂȘtre interchangeables.
PER Coding Style. Le standard de style de code actuel du FIG, successeur de PSR-12. Ce que les formateurs font respecter.
RFC. Request for Comments : la proposition publique par laquelle passe tout changement du langage avant un vote des dĂ©veloppeurs du cĆur.
PHP Foundation. Lâorganisation Ă but non lucratif qui, depuis 2021, salarie des dĂ©veloppeurs du cĆur pour maintenir et faire avancer lâinterprĂ©teur.
php-src. Le dĂ©pĂŽt source de lâinterprĂ©teur lui-mĂȘme, Ă©crit en C.
Zend Engine. Le cĆur de lâinterprĂ©teur : le compilateur et lâexĂ©cuteur. Le nom survit dans quelques rĂ©glages et messages dâerreur.
Superglobal (superglobale). Un tableau intégré au langage, visible dans toutes les portées : $_GET, $_POST, $_SERVER, $_COOKIE, $_FILES, $_SESSION, $_ENV.
Docblock. Un commentaire /** ... */ au-dessus dâun symbole, portant des annotations @param et @return que lisent les Ă©diteurs et les analyseurs statiques. Câest lĂ que vivent les gĂ©nĂ©riques.
Attribute (attribut). Des métadonnées structurées attachées à une classe, une méthode, une propriété ou un paramÚtre avec #[...], lues par réflexion. Ce que les annotations étaient en Java.
Trait. Un bloc de méthodes et de propriétés copié dans toute classe qui le use. De la réutilisation horizontale sans héritage.
Enum (énumération). Un type avec un ensemble fixe de cas nommés, éventuellement adossés à un entier ou une chaßne, avec méthodes et interfaces. Depuis 8.1.
Fiber. Une coroutine Ă pile qui peut se suspendre et reprendre depuis nâimporte quel point de sa pile dâappels. La primitive sur laquelle sâappuient les bibliothĂšques asynchrones ; pas quelque chose que le code applicatif pilote directement.
Generator (gĂ©nĂ©rateur). Une fonction qui produit des valeurs une Ă une avec yield et conserve son Ă©tat entre les appels. De lâitĂ©ration paresseuse sans construire de tableau.
SPL. Standard PHP Library : lâensemble intĂ©grĂ© de structures de donnĂ©es, dâitĂ©rateurs, dâexceptions et dâinterfaces tels que ArrayIterator, SplQueue, Countable, RuntimeException.
PDO. PHP Data Objects : lâabstraction de base de donnĂ©es avec une seule API pour tous les pilotes, requĂȘtes prĂ©parĂ©es comprises.
mbstring. Lâextension de chaĂźnes multi-octets. mb_strlen, mb_substr et leurs semblables comptent des caractĂšres lĂ oĂč les fonctions ordinaires comptent des octets.
intl. Lâextension dâinternationalisation : collation, formatage des nombres et des dates, normalisation Unicode, au-dessus de la bibliothĂšque ICU.
Xdebug. Le débogueur pas à pas et profileur, réservé au développement.
PHPUnit. Le framework de test de style xUnit avec lequel la plupart des tests PHP sont écrits.
Pest. Un framework de test Ă la syntaxe describe et expect, qui tourne sur le moteur de PHPUnit.
PHPStan. Un analyseur statique qui trouve les erreurs de type et le code impossible sans lâexĂ©cuter, avec des niveaux de 0 Ă 10.
Psalm. Lâautre analyseur statique, avec des niveaux de 8 Ă 1 et un accent sur la soliditĂ© des types et lâanalyse de souillure (taint analysis).
Rector. Un outil de refactoring automatisé qui réécrit le code vers une version plus récente de PHP ou une API de bibliothÚque plus récente.
phpt. Le format des fichiers de test de lâinterprĂ©teur lui-mĂȘme, dans php-src. Vous le croisez si vous contribuez Ă PHP ou lisez ses rapports de bug.
strict_types. La déclaration par fichier declare(strict_types=1); qui fait lever une exception, au lieu de convertir, sur les incompatibilités de types scalaires dans les appels faits depuis ce fichier.
Copy-on-write (copie Ă lâĂ©criture). Lâastuce du moteur qui rend lâaffectation de tableaux bon marchĂ© : la copie partage la mĂ©moire de lâoriginal jusquâĂ ce que lâun des deux soit modifiĂ©.
Late static binding (liaison statique tardive). static:: qui se rĂ©sout vers la classe sur laquelle lâappel a Ă©tĂ© fait, pas celle oĂč la mĂ©thode est Ă©crite. self:: est lâautre.
Magic method (mĂ©thode magique). Une mĂ©thode au nom rĂ©servĂ© Ă double tiret bas que le moteur appelle de lui-mĂȘme : __construct, __toString, __get, __call, __clone, __invoke.