Le systĂšme de types
PHP est typĂ© dynamiquement, et chaque type que vous Ă©crivez est vĂ©rifiĂ© Ă lâexĂ©cution, pas Ă la compilation comme en Java, et pas effacĂ© comme en TypeScript. Un paramĂštre dĂ©clarĂ© int reçoit un int, sinon lâappel lĂšve une TypeError, Ă chaque fois, y compris en production. Tout le modĂšle tient dans cette phrase, et la nuance se cache dans ce que « reçoit un int » veut dire, parce que cette nuance dĂ©pend dâun interrupteur.
Les types que vous pouvez écrire
<?php
declare(strict_types=1);
function describe(int|float $n, ?string $label, bool $verbose = false): string
{
return ($label ?? 'value') . ': ' . $n . ($verbose ? ' (verbose)' : '');
}
echo describe(3, null), PHP_EOL; // value: 3
echo describe(2.5, 'pi-ish', true), PHP_EOL; // pi-ish: 2.5 (verbose)
Les scalaires sont int, float, string et bool. Les types composĂ©s sont array, object, callable, iterable, et tout nom de classe ou dâinterface. Par-dessus, PHP a une poignĂ©e de types qui nâont de sens quâĂ une position : void et never comme types de retour (never signifie que la fonction lĂšve une exception ou quitte le programme), static comme type de retour dâune mĂ©thode fluide, self pour la classe courante, mixed quand vous acceptez vraiment nâimporte quoi, et null, true, false comme types autonomes depuis PHP 8.2.
Ces types se combinent : ?string est string|null, int|string est une union (PHP 8.0), Countable&Traversable est une intersection (PHP 8.1) qui exige que lâobjet implĂ©mente les deux, et (Countable&Traversable)|null mĂ©lange les deux formes (PHP 8.2). La grammaire sâarrĂȘte lĂ , sans gĂ©nĂ©riques, sans tuples et sans typage structurel.
Les types se posent sur les paramÚtres, les valeurs de retour, les propriétés, et depuis PHP 8.3 sur les constantes de classe :
<?php
declare(strict_types=1);
final class Temperature
{
public const string UNIT = 'C';
public function __construct(
public readonly float $degrees,
) {
}
}
$t = new Temperature(21.5);
var_dump($t->degrees); // float(21.5)
Les paramÚtres et propriétés non typés existent toujours et signifient mixed, mais le code moderne ne les laisse pas sans type.
Ce que int accepte vraiment
Câest ici que PHP diffĂšre de tout le reste. Sans strict_types, PHP convertit les arguments scalaires vers le type dĂ©clarĂ© quand il le peut. Passez la chaĂźne "12" Ă un paramĂštre int et la fonction reçoit lâentier 12 ; passez "12abc" et vous obtenez une TypeError ; passez 1.5 et vous obtenez 1 avec un avertissement de dĂ©prĂ©ciation.
| Vous passez | paramĂštre int, mode coercitif | paramĂštre int, mode strict |
|---|---|---|
12 | 12 | 12 |
"12" | 12 | TypeError |
"12abc" | TypeError | TypeError |
12.0 | 12 | TypeError |
1.5 | 1, déprécié depuis 8.1 | TypeError |
true | 1 | TypeError |
null | TypeError (?int lâaccepte) | TypeError (?int lâaccepte) |
La coercition ne concerne que les scalaires : un tableau nâest jamais transformĂ© en chaĂźne, ni un objet en entier. Une seule conversion reste permise mĂȘme en mode strict, celle dâun int lĂ oĂč un float est attendu, parce quâelle ne perd jamais dâinformation.
Lâinterrupteur est par fichier, et cĂŽtĂ© appelant
<?php
declare(strict_types=1);
function double(int $n): int
{
return $n * 2;
}
echo double(21), PHP_EOL; // 42
echo double('21'), PHP_EOL; // TypeError: must be of type int, string given
Supprimez la ligne declare et le second appel affiche 42. Cet interrupteur a trois propriĂ©tĂ©s, et chacune surprend quelquâun.
Il est par fichier. Il nâexiste ni rĂ©glage global, ni option dans php.ini, ni paramĂštre Ă lâĂ©chelle du projet : chaque fichier annonce son propre mode, et un fichier sans la ligne est en mode coercitif.
Il sâapplique aux appels faits depuis le fichier, pas aux fonctions qui y sont dĂ©finies. Si double() vit dans un fichier strict et quâun fichier coercitif lâappelle, double('21') convertit, parce que câest lâappelant qui dĂ©cide. Ce choix est voulu : lâauteur dâune bibliothĂšque ne peut pas imposer la rigueur au code qui lâappelle, et le code ancien continue de fonctionner quand il appelle une bibliothĂšque moderne.
Il couvre aussi les valeurs de retour. Une fonction en mode strict qui déclare : int et renvoie "42" lÚve une exception.
La rĂšgle pratique est courte : mettez declare(strict_types=1); en tĂȘte de chaque fichier que vous Ă©crivez, laissez votre outil de style de code lâimposer, et nây pensez plus.
strict_typesnâest pas « PHP avec les types activĂ©s », puisque PHP vĂ©rifie toujours les types. Lâinterrupteur dĂ©cide seulement si une chaĂźne qui ressemble Ă un nombre compte comme un nombre.
Convertir volontairement
Quand vous voulez une conversion, dites-le :
<?php
declare(strict_types=1);
var_dump((int) '42'); // int(42)
var_dump((int) '42 apples'); // int(42): a cast takes the leading digits
var_dump((int) 'apples'); // int(0)
var_dump((string) 3.0); // string(1) "3"
var_dump((bool) '0'); // bool(false): "0" is falsy, "0.0" is not
var_dump(intval('0x1A', 16)); // int(26)
Les transtypages ne lĂšvent jamais dâexception et nâĂ©mettent jamais dâavertissement : ils font de leur mieux avec ce quâils reçoivent. Cela en fait le bon outil pour une entrĂ©e utilisateur dĂ©jĂ validĂ©e, et le mauvais pour tout ce qui ne lâest pas. Pour vĂ©rifier avant de convertir, is_int(), is_string(), is_numeric() et leurs cousines renvoient des boolĂ©ens, et filter_var($x, FILTER_VALIDATE_INT) renvoie lâentier ou false.
Pour voir ce que vous tenez, var_dump() affiche le type et la valeur. get_debug_type() (PHP 8.0) renvoie le nom que vous Ă©cririez dans une dĂ©claration (int, string, App\User), lĂ oĂč lâancien gettype() renvoie integer et object.
Null et la bibliothĂšque standard
Vos propres fonctions refusent null pour un paramĂštre non nullable, dans les deux modes. Les fonctions natives sont plus tolĂ©rantes, et cette tolĂ©rance est en voie de disparition. strlen(null) renvoie 0 aujourdâhui avec un avertissement de dĂ©prĂ©ciation (depuis PHP 8.1), et la prochaine version majeure devrait lever une exception. Un code qui lit strlen($_GET['q']) sur un paramĂštre absent vit donc en sursis, alors que strlen($_GET['q'] ?? '') continuera de fonctionner.
La mĂȘme tolĂ©rance se voit dans lâautre sens. LĂ oĂč une fonction renvoyait false en cas dâĂ©chec, les plus rĂ©centes lĂšvent une ValueError (PHP 8.0), et celles qui renvoient encore false sont documentĂ©es ainsi. Quand la documentation dit string|false, testez false avec === et jamais avec if (!$result), parce que "0" et "" sont faux au sens large, eux aussi.
Les nombres, briĂšvement
Un int fait 64 bits sur toutes les plateformes que vous rencontrerez, et le dépassement ne boucle pas : un entier qui déborde devient un flottant, en silence.
<?php
declare(strict_types=1);
var_dump(PHP_INT_MAX + 1); // float(9.223372036854776E+18)
var_dump(0.1 + 0.2 === 0.3); // bool(false), as in every IEEE 754 language
var_dump(intdiv(7, 2), 7 / 2); // int(3), float(3.5)
La division produit toujours un flottant, sauf si les deux opĂ©randes sont des entiers et que le rĂ©sultat est exact. intdiv() donne la division entiĂšre, % est le modulo entier et fmod() celui des flottants. Pour la monnaie ou tout ce qui est dĂ©cimal, lâextension bcmath et sa classe BcMath\Number (PHP 8.4) font de lâarithmĂ©tique Ă prĂ©cision arbitraire.
OĂč sont passĂ©s les gĂ©nĂ©riques
Le langage nâen a pas. array est le type de tout tableau, quel que soit son contenu, et une classe Collection ne peut pas dire ce quâelle collectionne. LâĂ©cosystĂšme a rĂ©pondu avec des docblocks lus par les analyseurs statiques, et sur un projet maintenu cette rĂ©ponse a autant de poids quâun compilateur :
<?php
declare(strict_types=1);
/**
* @template T
* @param list<T> $items
* @param callable(T): bool $keep
* @return list<T>
*/
function keep(array $items, callable $keep): array
{
return array_values(array_filter($items, $keep));
}
/** @var list<int> $evens */
$evens = keep([1, 2, 3, 4], fn (int $n) => $n % 2 === 0);
PHP lit array et callable, tandis que PHPStan et Psalm lisent list<T> et callable(T): bool, en dĂ©duisent que $evens contient des entiers, et font Ă©chouer le build si vous passez des chaĂźnes. Avec list<int>, array<string, User>, non-empty-string ou int<1, max>, le vocabulaire est plus riche que le langage, et les deux outils sâaccordent sur lâessentiel. Tests, analyse statique et outillage montre comment en installer un.
Le piĂšge
La premiĂšre erreur est dâĂ©crire function f(int $n) dans un fichier sans strict_types, de passer "12" dans un test, de voir que ça marche, et dâen conclure que PHP ne vĂ©rifie rien, alors quâil a vĂ©rifiĂ© et converti dans la foulĂ©e. La seconde est lâinverse : ajouter declare(strict_types=1) Ă un fichier et sâattendre Ă ce que toute lâapplication devienne stricte, alors que seuls les appels de ce fichier ont changĂ©.
Les deux ont le mĂȘme remĂšde. La ligne va dans chaque fichier, une rĂšgle de style de code lâimpose, et un analyseur statique attrape au build les cas que le moteur attraperait en production.
Les tableaux ont Ă©tĂ© mentionnĂ©s trois fois dans ce chapitre sans un mot sur ce quâils sont, parce quâils ne sont pas ce que votre langage appelle un tableau. Les tableaux remet les choses en place.