Kodokon kodokon.com

PHP 8 avancé : attributs, readonly, callables

Explorez la mécanique interne des attributs, de readonly, des callables de première classe et du typage strict de PHP 8.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

Les attributs remplacent les annotations en docblocks par de vraies classes, validées par le moteur. Un attribut n'est qu'une métadonnée dormante : écrire #[Route('/users')] ne crée aucun objet. Il ne prend vie que lorsqu'un code l'interroge par réflexion et appelle newInstance() - c'est exactement ce que font les frameworks au démarrage pour construire leurs tables de routage. La cible autorisée (TARGET_METHOD, TARGET_CLASS...) est elle-même déclarée par un attribut sur la classe d'attribut.

PHP
<?php
declare(strict_types=1);

#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
    public function __construct(
        public readonly string $path,
        public readonly string $method = 'GET'
    ) {}
}

final class UserController
{
    #[Route('/users', method: 'GET')]
    public function index(): array
    {
        return ['users' => []];
    }
}

$ref = new ReflectionMethod(
    UserController::class,
    'index'
);

foreach ($ref->getAttributes(Route::class) as $attr) {
    $route = $attr->newInstance();
    echo $route->method, ' ', $route->path, PHP_EOL;
}
L'attribut n'est instancié qu'au moment de newInstance().

readonly - propriétés en 8.1, classes entières en 8.2 - garantit qu'une propriété n'est écrite qu'une seule fois, depuis la portée de la classe. C'est l'outil des objets-valeurs : toute « modification » devient la construction d'un nouvel objet, ce qui élimine une famille entière de bugs d'état partagé. Une classe readonly impose que toutes ses propriétés soient typées et non statiques.

PHP
<?php
declare(strict_types=1);

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency
    ) {}

    public function add(Money $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException(
                'Currency mismatch'
            );
        }
        return new self(
            $this->amount + $other->amount,
            $this->currency
        );
    }
}

$total = new Money(500, 'EUR');
$total = $total->add(new Money(250, 'EUR'));
echo $total->amount;
Un objet-valeur : additionner crée une nouvelle instance.

La syntaxe de callable de première classe strtoupper(...) (PHP 8.1) crée une Closure à partir de n'importe quelle fonction ou méthode. Contrairement aux chaînes comme 'strtoupper' ou aux tableaux [$obj, 'method'], le symbole est vérifié dès la création et les outils d'analyse statique suivent les types de bout en bout. Pour une méthode d'instance, la closure capture l'objet ; pour une méthode statique, seulement la classe.

PHP
<?php
declare(strict_types=1);

$words = ['php', 'rust', 'go'];

$upper = strtoupper(...);
var_dump($upper('php'));

print_r(array_map(strtoupper(...), $words));

final class Mailer
{
    public function send(string $to): bool
    {
        return $to !== '';
    }

    public static function ping(): string
    {
        return 'pong';
    }
}

$mailer = new Mailer();
$send = $mailer->send(...);
$ping = Mailer::ping(...);

var_dump($send('dev@example.org'), $ping());
Chaque expression f(...) produit une Closure vérifiée à la création.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Quand la classe d'un attribut #[Route(...)] est-elle instanciée ?
    • À la compilation du fichier qui le porte
    • Dès que la méthode décorée est appelée
    • Seulement quand un code l'interroge par réflexion et appelle newInstance()
  2. Que garantit exactement une propriété readonly ?
    • L'objet qu'elle référence devient entièrement immuable
    • Elle n'est écrite qu'une seule fois, depuis la portée de la classe ; l'objet référencé, lui, reste mutable
    • Elle n'est lisible que depuis la classe qui la déclare
  3. Quelle est la portée de declare(strict_types=1) ?
    • Tout le projet dès qu'un seul fichier le déclare
    • Les appels écrits dans le fichier qui contient la déclaration
    • Uniquement les fonctions définies dans ce fichier, où qu'elles soient appelées