PHP 8 : une révolution pour le langage
PHP 8 apporte des améliorations majeures qui modernisent le langage et améliorent les performances grâce au compilateur JIT.
Sorti en novembre 2020, PHP 8.0 a ouvert une série de versions annuelles qui ont profondément changé la façon d'écrire du PHP : typage plus expressif, syntaxe plus concise, objets immuables, énumérations natives. Le code Symfony ou Laravel moderne s'appuie massivement sur ces nouveautés, et les bibliothèques récentes exigent souvent PHP 8.2 ou plus. Cet article passe en revue les fonctionnalités qui ont le plus d'impact au quotidien, version par version, avec pour chacune son intérêt concret et ses pièges.
Un point de contexte avant de commencer : chaque version mineure de PHP reçoit deux ans de support actif puis deux ans de correctifs de sécurité. Rester sur une version en fin de vie n'est pas seulement se priver de fonctionnalités, c'est surtout s'exposer à des failles qui ne seront plus corrigées. Consultez la page des versions supportées pour planifier vos montées de version.
Named Arguments
Les arguments nommés permettent de passer un paramètre par son nom plutôt que par sa position. On n'a plus besoin de répéter toutes les valeurs par défaut pour modifier seulement le dernier paramètre, et l'appel devient auto-documenté :
// Avant
htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);
// PHP 8
htmlspecialchars($string, double_encode: false);
Revers de la médaille : le nom des paramètres fait désormais partie de l'API publique d'une fonction. Renommer un paramètre dans une bibliothèque peut casser le code de ses utilisateurs. Si vous maintenez un package, traitez ces renommages comme des changements incompatibles.
Promotion des propriétés, union types et opérateur nullsafe
PHP 8.0 a aussi introduit trois fonctionnalités que l'on utilise dans presque chaque fichier. La promotion des propriétés du constructeur supprime le code répétitif de déclaration et d'affectation. Les union types (int|string) décrivent précisément les valeurs acceptées. L'opérateur nullsafe ?-> interrompt une chaîne d'appels dès qu'un maillon vaut null :
final class InvoiceService
{
public function __construct(
private InvoiceRepository $invoices,
private ?LoggerInterface $logger = null,
) {}
public function findReference(int|string $id): ?string
{
$invoice = $this->invoices->find($id);
// null si la facture, le client ou l'adresse n'existe pas
return $invoice?->getCustomer()?->getAddress()?->getCountryCode();
}
}
À cela s'ajoutent des fonctions attendues depuis longtemps (str_contains(), str_starts_with(), str_ends_with()), le type mixed, et throw utilisable comme expression, par exemple $value ?? throw new InvalidArgumentException().
Match Expression
match est une version plus sûre de switch : c'est une expression qui retourne une valeur, la comparaison est stricte (===), il n'y a pas de break à oublier, et si aucune branche ne correspond sans default, PHP lève une UnhandledMatchError au lieu de continuer silencieusement.
$status = match($code) {
200 => 'OK',
301 => 'Redirect',
404 => 'Not Found',
500 => 'Server Error',
default => 'Unknown',
};
Attention à la comparaison stricte : si $code provient d'une chaîne ('200'), aucune branche entière ne correspondra. Convertissez la valeur en amont. Plusieurs valeurs peuvent partager une branche en les séparant par des virgules (301, 302 => 'Redirect'), et match(true) permet d'évaluer des conditions arbitraires.
Enums (PHP 8.1)
Les énumérations remplacent les constantes de classe et les chaînes « magiques ». Une enum « backed » associe chaque cas à une valeur scalaire, ce qui la rend facile à stocker en base ou à sérialiser en JSON. Elle peut porter des méthodes et implémenter des interfaces :
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
public function label(): string {
return match($this) {
self::Active => 'Actif',
self::Inactive => 'Inactif',
self::Pending => 'En attente',
};
}
}
Les méthodes générées automatiquement couvrent les usages courants :
$status = Status::from('active'); // Status::Active
$maybe = Status::tryFrom('archived'); // null au lieu d'une exception
$all = Status::cases(); // liste de tous les cas
function activate(User $user): void
{
// le type garantit une valeur valide, sans validation manuelle
$user->setStatus(Status::Active);
}
Doctrine ORM (via enumType sur une colonne), le Serializer et les formulaires Symfony (EnumType) gèrent nativement les enums. Utilisez from() quand une valeur invalide est un bug, et tryFrom() pour les entrées utilisateur.
Fibers (PHP 8.1)
Les Fibers sont des fonctions interruptibles : elles peuvent suspendre leur exécution et la reprendre plus tard, en conservant leur pile d'appels. Elles ne créent pas de parallélisme (tout s'exécute dans un seul thread), mais elles permettent d'écrire du code asynchrone qui ressemble à du code synchrone.
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('première pause');
echo "Reçu : $value\n";
});
$result = $fiber->start(); // 'première pause'
$fiber->resume('données');
En pratique, on manipule rarement les Fibers directement. Ce sont des briques bas niveau utilisées par des bibliothèques comme AMPHP ou la boucle d'événements Revolt, qui orchestrent les suspensions pendant les entrées/sorties réseau.
Autres ajouts de PHP 8.1
- Syntaxe callable de première classe :
$fn = strlen(...);crée uneClosureà partir de n'importe quelle fonction ou méthode. - Type de retour
neverpour les fonctions qui lèvent toujours une exception ou terminent le script. newdans les initialiseurs : un objet peut servir de valeur par défaut d'un paramètre, comme dans l'exempleUserci-dessous.array_is_list()pour vérifier qu'un tableau a des clés 0, 1, 2… consécutives.
Readonly Properties (PHP 8.1) et classes readonly (PHP 8.2)
Une propriété readonly ne peut être initialisée qu'une seule fois, depuis la portée de la classe. C'est idéal pour les value objects, les DTO et les événements, qui ne doivent pas changer après leur création :
class User {
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly DateTimeImmutable $createdAt = new DateTimeImmutable(),
) {}
}
PHP 8.2 permet de déclarer toute la classe readonly, ce qui évite de répéter le mot-clé sur chaque propriété. Pour « modifier » un tel objet, on en crée une copie avec la nouvelle valeur (méthodes with…()) :
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
public function withAmount(int $amount): self
{
return new self($amount, $this->currency);
}
}
Limites à connaître : une propriété readonly doit être typée, n'accepte pas de valeur par défaut dans sa déclaration, et l'immuabilité est superficielle. Si la propriété contient un objet mutable, cet objet peut toujours être modifié. D'où l'intérêt de DateTimeImmutable plutôt que DateTime. PHP 8.2 a aussi apporté les types DNF ((Countable&ArrayAccess)|null) et a déprécié la création dynamique de propriétés.
PHP 8.3 et 8.4 : les dernières nouveautés
PHP 8.3 a introduit les constantes de classe typées, l'attribut #[\Override] (PHP lève une erreur si la méthode ne surcharge en réalité rien, par exemple après un renommage dans la classe parente) et la fonction json_validate(). PHP 8.4 a ajouté deux fonctionnalités importantes pour la modélisation : les property hooks, qui attachent une logique de lecture ou d'écriture directement à une propriété, et la visibilité asymétrique, qui permet par exemple une lecture publique et une écriture privée :
final class Product
{
public const string DEFAULT_CURRENCY = 'EUR'; // PHP 8.3
public private(set) string $status = 'draft'; // PHP 8.4
public int $priceInCents { // PHP 8.4
set(int $value) {
if ($value < 0) {
throw new InvalidArgumentException('Le prix doit être positif.');
}
$this->priceInCents = $value;
}
}
public string $formattedPrice {
get => number_format($this->priceInCents / 100, 2).' '.self::DEFAULT_CURRENCY;
}
public function __construct(int $priceInCents)
{
$this->priceInCents = $priceInCents;
}
public function publish(): void
{
$this->status = 'published';
}
}
PHP 8.4 permet aussi d'enchaîner un appel directement après new sans parenthèses supplémentaires (new Foo()->bar()) et ajoute des fonctions comme array_find() et array_any(). PHP 8.5, sorti en novembre 2025, poursuit dans cette direction avec notamment l'opérateur pipe |>.
Performances JIT
Le compilateur JIT améliore les performances de 2 à 3x pour les opérations intensives en CPU. Activez-le dans php.ini :
opcache.jit=1255
opcache.jit_buffer_size=100M
Il faut cependant nuancer : le JIT compile le bytecode en code machine, ce qui profite surtout aux calculs purs (traitement d'image, algorithmes mathématiques, parsers). Une application web classique passe l'essentiel de son temps à attendre la base de données, le réseau ou le système de fichiers, et le gain y est généralement faible. OPcache, lui, reste indispensable dans tous les cas, et le JIT ne fonctionne que s'il est activé. Les valeurs tracing et function sont des alias plus lisibles pour opcache.jit, et depuis PHP 8.4 le JIT est désactivé par défaut (opcache.jit=disable). Mesurez avec votre propre charge avant de l'activer en production.
Checklist pour migrer vers PHP 8.x
- Lisez les guides de migration officiels de chaque version sur php.net, en particulier les sections « incompatibilités » et « dépréciations ».
- Activez l'affichage des dépréciations dans l'environnement de test (
error_reporting=E_ALL) et corrigez-les avant de changer de version. - Lancez une analyse statique (PHPStan ou Psalm) et automatisez les réécritures répétitives avec Rector.
- Mettez à jour les dépendances Composer et vérifiez la contrainte
"php"danscomposer.json. - Faites tourner la suite de tests complète sur la nouvelle version en CI avant de basculer la production.
Adopter ces fonctionnalités progressivement, fichier par fichier, suffit à rendre une base de code plus sûre et plus lisible : les types attrapent les erreurs plus tôt, les enums et les objets readonly éliminent des catégories entières de bugs, et match rend les branches exhaustives explicites.