Kodokon kodokon.com

PHP 8の応用:アトリビュート、readonly、callable

アトリビュート、readonly、第一級callable、そしてPHP 8の厳密な型付けの内部の仕組みを探求しましょう。

10 分 · 3 問

このレッスンを Kodokon で開く

アトリビュートは、docblockのアノテーションを、エンジンによって検証される本物のクラスに置き換えます。アトリビュートは休眠中のメタデータにすぎません。#[Route('/users')]と書いても、オブジェクトは作られません。それが命を得るのは、あるコードがリフレクションを通してそれに問い合わせ、newInstance()を呼んだときだけです。これはまさに、フレームワークが起動時にルーティングテーブルを構築するために行うことです。許可されるターゲット(TARGET_METHODTARGET_CLASSなど)自体も、アトリビュートクラスに付けられたアトリビュートによって宣言されます。

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;
}
アトリビュートはnewInstance()の瞬間にのみインスタンス化される

readonlyは、8.1ではプロパティに、8.2ではクラス全体に適用でき、プロパティがクラスのスコープの内側から一度だけ書き込まれることを保証します。これは値オブジェクトのための道具です。あらゆる「変更」は新しいオブジェクトの構築になり、共有状態のバグの一族をまるごと排除します。readonlyクラスは、そのすべてのプロパティが型付けされ、非staticであることを要求します。

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;
値オブジェクト。加算は新しいインスタンスを作る

第一級callable構文strtoupper(...)(PHP 8.1)は、任意の関数やメソッドからClosureを作ります。'strtoupper'のような文字列や[$obj, 'method']のような配列とは違い、シンボルは作成時に検証され、静的解析ツールは型を端から端まで追いかけます。インスタンスメソッドの場合、クロージャはオブジェクトを捕捉し、staticメソッドの場合はクラスだけを捕捉します。

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());
それぞれのf(...)式は、作成時に検証されるClosureを生み出す

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. #[Route(...)]アトリビュートのクラスはいつインスタンス化されますか?
    • それを含むファイルがコンパイルされるとき
    • 装飾されたメソッドが呼ばれるとすぐ
    • あるコードがリフレクションを通してそれに問い合わせ、newInstance()を呼んだときだけ
  2. readonlyプロパティは正確には何を保証しますか?
    • それが参照するオブジェクトが完全に不変になる
    • クラスのスコープの内側から一度だけ書き込まれる。参照先のオブジェクト自体は変更可能なまま
    • それを宣言したクラスからしか読み取れない
  3. declare(strict_types=1)のスコープは何ですか?
    • 単一のファイルがそれを宣言するや否や、プロジェクト全体
    • 宣言を含むファイルの中に書かれた呼び出し
    • そのファイルで定義された関数だけ、どこから呼ばれようとも