Kodokon kodokon.com

Fortgeschrittenes PHP 8: Attribute, readonly, Callables

Erkunde das Innenleben von Attributen, readonly, First-Class-Callables und der strengen Typisierung von PHP 8.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Attribute ersetzen die Docblock-Annotationen durch echte Klassen, die von der Engine validiert werden. Ein Attribut ist nichts als schlafende Metadaten: #[Route('/users')] zu schreiben erzeugt kein Objekt. Es erwacht erst zum Leben, wenn Code es per Reflection abfragt und newInstance() aufruft - genau das tun Frameworks beim Start, um ihre Routing-Tabellen aufzubauen. Das erlaubte Ziel (TARGET_METHOD, TARGET_CLASS...) wird selbst durch ein Attribut auf der Attributklasse deklariert.

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;
}
Das Attribut wird erst im Moment von newInstance() instanziiert.

readonly - Eigenschaften in 8.1, ganze Klassen in 8.2 - garantiert, dass eine Eigenschaft nur einmal geschrieben wird, und zwar aus dem Geltungsbereich der Klasse heraus. Es ist das Werkzeug für Value Objects: Jede "Änderung" wird zur Konstruktion eines neuen Objekts, was eine ganze Familie von Bugs mit geteiltem Zustand ausschließt. Eine readonly-Klasse verlangt, dass alle ihre Eigenschaften typisiert und nicht statisch sind.

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;
Ein Value Object: Das Addieren erzeugt eine neue Instanz.

Die First-Class-Callable-Syntax strtoupper(...) (PHP 8.1) erzeugt aus jeder Funktion oder Methode eine Closure. Anders als Zeichenketten wie 'strtoupper' oder Arrays wie [$obj, 'method'] wird das Symbol schon bei der Erzeugung überprüft, und die Werkzeuge der statischen Analyse verfolgen die Typen durchgehend. Bei einer Instanzmethode fängt die Closure das Objekt ein, bei einer statischen Methode nur die Klasse.

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());
Jeder Ausdruck f(...) erzeugt eine Closure, die bei der Erzeugung überprüft wird.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Wann wird die Klasse eines Attributs #[Route(...)] instanziiert?
    • Wenn die Datei kompiliert wird, die es trägt
    • Sobald die dekorierte Methode aufgerufen wird
    • Erst wenn Code es per Reflection abfragt und newInstance() aufruft
  2. Was garantiert eine readonly-Eigenschaft genau?
    • Das Objekt, auf das sie verweist, wird vollständig unveränderlich
    • Sie wird nur einmal geschrieben, aus dem Geltungsbereich der Klasse heraus; das referenzierte Objekt selbst bleibt veränderbar
    • Sie kann nur von der Klasse gelesen werden, die sie deklariert
  3. Welchen Geltungsbereich hat declare(strict_types=1)?
    • Das gesamte Projekt, sobald eine einzige Datei es deklariert
    • Die Aufrufe, die in der Datei stehen, die die Deklaration enthält
    • Nur die in dieser Datei definierten Funktionen, wo auch immer sie aufgerufen werden