Vous connaissez peut-être PHP par ses vieux réflexes : des variables non typées, des tableaux à tout faire, des fonctions qui acceptent n’importe quoi. PHP 8.4 propose une autre façon d’écrire — plus sûre, plus lisible, et comprise par les outils. Dans ce tutoriel, on prend la première brique du projet « Atelier », un gestionnaire de stock de pièces détachées, et on écrit la classe qui représente une pièce en exploitant les nouveautés du langage. À la fin, vous saurez tirer parti du typage strict, des property hooks, de la visibilité asymétrique et des fonctions de tableau récentes.
Article de référence : ce tutoriel fait partie du guide complet du PHP moderne. Pour la vue d’ensemble du parcours et du projet « Atelier », commencez par là.
Ce que vous allez apprendre
- Activer et comprendre le typage strict avec
declare(strict_types=1). - Déclarer des propriétés calculées avec les property hooks de PHP 8.4.
- Protéger l’écriture d’une propriété avec la visibilité asymétrique
private(set). - Chaîner un appel directement après
newsans parenthèses. - Filtrer et interroger un tableau avec
array_find(),array_any()etarray_all(). - Marquer du code obsolète avec l’attribut
#[\Deprecated].
Ce que vous allez construire
Une classe Piece complète et une petite collection d’inventaire en mémoire. À la fin, un script qui crée quelques pièces, calcule leur valeur de stock et répond à des questions du type « toutes les pièces sont-elles en stock ? » — le tout en PHP 8.4 idiomatique, prêt à être enrichi dans les tutoriels suivants.
Prérequis
- PHP 8.4 installé (vérifiez avec
php --version). Les property hooks et la visibilité asymétrique n’existent pas avant 8.4. - Un éditeur avec coloration PHP (VS Code + l’extension PHP, ou PhpStorm).
- Les bases de la syntaxe PHP : variables, fonctions, tableaux. Si vous savez écrire une fonction qui additionne deux nombres, vous êtes prêt.
- ⏱️ Temps estimé : ~35 minutes.
Étape 1 — Activer le typage strict
Avant toute chose, il faut comprendre la différence entre typage faible et typage strict. Par défaut, PHP convertit les valeurs pour les faire correspondre au type attendu : passer la chaîne "3" à une fonction qui attend un int fonctionne, PHP convertit silencieusement. C’est commode, mais cela masque des bugs. Le typage strict désactive ces conversions implicites : on doit passer exactement le bon type, sinon une TypeError est levée.
On l’active fichier par fichier, avec une déclaration qui doit être la toute première instruction du fichier, juste après la balise d’ouverture :
<?php
declare(strict_types=1);
namespace Atelier;
function prixTotal(int $quantite, float $prixUnitaire): float
{
return $quantite * $prixUnitaire;
}
echo prixTotal(4, 250.0); // 1000
echo prixTotal("4", 250.0); // TypeError en mode strict
Avec strict_types=1, le deuxième appel échoue immédiatement avec une TypeError : « Argument #1 must be of type int, string given ». Sans cette déclaration, PHP aurait converti "4" en 4 et renvoyé 1000 sans broncher. Le mode strict transforme donc une erreur potentielle, qui aurait pu se propager loin dans le programme, en échec net et localisé. Prenez l’habitude de mettre cette ligne en tête de chaque fichier : c’est le réglage de base du PHP moderne.
Point d’étape — Créez un fichier
test.phpavec le code ci-dessus et lancezphp test.php. Vous devez voir1000puis uneTypeErrorsur le second appel. Si les deux appels affichent1000, c’est que la lignedeclaren’est pas exactement la première instruction du fichier.
Étape 2 — Modéliser la pièce avec promotion de constructeur
Passons à la classe Piece. Une pièce a une référence, un nom, une catégorie, un prix et une quantité. Plutôt que de déclarer chaque propriété puis de la recopier dans le constructeur, PHP 8 permet la promotion de constructeur : on déclare les propriétés directement dans la signature du constructeur, ce qui supprime la répétition.
<?php
declare(strict_types=1);
namespace Atelier;
final class Piece
{
public function __construct(
public string $reference,
public string $nom,
public float $prix,
public int $quantite = 0,
) {}
}
$vis = new Piece('VIS-M6', 'Vis métaux M6', 250.0, 120);
echo $vis->reference; // VIS-M6
Chaque paramètre préfixé d’un modificateur de visibilité (public, private, protected) devient automatiquement une propriété de l’objet, initialisée avec la valeur reçue. Le mot-clé final empêche qu’on hérite de Piece ; pour un objet de données simple, c’est une bonne valeur par défaut qui clarifie l’intention. La pièce est créée en une ligne, et toutes ses propriétés sont typées.
Étape 3 — Rendre la référence immuable avec readonly et la visibilité asymétrique
La référence d’une pièce ne doit jamais changer après sa création : c’est son identité. PHP 8.1 a introduit readonly pour cela — une propriété en lecture seule, qu’on ne peut affecter qu’une fois, dans le constructeur. Mais on veut souvent un cas plus souple : une propriété qu’on peut lire de partout mais modifier seulement depuis l’intérieur de la classe. C’est exactement ce que résout la visibilité asymétrique, nouveauté de PHP 8.4.
<?php
declare(strict_types=1);
namespace Atelier;
final class Piece
{
public function __construct(
public readonly string $reference, // figée pour toujours
public string $nom,
public float $prix,
public private(set) int $quantite = 0, // lisible partout, modifiable en interne
) {}
public function entrerStock(int $nombre): void
{
$this->quantite += $nombre;
}
}
$vis = new Piece('VIS-M6', 'Vis métaux M6', 250.0, 120);
echo $vis->quantite; // 120 — lecture OK
$vis->entrerStock(30);
echo $vis->quantite; // 150 — modifié via une méthode
// $vis->quantite = 0; // Error : écriture interdite depuis l'extérieur
La syntaxe public private(set) se lit « public en lecture, privé en écriture ». De l’extérieur, on peut afficher $vis->quantite, mais toute affectation directe déclenche une erreur ; seules les méthodes de la classe, comme entrerStock(), peuvent la modifier. On obtient l’encapsulation d’un getter/setter classique sans écrire une seule ligne de getter. Quant à reference, marquée readonly, elle est gravée dans le marbre dès la construction : aucune méthode, même interne, ne pourra la changer.
Point d’étape — Testez les trois dernières lignes une à une. La lecture de
quantitedoit afficher la valeur ; l’appel àentrerStock()doit l’augmenter ; et décommenter l’affectation directe doit produire uneError. Si l’affectation externe passe sans erreur, vérifiez que vous êtes bien en PHP 8.4 (php --version).
Étape 4 — Ajouter une propriété calculée avec un property hook
Souvent, une propriété n’a pas besoin d’être stockée : elle se déduit d’autres. « La pièce est-elle en stock ? » se calcule à partir de quantite. Avant PHP 8.4, on écrivait une méthode estEnStock(). Désormais, les property hooks permettent d’exposer cela comme une propriété, avec un bloc get qui calcule la valeur à la volée.
<?php
declare(strict_types=1);
namespace Atelier;
final class Piece
{
public function __construct(
public readonly string $reference,
public string $nom,
public float $prix,
public private(set) int $quantite = 0,
) {}
// Property hook en lecture seule : calculée, jamais stockée
public bool $enStock {
get => $this->quantite > 0;
}
// Property hook calculé : un libellé lisible (référence + nom)
public string $libelle {
get => "{$this->reference} — {$this->nom}";
}
}
$vis = new Piece('VIS-M6', 'Vis métaux M6', 250.0, 0);
var_dump($vis->enStock); // bool(false)
echo $vis->libelle; // VIS-M6 — Vis métaux M6
La propriété $enStock n’occupe aucune place en mémoire : à chaque lecture, son hook get évalue l’expression. Du point de vue de l’utilisateur de la classe, $vis->enStock se lit comme une propriété normale, mais sa valeur reflète toujours l’état courant de quantite. C’est plus lisible qu’un appel de méthode et, contrairement à un getter, c’est compris nativement par les IDE et l’analyse statique. Les hooks acceptent aussi un bloc set pour intercepter l’écriture (par exemple normaliser une chaîne) — utile, mais à réserver aux cas où la transformation est simple ; une logique complexe mérite une vraie méthode nommée.
Étape 5 — Instancier et chaîner sans parenthèses
Petit confort de syntaxe, mais qui revient sans cesse : PHP 8.4 autorise enfin l’accès à une propriété ou l’appel d’une méthode directement après new, sans entourer l’expression de parenthèses. Auparavant, pour appeler une méthode sur un objet fraîchement créé en une expression, il fallait écrire (new Piece(...))->libelle. C’est désormais inutile.
<?php
// Avant PHP 8.4 :
echo (new Piece('VIS-M6', 'Vis métaux M6', 250.0))->libelle;
// Depuis PHP 8.4 :
echo new Piece('VIS-M6', 'Vis métaux M6', 250.0)->libelle;
Le gain paraît cosmétique, mais il allège la lecture quand on enchaîne plusieurs appels, par exemple new RequeteBuilder()->where(...)->limit(10). Une paire de parenthèses en moins par instanciation chaînée, c’est du bruit visuel en moins. À noter : c’est une simplification de syntaxe, pas un changement de comportement — l’objet est créé exactement de la même façon.
Étape 6 — Interroger l’inventaire avec les nouvelles fonctions de tableau
PHP 8.4 ajoute quatre fonctions très attendues pour travailler sur les tableaux sans boucle manuelle : array_find() renvoie le premier élément qui satisfait une condition, array_find_key() sa clé, array_any() indique si au moins un élément la satisfait, et array_all() si tous la satisfont. Elles rendent le code déclaratif et lisible.
<?php
declare(strict_types=1);
use Atelier\Piece;
use Atelier\Categorie;
$inventaire = [
new Piece('VIS-M6', 'Vis métaux M6', 250.0, 120),
new Piece('HUI-10W', 'Huile moteur 10W40', 4500.0, 8),
new Piece('FUS-5A', 'Fusible 5A', 100.0, 0),
];
// Première pièce en rupture de stock
$rupture = array_find($inventaire, fn(Piece $p) => $p->quantite === 0);
echo $rupture?->reference ?? 'aucune'; // FUS-5A
// Au moins une pièce en rupture ?
var_dump(array_any($inventaire, fn(Piece $p) => $p->quantite === 0)); // true
// Toutes les pièces ont-elles un prix positif ?
var_dump(array_all($inventaire, fn(Piece $p) => $p->prix > 0)); // true
Chaque fonction prend le tableau et une fonction de rappel (ici une fonction fléchée fn(...) => ...) qui renvoie un booléen. array_find() renvoie l’élément trouvé ou null ; on combine ce null possible avec l’opérateur nullsafe ?-> et l’opérateur de coalescence ?? pour afficher proprement « aucune » le cas échéant. Avant 8.4, il fallait écrire une boucle foreach avec un drapeau ou utiliser array_filter() puis reset() — plus verbeux et moins clair sur l’intention. Ces fonctions sont un petit pas, mais elles rapprochent PHP des langages où ces opérations sont natives.
Point d’étape — Rassemblez les classes
PieceetCategorieet ce script dans un même dossier, puis lancez-le. Vous devez voirFUS-5A, puisbool(true)deux fois. Si vous obtenez « Call to undefined function array_find() », votre PHP est antérieur à 8.4.
Étape 7 — Signaler du code obsolète proprement
Quand on fait évoluer une base de code, on remplace des méthodes sans pouvoir les supprimer immédiatement. PHP 8.4 formalise cela avec l’attribut #[\Deprecated] : il marque une fonction ou une méthode comme dépréciée, et tout appel déclenche un avertissement à l’exécution, exactement comme les dépréciations internes de PHP.
<?php
declare(strict_types=1);
namespace Atelier;
final class Piece
{
// ... constructeur et hooks ...
#[\Deprecated(message: "Utilisez la propriété \$enStock", since: "1.2")]
public function estEnStock(): bool
{
return $this->enStock;
}
}
$p = new Piece('VIS-M6', 'Vis', 250.0, 5);
$p->estEnStock(); // Deprecated: Method Atelier\Piece::estEnStock() is deprecated since 1.2, Utilisez la propriété $enStock
Le message et la version (since) apparaissent dans le journal des dépréciations, ce qui aide les autres développeurs — et vous-même dans six mois — à migrer en douceur. C’est bien plus fiable qu’un commentaire // déprécié que personne ne lit. Combiné à l’analyse statique, l’attribut permet même de repérer les appels à du code déprécié avant l’exécution.
Étape 8 — Vérification finale
Réunissez la classe Piece dans son état final (readonly, visibilité asymétrique, hooks) et un script de démonstration qui crée trois pièces, en met une en rupture, puis affiche la valeur totale du stock. La valeur se calcule en une expression grâce à array_sum() et array_map() :
<?php
$valeurStock = array_sum(
array_map(fn(Piece $p) => $p->prix * $p->quantite, $inventaire)
);
echo "Valeur du stock : " . number_format($valeurStock, 0, ',', ' ');
Si l’ensemble s’exécute sans erreur de type et affiche une valeur cohérente, votre première brique est solide. Vous avez écrit une classe métier complète en PHP 8.4 idiomatique : typée, immuable là où il faut, avec des propriétés calculées et des requêtes de collection lisibles.
Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
declare() must be the first statement |
Une instruction (espace, BOM, balise) précède declare |
Mettre declare(strict_types=1); immédiatement après <?php |
Cannot modify readonly property |
Tentative d’écriture sur une propriété readonly |
L’initialiser uniquement dans le constructeur, ou utiliser private(set) |
Call to undefined function array_find() |
PHP antérieur à 8.4 | Mettre à jour PHP, ou utiliser array_filter() en attendant |
Le hook set boucle à l’infini |
Le hook réaffecte la propriété qu’il intercepte | Dans un hook, on assigne $this->prop seulement dans un set qui reçoit la valeur, sans se rappeler lui-même |
| Aucun avertissement de dépréciation | error_reporting n’inclut pas E_DEPRECATED |
Vérifier error_reporting(E_ALL) en développement |
Récapitulatif
Vous venez de poser la première pierre du projet « Atelier » : une classe Piece écrite comme on écrit le PHP moderne. Le typage strict garantit la cohérence des données ; readonly et la visibilité asymétrique encadrent ce qui peut changer ; les property hooks exposent des valeurs calculées sans code répétitif ; les fonctions array_find et compagnie interrogent une collection de façon déclarative ; et #[\Deprecated] documente l’évolution du code. Ces réflexes ne sont pas propres à PHP 8.4 : ils définissent ce que veut dire « écrire du PHP propre » aujourd’hui.
Aide-mémoire
| Élément | Rôle |
|---|---|
declare(strict_types=1); |
Active le typage strict (1re ligne du fichier) |
public private(set) int $x |
Visibilité asymétrique : lecture publique, écriture privée |
public readonly string $ref |
Propriété immuable après construction |
public bool $y { get => ...; } |
Property hook : propriété calculée |
new Piece(...)->methode() |
Chaînage après new sans parenthèses |
array_find($a, $fn) |
Premier élément satisfaisant la condition (ou null) |
array_any($a, $fn) / array_all($a, $fn) |
Au moins un / tous les éléments satisfont la condition |
#[\Deprecated(message:..., since:...)] |
Marque une fonction/méthode comme dépréciée |
À vous de jouer
Ajoutez à Piece une propriété calculée $valeurStock (prix × quantité) sous forme de property hook, puis écrivez une expression qui trouve, avec array_find, la première pièce dont la valeur de stock dépasse 10 000.
Voir une solution
// Dans la classe Piece :
public float $valeurStock {
get => $this->prix * $this->quantite;
}
// Recherche :
$chere = array_find($inventaire, fn(Piece $p) => $p->valeurStock > 10000);
echo $chere?->libelle ?? 'aucune';
Tutoriels liés
- La programmation orientée objet en PHP — approfondir classes, interfaces et énumérations.
- Composer et l’autoloading PSR-4 — organiser ces classes en un vrai projet.
Pour aller plus loin
- 🔝 Retour au guide : PHP moderne, le guide complet.
- Notes de version officielles : php.net/releases/8.4.
- Documentation des property hooks : php.net — Property Hooks.
Foire aux questions
Dois-je mettre declare(strict_types=1) dans tous les fichiers ?
Oui, dans chaque fichier qui contient du code exécutable. La déclaration est locale au fichier où elle figure : elle n’affecte pas les fichiers inclus. C’est une habitude à prendre dès la première ligne.
Les property hooks ralentissent-ils l’exécution ?
Le coût est négligeable pour un hook simple comme une comparaison ou une concaténation. Si un hook get effectue un calcul lourd à chaque lecture, mieux vaut le mémoïser ou en faire une méthode explicite, mais c’est un cas rare.
Quelle différence entre readonly et private(set) ?
readonly interdit toute modification après la première affectation, même depuis l’intérieur de la classe. private(set) autorise les modifications, mais seulement depuis l’intérieur de la classe. On choisit readonly pour une identité figée, private(set) pour un état qui évolue de façon contrôlée.