PHP 8.xのreadonlyプロパティ、どこで使う?
Laravel
PHP
以前、PHPカンファレンスで興味深い場面がありました。PHP 8.xで導入されたreadonlyプロパティについて、「実際にどこで使っていますか?」という質問が会場から飛んだのですが、登壇者がうまく答えられなかったのです。
発言しておいて使ってないんかい!と思ったが、確かに今までPHPにはなかったから使いどころ難しい。
私はDTO(Data Transfer Object)によく使っています。
Laravelで開発をしていると、FormRequestでバリデーションを通した後のデータをどう扱うかという問題に直面します。配列のまま持ち回すとキーのタイポや型の不一致に気づけない。かといって、普通のクラスに詰め替えても途中で値を書き換えられるリスクが残ります。
ここでreadonlyの出番です。
final readonly class CreateOrderDto
{
public function __construct(
public string $orderNumber,
public int $userId,
public int $totalCents,
public string $currency,
public CarbonImmutable $orderedAt,
public array $items,
) {}
public static function fromRequest(StoreOrderRequest $request): self
{
$validated = $request->validated();
// 注文番号の採番、金額の整数変換、日時のパースなど
// 「加工ロジック」をDTO生成に閉じ込める
return new self(
orderNumber: 'ORD-' . now()->format('Ymd') . '-' . Str::random(6),
userId: $request->user()->id,
totalCents: (int) ($validated['total'] * 100),
currency: Str::upper($validated['currency'] ?? 'jpy'),
orderedAt: CarbonImmutable::parse($validated['ordered_at']),
items: collect($validated['items'])->map(fn (array $item) => [
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'unit_price' => (int) ($item['price'] * 100),
])->all(),
);
}
}
ポイントは fromRequest がただのセッターではないこと。注文番号の採番、金額の小数→整数変換、通貨コードの正規化、日時文字列のパース、ネスト配列の整形——こうした「バリデーション後の加工ロジック」をDTOの生成処理に閉じ込めることで、コントローラーやサービス層がスッキリします。
// コントローラーはDTOを渡すだけ
public function store(StoreOrderRequest $request): RedirectResponse
{
$dto = CreateOrderDto::fromRequest($request);
// $dto->totalCents = 0; // Error: readonly は再代入不可
// 加工済み・変更不能なデータをサービスに渡す
$this->orderService->place($dto);
return redirect()->route('orders.show', $dto->orderNumber);
}
つまり「バリデーション済みの値が、その後の処理で意図せず変わることがない」という安心感を、言語レベルで保証できるわけです。
readonlyは一見地味な機能ですが、「値を守る」という点においてDTOとの相性は抜群です。もしまだ活用していない方がいれば、ぜひFormRequest + readonly DTOの組み合わせを試してみてください。