Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🐘 Et maintenant, PHP

Un guide de PHP moderne qui se lit en deux Ă  trois heures, pour les dĂ©veloppeurs venus d’un autre langage.

Un petit Ă©lĂ©phant rond tient un panneau indicateur dont les flĂšches Python, JavaScript, Java et Go pointent toutes vers une mĂȘme porte marquĂ©e PHP

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.

Deux Ă©lĂ©phants cĂŽte Ă  cĂŽte : Ă  gauche un vieil Ă©lĂ©phant poussiĂ©reux et rapiĂ©cĂ©, assis sur un tas de code spaghetti emmĂȘlĂ© ; Ă  droite un Ă©lĂ©phant moderne et Ă©lancĂ©, avec un nƓud papillon, debout sur des boĂźtes bien Ă©tiquetĂ©es

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.

Une boucle en quatre Ă©tapes : un navigateur envoie une requĂȘte, un Ă©lĂ©phant tout neuf se rĂ©veille dans une piĂšce vide, il construit la rĂ©ponse sur un Ă©tabli, puis la remet et la piĂšce est nettoyĂ©e pour la requĂȘte suivante

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.

À gauche : une rangĂ©e de petites piĂšces identiques, chacune avec un Ă©lĂ©phant neuf, une requĂȘte qui entre et une rĂ©ponse qui sort, puis la piĂšce vidĂ©e. À droite : une grande piĂšce avec un seul Ă©lĂ©phant qui reste Ă  son bureau pendant qu'une file de requĂȘtes dĂ©file

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.

Trois panneaux cÎte à cÎte : une flÚche marquée -> pointe vers un objet unique, une flÚche marquée :: pointe vers le plan d'une classe, et une double flÚche marquée => relie une carte clé à une carte valeur

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

PythonJavaScriptJavaPHP
Concaténationa + ba + ba + b$a . $b
ÉgalitĂ© strictea == ba === ba.equals(b)$a === $b
Coalescence de nulla or ba ?? b(aucun)$a ?? $b
AccÚs sûr au membre(aucun)a?.b(aucun)$a?->b
Lambdalambda x: x * 2x => x * 2x -> x * 2fn($x) => $x * 2
Dictionnaire littéral{'k': 1}{k: 1}Map.of("k", 1)['k' => 1]
Interpolationf"{x}"`${x}`(aucune)"{$x}"
Membre d’instanceobj.nameobj.nameobj.name$obj->name
Membre statiqueCls.nameCls.nameCls.nameCls::$name
Ternairea if c else bc ? a : bc ? 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 haute commode dont chaque tiroir porte un nom de fonction PHP dans un lettrage différent, certains avec des tirets bas et d'autres sans, et un petit éléphant tenant une lampe torche qui éclaire le bon tiroir

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 passezparamĂštre int, mode coercitifparamĂštre int, mode strict
121212
"12"12TypeError
"12abc"TypeErrorTypeError
12.012TypeError
1.51, déprécié depuis 8.1TypeError
true1TypeError
nullTypeError (?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.

Un portail avec deux voies vers une fonction. Sur la voie souple, un petit Ă©lĂ©phant remodĂšle une chaĂźne 12 en nombre 12 avant de la laisser passer. Sur la voie stricte, un Ă©lĂ©phant sĂ©vĂšre tient un panneau stop devant la mĂȘme chaĂźne

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_types n’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.

Une ligne de code avec un docblock au-dessus. Le moteur PHP, dessinĂ© en Ă©lĂ©phant, ne lit que la ligne de code. Un second personnage avec une loupe lit le docblock et hoche la tĂȘte

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.

Un tableau confié à une fonction : la fonction reçoit une photocopie de la feuille pendant que l'original reste intact sur le bureau de l'appelant ; ce n'est que lorsque la fonction écrit sur sa copie que les deux feuilles diffÚrent vraiment

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.

À gauche, une caisse ouverte Ă©tiquetĂ©e array oĂč l'on a jetĂ© n'importe quoi ; Ă  droite, une boĂźte Ă©tiquetĂ©e avec une fente typĂ©e sur le dessus qui n'accepte que des piĂšces en forme d'Order, avec un petit compteur et une poignĂ©e sur le cĂŽtĂ©

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 par array_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->send sans 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.

Une closure en cours de crĂ©ation prend une photo de la variable rate qui affiche 0.2 ; ensuite, le rate extĂ©rieur passe Ă  0.5 sur le bureau, mais la closure tient toujours la photo oĂč on lit 0.2

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.

À gauche : deux variables qui tiennent deux boĂźtes sĂ©parĂ©es Ă©tiquetĂ©es array, chacune avec son propre contenu. À droite : deux variables qui tiennent des ficelles attachĂ©es Ă  une seule boĂźte Ă©tiquetĂ©e object, de sorte que les deux tirent sur la mĂȘme chose

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).

