Chapitre 2.2 — Le système de types de PHP
Partie 2 : PHP moderne — Chapitre 2/8 Version de référence : PHP 8.4 pour Symfony 7.4.
⏱️ TL;DR
- Mettez toujours
declare(strict_types=1);en tête de chaque fichier : sans ça, PHP « convertit » silencieusement les types ("3"devient3). Avec, il refuse et lève uneTypeError.- Types de base :
int,float,string,bool,array,object,iterable,callable.- Types spéciaux :
mixed(n’importe quoi),void(ne retourne rien),never(ne revient jamais),null,self,static,parent.- Nullable :
?Booking=Booking|null. Union :int|string. Intersection :Countable&Traversable. DNF (8.2) :(A&B)|null.- Différence majeure avec TypeScript : PHP est nominal (deux classes identiques mais de noms différents sont incompatibles) là où TS est structurel ; et les types PHP sont vérifiés à l’exécution, pas effacés.
- Pas de génériques natifs :
Booking[]se documente via PHPStan (analyse statique), pas via le moteur.
🎯 Objectifs
- Écrire des signatures typées correctes (paramètres, retours, propriétés).
- Comprendre
strict_typeset pourquoi il est non négociable. - Manier nullable, union, intersection, et les types spéciaux (
never,mixed,void). - Connaître les deux pièges qui surprennent un dev TS : typage nominal et absence de génériques.
1. declare(strict_types=1) : la première ligne de tout fichier
PHP a un mode « coercitif » par défaut, hérité de son passé : si une fonction attend un int et reçoit "3", il convertit sans broncher. C’est une source de bugs. La parade est une directive, par fichier, tout en haut :
<?php
declare(strict_types=1);
// … le reste du fichierAvec strict_types=1, PHP exige le bon type et lève une TypeError sinon (sauf la promotion int → float, tolérée car sûre).
declare(strict_types=1);
function double(int $n): int { return $n * 2; }
double(3); // 6
double("3"); // TypeError : "3" (string) n'est pas un int⚠️ Piège :
strict_typess’applique au fichier appelant, fichier par fichier, et uniquement aux appels de fonctions/méthodes — pas aux valeurs déjà stockées. La règle simple et sans exception : toujours commencer vos fichiers pardeclare(strict_types=1);. Symfony et tous les outils modernes le supposent. (Le MakerBundle l’ajoute automatiquement au code généré.)
💡 Pour un dev Next.js : c’est l’équivalent d’activer
"strict": truedanstsconfig.json— sauf qu’ici ce n’est pas global mais par fichier, et que la vérification a lieu au runtime. Considérez-le comme obligatoire, exactement comme vous n’écririez pas de TS sansstrict.
2. Où placer les types
Trois emplacements, tous avant ce qu’ils qualifient (contrairement à TS où le type suit la variable) :
declare(strict_types=1);
class SlotService
{
// 1) Propriété typée
private int $capacity = 10;
// 2) Paramètres typés + 3) type de retour
public function remaining(int $booked): int
{
return $this->capacity - $booked;
}
}Notez : private int $capacity (le type précède le nom), remaining(int $booked): int (paramètre typé, retour après les :). En TS vous écririez capacity: number et remaining(booked: number): number — même information, ordre inversé.
3. Les types de base
| Catégorie | Types | Équivalent TS |
|---|---|---|
| Scalaires | int, float, string, bool | number (×2), string, boolean |
| Composés | array, object, iterable, callable | Array/Record, object, Iterable, Function |
| Classe/interface | Booking, DateTimeImmutable, Stringable | la classe/interface elle-même |
| Spéciaux | mixed, void, never, null, self, static, parent | unknown/any, void, never, null, this… |
Quelques précisions utiles :
intvsfloat: PHP distingue les deux (là où JS n’a qu’unnumber).1est unint,1.0unfloat.iterable=array|Traversable: tout ce qui se parcourt enforeach(utile pour accepter aussi bien un tableau qu’un générateur).callable: n’importe quoi d’appelable (fonction, closure,[$obj, 'méthode']).object: n’importe quel objet.mixed: « n’importe quoi » (commeunknownen TS, mais permissif commeany). À éviter sauf nécessité.
4. Nullable, union, intersection
4.1 Nullable
Un ? devant un type le rend nullable :
public function findBooking(int $id): ?Booking // retourne Booking OU null
{
return $this->repository->find($id);
}?Booking est un raccourci pour l’union Booking|null. Les deux écritures sont valides ; ?Type est privilégié quand il n’y a qu’un type + null.
4.2 Types union (PHP 8.0)
Un paramètre ou un retour peut accepter plusieurs types :
function normalizeId(int|string $id): string
{
return (string) $id; // caste explicitement vers string
}4.3 Types intersection (PHP 8.1)
L’objet doit satisfaire tous les types à la fois (utile avec des interfaces) :
function iterateCount(Countable&Traversable $collection): void
{
// $collection est à la fois comptable ET parcourable
}4.4 Types DNF (PHP 8.2)
Combinaison des deux, en forme normale disjonctive — des intersections dans une union, avec parenthèses :
function handle((Countable&Traversable)|null $items): void { /* … */ }💡 Pour un dev Next.js : union (
int|string) = union TS (number | string). Intersection (A&B) = intersection TS (A & B).?T=T | null. La grammaire est presque identique ; seul le?en préfixe (au lieu de| null) est une commodité PHP.
5. Les types spéciaux qui comptent
void: la fonction ne retourne rien d’exploitable (return;nu autorisé, mais pasreturn $x;).never(8.1) : la fonction ne revient jamais — elle lève toujours une exception ou termine le script. Le moteur et l’analyse statique en tirent parti.function fail(string $message): never { throw new \RuntimeException($message); }self/static: types de retour pour le chaînage.static(retour tardif) est préféré pour renvoyer « une instance de la classe réelle appelée » (utile pour les fluent interfaces et les entités).selfvsstatic:self= la classe où c’est écrit ;static= la classe réellement instanciée (late static binding). Pour un retour chaînable dans une classe héritable, utilisezstatic.
6. Deux vérités qui surprennent un dev TypeScript
6.1 PHP est nominal, TypeScript est structurel
C’est le faux-ami le plus important. En TypeScript, deux types ayant la même forme sont interchangeables (typage structurel) :
// TypeScript : ceci COMPILE
interface A { name: string }
interface B { name: string }
const a: A = { name: "x" };
const b: B = a; // OK : même structureEn PHP, ce qui compte est le nom du type (typage nominal). Deux classes avec exactement les mêmes propriétés mais des noms différents sont incompatibles :
final class A { public function __construct(public string $name) {} }
final class B { public function __construct(public string $name) {} }
function takeB(B $b): void {}
takeB(new A('x')); // TypeError : A n'est pas un B, même structure identiquePour qu’un objet soit accepté « par contrat », il faut explicitement implémenter une interface commune. PHP ne devine pas la compatibilité par la forme.
⚠️ Piège : ne cherchez pas le duck typing structurel de TS en PHP. Si vous voulez qu’un service accepte « tout objet qui sait faire X », définissez une interface
Xet faites implémenter cette interface aux classes concernées. C’est plus verbeux, mais c’est le mécanisme de découplage de Symfony (on injecte des interfaces, pas des classes — Partie 5).
6.2 Pas de génériques natifs
PHP n’a pas de génériques dans le langage. Un array de Booking est juste… un array. Le moteur ne connaît pas array<int, Booking>. On documente la forme avec des annotations PHPStan/Psalm dans un commentaire, analysées statiquement (jamais à l’exécution) :
/**
* @param Booking[] $bookings liste de réservations
* @return array<string, int> nb de réservations par statut
*/
public function countByStatus(array $bookings): array
{
// …
}Ces annotations @param/@return ne sont pas de la simple documentation : l’outil PHPStan (Partie 14) les vérifie et signale les incohérences, exactement comme le compilateur TS le ferait. On récupère donc l’essentiel de la sécurité des génériques, via un outil à côté du langage.
💡 Pour un dev Next.js : mentalement,
Booking[]en annotation PHPStan ≈Booking[]en TS. La différence : en TS c’est le compilateur intégré qui vérifie ; en PHP c’est PHPStan lancé en CI. Le résultat pratique est très proche.
7. Casts et vérifications de type
- Cast explicite :
(int) $x,(string) $n,(bool) $v,(array) $obj. - Vérifier un type :
is_int($x),is_string($x),$obj instanceof Booking. - Fonctions de conversion sûres : préférez des conversions explicites plutôt que compter sur la coercition.
$id = (int) $request->query->get('id'); // '42' → 42
if ($value instanceof BookingStatus) { /* … */ }⚠️ Piège : le type juggling de PHP (comparaisons
==laxistes) est célèbre pour ses surprises (0 == "a"a longtemps ététrueavant PHP 8 !). PHP 8 a assaini ces règles, mais la parade reste : utilisez===(strict) et castez explicitement. Ne comptez jamais sur==.
✏️ Exercices
Exercice 1. Typez complètement cette méthode : elle prend une liste de Booking et un statut, et renvoie le nombre de réservations ayant ce statut.
public function countWithStatus($bookings, $status)
{
// …
}✅ Solution
/** @param Booking[] $bookings */
public function countWithStatus(array $bookings, BookingStatus $status): int
{
$n = 0;
foreach ($bookings as $b) {
if ($b->status === $status) {
$n++;
}
}
return $n;
}array typé au niveau moteur + @param Booking[] pour PHPStan ; BookingStatus (enum) pour le statut ; retour int. Comparaison en ===.
Exercice 2. Ce code TypeScript compile grâce au typage structurel. L’équivalent PHP « naïf » échoue. Pourquoi, et comment le corriger proprement ?
interface HasEmail { email: string }
function notify(x: HasEmail) {}
class Customer { constructor(public email: string) {} }
notify(new Customer("a@b.c")); // OK en TS✅ Solution
En PHP, le typage est nominal : Customer n’est pas un HasEmail juste parce qu’il a une propriété email. Il faut implémenter explicitement l’interface :
interface HasEmail { public function getEmail(): string; }
final class Customer implements HasEmail
{
public function __construct(private string $email) {}
public function getEmail(): string { return $this->email; }
}
function notify(HasEmail $x): void {}
notify(new Customer('a@b.c')); // OK : Customer implements HasEmailLa compatibilité vient du contrat déclaré (implements HasEmail), pas de la forme.
Exercice 3. Quel type de retour pour : (a) une fonction qui lève toujours une exception ; (b) une méthode qui modifie l’objet et ne renvoie rien ; (c) une méthode chaînable renvoyant l’instance courante d’une classe héritable ?
✅ Solution
(a) never (ne revient jamais). (b) void (ne retourne rien). (c) static (renvoie une instance de la classe réellement appelée, pour permettre le chaînage même dans les sous-classes).
🧠 Quiz de révision
1. Que fait declare(strict_types=1) et où le place-t-on ?
declare(strict_types=1) et où le place-t-on ?Il désactive la coercition automatique des types : PHP exige le type exact et lève une TypeError sinon. On le place en première instruction de chaque fichier PHP.
2. Qu’est-ce que ?Booking ?
?Booking ?Un raccourci pour l’union Booking|null : la valeur est soit une instance de Booking, soit null.
3. Différence entre never et void ?
never et void ?void : la fonction se termine normalement mais ne renvoie rien d’exploitable. never : la fonction ne revient jamais (elle lève toujours une exception ou termine le script).
4. PHP est-il à typage nominal ou structurel ? Conséquence pratique ?
Nominal : la compatibilité dépend du nom du type, pas de la forme. Conséquence : pour qu’une classe soit acceptée « par contrat », elle doit implémenter explicitement l’interface attendue (pas de duck typing structurel comme en TS).
5. Comment type-t-on un array de Booking puisqu’il n’y a pas de génériques natifs ?
array de Booking puisqu’il n’y a pas de génériques natifs ?Avec une annotation PHPStan dans un commentaire : /** @param Booking[] $x */ (ou array<int, Booking>), vérifiée statiquement par PHPStan en CI. Le moteur, lui, ne voit qu’un array.
Prochain chapitre : 2.3 — Les enums — de vrais types énumérés, l’équivalent (en mieux) des unions de littéraux TypeScript.