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