Une petite usine avec trois casiers étiquetés Draft, Published et Archived. Une chaßne 'published' arrive sur un tapis roulant par une porte marquée from et est dirigée vers le casier Published ; une chaßne 'deleted' arrive et est renvoyée avec un panneau null

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.

Un arbre avec Throwable à la racine qui se sépare en deux branches : Error, dont les feuilles sont TypeError, ValueError et UnhandledMatchError avec une petite icÎne d'engrenage, et Exception, dont les feuilles sont RuntimeException, InvalidArgumentException et une feuille étiquetée « les vÎtres »

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"
De petits papiers étiquetés warning, notice et deprecated tombent d'en haut dans un entonnoir. De son bec sort une seule enveloppe propre, tamponnée du mot exception, qui atterrit dans un bloc try dessiné comme une boßte

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.

Un petit Ă©lĂ©phant lit un bout de papier oĂč est Ă©crit App\Billing\Invoice, puis suit une ligne pointillĂ©e le long d'un classeur dont les tiroirs sont Ă©tiquetĂ©s App, Billing, Invoice.php, et ouvre le dernier

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.

Deux panneaux. À gauche, Ă©tiquetĂ© install : un Ă©lĂ©phant lit un registre cadenassĂ© et empile des boĂźtes exactement comme listĂ©. À droite, Ă©tiquetĂ© update : le mĂȘme Ă©lĂ©phant consulte un tableau d'affichage couvert d'annonces de versions, choisit de nouvelles boĂźtes, puis réécrit le registre

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 $_GET et header() 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;
Le mot café dessiné deux fois : en haut, cinq cases d'octets séparées, l'accent à cheval sur deux d'entre elles, mesurées par une rÚgle ordinaire étiquetée strlen ; en bas, quatre tuiles de caractÚres mesurées par une rÚgle étiquetée mb_strlen

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.

À gauche : une page de calendrier Ă©tiquetĂ©e DateTime avec une flĂšche qui se replie sur elle-mĂȘme, la mĂȘme page raturĂ©e avec une nouvelle date et un visage inquiet ; Ă  droite : une page Ă©tiquetĂ©e DateTimeImmutable qui reste intacte pendant qu'une page neuve avec la nouvelle date apparaĂźt Ă  cĂŽtĂ©

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.

Une enveloppe Ă©tiquetĂ©e request s'ouvre sur quatre bacs Ă©tiquetĂ©s GET, POST, COOKIE et SERVER, qui alimentent un script dessinĂ© comme une page ; la sortie imprimĂ©e du script coule dans une seconde enveloppe Ă©tiquetĂ©e response, avec un petit autocollant d'en-tĂȘte collĂ© avant le corps

É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 RequestInterface et ResponseInterface, 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.

Des anneaux concentriques dessinĂ©s comme un oignon coupĂ© en deux, Ă©tiquetĂ©s de l'extĂ©rieur vers l'intĂ©rieur : logging, auth, CORS, et au centre une petite boĂźte Ă©tiquetĂ©e handler ; une flĂšche Ă©tiquetĂ©e request entre par la gauche Ă  travers chaque anneau et une flĂšche Ă©tiquetĂ©e response ressort Ă  droite Ă  travers les mĂȘmes anneaux

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 le Transliterator d’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.

Un petit Ă©lĂ©phant tient un Ă©cran Ă  rayons X au-dessus d'une pile de fichiers PHP ; Ă  travers l'Ă©cran, une ligne pointillĂ©e suit une valeur d'un fichier Ă  l'autre et se termine sur une marque rouge, lĂ  oĂč un null atteint un appel de mĂ©thode

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.

Panneau de gauche : un Ă©lĂ©phant jongleur qui maintient de nombreuses balles en l'air, Ă©tiquetĂ© boucle d'Ă©vĂ©nements. Panneau de droite : une rangĂ©e d'Ă©lĂ©phants qui tiennent chacun calmement une balle, Ă©tiquetĂ©e pool de processus. Les deux panneaux ont le mĂȘme nombre de balles

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.

Une barre horizontale qui montre une requĂȘte web comme une frise chronologique. Une fine tranche Ă  gauche est Ă©tiquetĂ©e PHP, puis un long segment Ă©tiquetĂ© base de donnĂ©es, puis un segment moyen Ă©tiquetĂ© appel HTTP, puis une fine tranche Ă©tiquetĂ©e PHP Ă  nouveau. Un petit Ă©lĂ©phant pointe le long segment base de donnĂ©es avec une loupe

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.

Deux colonnes de code sur un tableau blanc. Colonne de gauche, barrée à l'encre : mysql_query, array(), require_once, global. Colonne de droite, en bleu : PDO, crochets, autoload de Composer, injection par le constructeur. Un petit éléphant tient le marqueur

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() et create_function(), supprimĂ©es en 8.0. Utilisez foreach et les closures.
  • Le cast (unset), supprimĂ© en 8.0.
  • Les paramĂštres implicitement nullables, Foo $x = null sans le ?, dĂ©prĂ©ciĂ©s en 8.4. Écrivez ?Foo $x = null.
  • split(), supprimĂ©e en 7.0. Utilisez explode() ou preg_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.

ConceptPythonJavaScriptJavaPHP
Variablex = 1let x = 1int x = 1;$x = 1;
ConstanteX = 1 (convention)const X = 1static final int X = 1;const X = 1;
Interpolation de chaĂźnef"hi {name}"`hi ${name}`"hi " + name"hi {$name}"
Concaténationa + ba + ba + b$a . $b
ChaĂźne multiligne"""..."""`...`"""..."""<<<TXT ... TXT;
Division entiĂšrea // bMath.trunc(a / b)a / bintdiv($a, $b)
Puissancea ** ba ** bMath.pow(a, b)$a ** $b
ÉgalitĂ© strictea == ba === ba.equals(b)$a === $b
Coalescence de nulla if a is not None else ba ?? bOptional.ofNullable(a).orElse(b)$a ?? $b
Ternairea if c else bc ? a : bc ? 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 listexs.append(v)xs.push(v)xs.add(v)$xs[] = $v;
Lecture avec valeur par défautd.get("k", 0)d.k ?? 0d.getOrDefault("k", 0)$d['k'] ?? 0
Longueurlen(xs)xs.lengthxs.size()count($xs)
Longueur d’une chaünelen(s)s.lengths.length()mb_strlen($s)
Tranchexs[1:3]xs.slice(1, 3)xs.subList(1, 3)array_slice($xs, 1, 2)
Parcourir une listefor v in xs:for (const v of xs)for (var v : xs)foreach ($xs as $v)
Parcourir un dictionnairefor 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)
Lambdalambda x: x * 2x => x * 2x -> x * 2fn($x) => $x * 2
Capture des closurespar référencepar référenceeffectivement finalepar valeur (use ($x) ou fn)
Argument par défautdef f(x=1):function f(x = 1)surchargefunction f(int $x = 1)
Argument nomméf(x=1)f({x: 1})aucunf(x: 1)
Variadiquedef f(*xs):function f(...xs)void f(int... xs)function f(int ...$xs)
Classeclass A:class A {}class A {}class A {}
Constructeurdef __init__(self, x):constructor(x) {}A(int x) {}public function __construct(public int $x) {}
Membre d’instanceself.xthis.xthis.x$this->x
Membre statiqueA.xA.xA.xA::$x
InterfaceProtocolaucune (TS : interface)interface A {}interface A {}
ÉnumĂ©rationclass C(Enum):aucune (TS : enum)enum C { A, B }enum C { case A; case B; }
Rattraper une exceptionexcept E as e:catch (e)catch (E e)catch (E $e)
Navigation sûreaucunea?.bOptional.map$a?->b
ChaĂźne vers entierint(s)parseInt(s)Integer.parseInt(s)(int) $s
Test de typeisinstance(x, A)x instanceof Ax instanceof A$x instanceof A
Afficherprint(x)console.log(x)System.out.println(x)echo $x;
Importfrom a import Bimport { B } from 'a'import a.B;use A\B;
Gestionnaire de paquetspip, pyproject.tomlnpm, package.jsonMaven, pom.xmlComposer, composer.json
Lanceur de testspytestJest, VitestJUnitPHPUnit, Pest
Formateurblack, ruffPrettiergoogle-java-formatPHP-CS-Fixer, PHP_CodeSniffer
Analyse statiquemypy, pyrighttscle compilateurPHPStan, Psalm
Exécuter un scriptpython a.pynode a.jsjava A.javaphp a.php
REPLpythonnodejshellphp -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.
  • mixed et static comme types.
  • throw comme 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' vaut false.
  • Virgule finale autorisĂ©e dans les listes de paramĂštres.
  • Les fonctions internes lĂšvent TypeError et ValueError au lieu d’émettre un avertissement et de renvoyer null ou false.

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.
  • new dans 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, false et null autonomes.
  • 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\Randomizer et l’extension random.
  • 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 readonly peuvent ĂȘ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.
  • new sans 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, via Pdo::connect().
  • request_parse_body() pour les corps de formulaire quelle que soit la mĂ©thode HTTP.
  • ParamĂštres implicitement nullables (Foo $x = null sans ?) dĂ©prĂ©ciĂ©s.
  • exit et die sont dĂ©sormais des fonctions.

PHP 8.5, novembre 2025

  • OpĂ©rateur pipe : $slug = $title |> trim(...) |> strtolower(...);.
  • clone avec 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.