
ただし、ここには少し厄介な罠がある。プロパティフックはゲッター・セッターを廃止する機能ではない。PHP 8.4で既存のアクセスメソッドが非推奨になったわけでもなく、LaravelやEloquentの内部設計が一気に置き換わるわけでもない。
結局のところ、プロパティフックは「値へのアクセス処理を、メソッドではなくプロパティの宣言側に寄せるための言語機能」だ。便利ではある。しかし、便利だからという理由だけで既存コードへ一斉投入すると、別の種類の保守地獄が始まる。
プロパティフックは何を変えたのか
これまでのPHPでは、プロパティの読み取りや書き込みに処理を挟みたい場合、専用のメソッドを用意するのが基本だった。
たとえば、ユーザー名を正規化して保存するクラスなら、内部プロパティに直接代入させず、getName() や setName() のようなメソッドを経由させる。値の変換、バリデーション、ログ出力などをメソッド内にまとめるわけだ。
この設計は今でも有効だ。むしろ、複雑なドメインロジックを扱うなら、処理をメソッドとして明示したほうが読みやすい場面は多い。
PHP 8.4のプロパティフックでは、プロパティそのものに get と set の処理を定義できる。利用側から見れば、通常のプロパティと同じように読み書きする。しかし内部では、値を返すときや代入するときに指定した処理が実行される。
プロパティフックで追加された主な要素は、次のとおりだ。
getによって、プロパティを読み取るときの処理を定義できるsetによって、値を代入するときの処理を定義できる- 実体を持つバックドプロパティと、値を保持しない仮想プロパティを定義できる
- インターフェース上で、プロパティに必要な読み書きの契約を宣言できる
- 非対称可視性と組み合わせて、外部からの書き込みだけを制限できる
従来のゲッター・セッターと違うのは、利用側の記述をメソッド呼び出しからプロパティアクセスへ戻せる点だ。つまり、クラスの外から見える形が、データ構造として自然になる。
バックドプロパティと仮想プロパティ
プロパティフックには、大きく分けて二つの使い方がある。
一つは、実際に値を保持するバックドプロパティだ。値を保存する場所は通常のプロパティとして存在し、set で代入前の変換を行ったり、get で返却時の整形を行ったりする。
たとえば、氏名を常に前後の空白を除いた状態で保持する場合、設定時に文字列を整形してから保存できる。呼び出し側は user->name のようにアクセスするだけでよく、setName() を呼び出す必要がない。
もう一つは、値を保持しない仮想プロパティだ。データベースや内部プロパティに保存された値をもとに、計算結果を返す用途に向いている。
たとえば、姓と名を結合した表示名、金額に税率を適用した合計値、現在の状態から算出する表示ラベルなどだ。こうした値は保存する必要がない。読み取り時に計算すればよい。
従来なら getDisplayName() のようなメソッドを追加していたところを、プロパティとして表現できる。これは単に短く書けるという話ではない。呼び出し側から見たモデルが、「操作」ではなく「持っている値」に近くなる。
ただし、仮想プロパティを乱用すると、プロパティ名だけでは重い処理の存在が分からなくなるという問題もある。内部で複雑な集計や外部サービスへの問い合わせを行うなら、あえてメソッドとして残したほうがよい。
プロパティフックは、コードを短くする機能というより、値の境界をどこに置くかを整理する機能だ。
get と set の挙動を整理する
プロパティフックを導入するときに最初に確認したいのは、読み取り時と書き込み時の責務を分けることだ。
set は値が代入されるタイミングで動く。入力値の正規化、型に合わせた変換、簡単な制約チェックなどはここに置きやすい。一方で、データベースへ保存する処理や、他の集約に影響する処理まで入れると、単純な代入が急に副作用を持ち始める。
これはプロパティフックに限った話ではない。セッターが巨大化して炎上する案件は以前から存在した。ただ、プロパティとして自然に見える分、フックの中身が重くても気づきにくくなるという罠がある。
set に向いている処理、向いていない処理
set に適しているのは、代入された値だけを見て完結する処理だ。
- 前後の空白を取り除く
- 大文字・小文字を統一する
- 単位を変換する
- 軽量な形式チェックを行う
- 内部的な値の表現を統一する
反対に、次のような処理は慎重に扱うべきだ。
- データベースへの保存
- メール送信やキュー投入
- 外部APIの呼び出し
- 他のオブジェクトの状態変更
- 現在時刻や認証ユーザーに依存する処理
プロパティへの代入は、コード上では非常に軽く見える。そこに外部通信が隠れていると、呼び出し元の開発者は処理コストを見誤る。
「値を設定しただけなのに、なぜトランザクションが必要なのか」という事故は、設計レビューで何度も見てきた。力技で動かすことはできるが、後から読む人間には優しくない。
get に向いている処理、向いていない処理
get は値を返すための処理だ。表示用の整形や、単純な派生値の計算は相性がよい。
たとえば、内部では日時オブジェクトを保持していて、画面表示用の文字列を返すケースだ。あるいは、複数のフラグから状態名を算出するケースも分かりやすい。
しかし、get の中で毎回データベースへアクセスする設計は危険だ。テンプレートから同じプロパティを何度も読むだけで、問い合わせが繰り返される可能性がある。LaravelのEloquentでは遅延ロードやアクセサーとの組み合わせもあるため、なおさら処理の見え方と実コストがずれやすい。
プロパティを読んでいるだけに見えても、実際には計算量やクエリ数が増えている。その差をチーム全員が理解できないなら、メソッドのほうが安全な場合がある。
従来のゲッター・セッターは本当に不要になるのか
結論から言えば、不要にはならない。
PHP 8.4でプロパティフックが導入されても、従来のゲッター・セッターは引き続き利用できる。既存のコードをすべて書き換える必要もない。大量の呼び出し箇所を持つ公開パッケージなら、互換性の観点から従来のAPIを残す判断も普通にあり得る。
プロパティフックへ移行しやすいのは、次のようなクラスだ。
- 新規に作る値オブジェクト
- 内部利用に限定されたデータクラス
- 読み取り時の軽い整形だけを行うモデル
- 単純な正規化処理を持つ設定オブジェクト
- 外部公開APIとしてメソッド名を維持する必要がないクラス
一方、ゲッター・セッターを残したほうがよいケースもある。
- メソッド名自体がドメイン上の意味を持つ
- 書き込みが状態遷移を表す
- 代入時に複数の副作用が発生する
- 例外の種類や失敗条件を明示したい
- 既存の公開APIを壊せない
- ORMやシリアライザーがメソッドを前提としている
「プロパティに代入する」という文法は、どうしても単純なデータ変更に見える。実際には「注文を確定する」「支払い状態を変更する」「会員資格を更新する」といった意味を持つなら、setStatus() よりさらに明示的な confirm() や suspend() のような操作メソッドを使うほうがよい。
プロパティフックは、名前を短くするための免罪符ではない。ドメイン上の操作を、単なる代入に見せてはいけない。
比較すると見える使い分け
| 観点 | プロパティフック | ゲッター・セッター |
|---|---|---|
| 記述量 | 単純な変換なら少なくできる | メソッド定義が必要で増えやすい |
| 呼び出し側 | プロパティとして自然に読める | 処理を呼び出していることが明確 |
| 軽い正規化 | 非常に向いている | 問題なく対応できる |
| 複雑な副作用 | 意図が隠れやすい | メソッド名で明示しやすい |
| 公開APIの互換性 | 新規設計で扱いやすい | 既存コードとの互換性を維持しやすい |
| ORMとの相性 | ORMの仕組み次第で注意が必要 | 既存の拡張方式と合わせやすい |
| デバッグ | 代入の裏側を追う必要がある | 呼び出し箇所を追いやすい |
| 値オブジェクト | 非常に相性がよい | 複雑な検証ではこちらも有効 |
速度についても、過剰な期待は禁物だ。測定では、公開プロパティへの直接アクセスと比べて、ゲッター・セッター経由のアクセスが約60%遅いという結果がある。しかし、これはアクセス処理単体のベンチマークだ。
Webアプリケーション全体で見ると、データベース、テンプレート描画、ネットワーク、シリアライズなどの処理が支配的になる。たとえば多数のエンティティを描画する処理でも、ゲッター・セッターの差がレスポンス全体へ与える影響は、ミリ秒未満にとどまるケースがある。
つまり、プロパティフックを性能改善の主役にするのは筋が悪い。性能問題を解決したいなら、まずクエリ数、インデックス、N+1、キャッシュ、シリアライズを疑うべきだ。プロパティアクセスの数十ナノ秒を削っても、SQLが一つ余計に走れば簡単に帳消しになる。
LaravelとEloquentではどう考えるべきか
LaravelでPHP 8.4を使う場合、プロパティフックの影響を考えるには、まずEloquentのモデル構造を理解しておく必要がある。
Eloquentモデルは、一般的なデータクラスのように各カラムを通常のプロパティへ直接保持しているわけではない。属性値は内部の属性配列を中心に管理され、マジックメソッドやキャスト、アクセサー、ミューテーターなどを通じて読み書きされる。
そのため、Eloquentモデルのカラムへプロパティフックを追加すれば、従来のアクセサーやキャストをそのまま置き換えられる、と考えるのは危険だ。PHPの言語機能としてのプロパティアクセスと、Eloquentが提供する属性アクセスは、似て見えても処理の層が違う。
Eloquentのカラムを直接フック化する発想の問題
仮に name というデータベースカラムへPHPのプロパティフックを定義したとしても、Eloquentの属性配列との同期、モデルの水和、ダーティ判定、キャスト、シリアライズなどが自動的にきれいに統合されるとは限らない。
既存のEloquentモデルには、次のような仕組みが絡んでいる。
- データベースから取得した属性の格納
- 変更された属性の追跡
- 型変換と日付処理
- アクセサーとミューテーター
- リレーションの遅延ロード
- 配列化やJSON化
- イベントや保存処理との連動
この状態でプロパティフックを追加すると、どの層が値の正規化を担当するのかが曖昧になりやすい。フックにも変換処理を書き、Eloquentのキャストにも同じ処理を書けば、二重変換が起こる。片方だけを変更すれば、読み取りと書き込みで挙動がずれる。
ここは「新しい機能だから使う」という判断をしてはいけない場所だ。LaravelのEloquentは、長年積み重なった便利さと癖の塊である。中身を知らずに言語機能だけ差し込むと、謎のバグが発生したときに原因が三つの層へ分散する。
Laravelで導入しやすい場所
プロパティフックを使うなら、まずEloquentの中心から少し離れたクラスで試すのがよい。
たとえば、フォーム入力を受け取るデータ転送用オブジェクト、APIレスポンス用の値オブジェクト、設定をまとめるクラスなどだ。これらはデータベースの属性配列に依存しないため、フックの挙動を予測しやすい。
特に、入力値を内部表現へ変換するクラスとは相性がよい。
- 文字列で受け取った日時を日時オブジェクトへ変換する
- 金額を整数の最小単位へ正規化する
- メールアドレスの空白や大文字を統一する
- APIの外部形式と内部形式を変換する
- 列挙型や識別子を扱いやすい値へ変換する
ここで大切なのは、フックの中でLaravelのサービスコンテナやデータベースへ手を伸ばさないことだ。値の変換を閉じ込めるための機能として使うなら、テストもしやすい。
逆に、Eloquentのアクセサーやミューテーターをすぐに置き換えるのは避けたほうがよい。既存の機能がフックで完全に代替できるか、フレームワークのバージョンや周辺パッケージがどのように扱うかを確認してから判断するべきだ。
Laravelでは、PHP 8.4の機能を使えることと、Eloquentの設計へ安全に組み込めることは別問題である。
非対称可視性と読み取り専用設計
プロパティフックを扱うとき、readonly との関係も避けて通れない。
PHP 8.4のプロパティフックは、readonly プロパティと同時には使えない。読み取り用の処理を定義しながら、同じプロパティを言語仕様上の readonly にすることはできない。
ここで必要になるのが、非対称可視性だ。外部からは読み取れるが、書き込みはクラス内部に限定する、といった設計を表現できる。読み取り専用のデータを作りたい場合でも、必ずしも readonly に固執する必要はない。
たとえば、生成後に外部から変更させたくない識別子や、内部でだけ更新される状態値なら、公開範囲を読み取りと書き込みで分けるほうが実用的だ。
ただし、非対称可視性を採用したからといって、値のライフサイクルが自動的に正しくなるわけではない。どのメソッドから変更できるのか、初期化のタイミングはいつか、再代入が許されるのかは、クラスの設計として決める必要がある。
readonly を使いたい場面との違い
readonly は、オブジェクトの状態が初期化後に変更されないことを強く示す機能だ。値を一度決めたら変えないという契約を、言語側に持たせたい場合に向いている。
一方、非対称可視性は、読み書きの権限を分けるための仕組みだ。内部からは変更できるが、外部からは読み取りだけ許すという設計に使える。
この二つは似ているようで、意図が違う。
- オブジェクトの生存期間を通じて変更しないなら
readonly - 外部からの変更だけを防ぎ、内部では更新するなら非対称可視性
- 読み取り時や書き込み時に変換処理が必要ならプロパティフック
- 状態遷移そのものに意味があるなら専用メソッド
一つの機能ですべてを表現しようとすると、設計が不自然になる。PHP 8.4では選択肢が増えたが、選択肢が増えた分だけ、意図を分けて考えなければならない。
インターフェースでプロパティの契約を定義する
PHP 8.4では、インターフェース上でプロパティフックに関する要件を定義できるようになった。これにより、実装クラスへ特定のプロパティを持たせる契約を、メソッドだけでなくプロパティとして表現できる。
これは値オブジェクトや、複数の実装を持つアダプターで有効だ。
これまでインターフェースで値へのアクセスを要求する場合、getValue() や setValue() のようなメソッドを定義するのが一般的だった。PHP 8.4では、プロパティに対して読み取りや書き込みが必要であることを宣言し、実装側ではフック付きプロパティや通常の公開プロパティで契約を満たせる。
この変更は、単なる記述量の削減ではない。インターフェースが表すものを、「このオブジェクトは何を操作できるか」から「このオブジェクトはどのような値を提供するか」へ寄せられる。
ただし、公開APIに関わるインターフェースでは慎重さが必要だ。プロパティはメソッドと違い、呼び出し時に処理があることを表面上示しにくい。実装を差し替えたとき、片方はメモリ上の値を返し、もう片方は計算や外部読み込みを行う、といった差が生まれる可能性がある。
契約としてプロパティを要求するなら、実装間で処理の重さや副作用に大きな差が出ないようにするべきだ。インターフェースの見た目だけを統一しても、実行時の意味がばらばらなら、抽象化としては失敗している。
静的解析と既存ツールの対応をどう見るか
PHP 8.4の新機能を実案件へ導入する場合、言語処理系だけでなく周辺ツールの対応も確認しなければならない。
PHPStanやPsalmなどの静的解析ツール、統合開発環境の補完、デバッガー、シリアライザー、ORM、コード生成ツール。プロパティフックは文法としてはPHP本体の機能だが、開発体験は周辺ツールの理解に大きく左右される。
特に問題になりやすいのは、次のような場面だ。
- フック内で使う特殊な変数や値の型推論
- 仮想プロパティの戻り値解析
- インターフェース上のプロパティ契約
- 非対称可視性を含むアクセス制御
- リフレクションを使ったシリアライズ
- コード生成や自動マッピング
- 古いPHPバージョンを対象にした構文解析
アプリケーション本体がPHP 8.4で動いていても、開発環境の解析ツールやCIの構文チェックが追いついていないことはある。コードを書いた直後にエディターが赤線だらけになれば、便利な新機能もただのストレス源だ。
このあたりは、対応状況を一度確認して終わりではない。フレームワークやパッケージの更新で挙動が変わる可能性もあるため、導入範囲を小さくしてCIで検証するのが現実的だ。
既存コードへ段階的に入れる手順
プロパティフックを既存プロジェクトへ取り入れるなら、いきなりモデル層全体を書き換えるのは避けたい。次のような順番なら、問題の切り分けがしやすい。
1. 依存関係と実行環境を確認する
本番、開発、CIでPHP 8.4のバージョンが揃っているか確認する。Composerの制約だけを変更して、実際のコンテナやサーバーが古いままという事故は珍しくない。
2. 副作用のない値オブジェクトから始める
文字列や金額、日時など、入力値を変換して保持するクラスを対象にする。データベースやHTTP通信が絡まない場所なら、失敗しても影響範囲を限定できる。
3. 通常のテストに加えてアクセス経路を確認する
直接代入、読み取り、再代入、未初期化状態、型エラーなどを確認する。プロパティフックは見た目が単純なので、境界条件がテストから漏れやすい。
4. 静的解析とシリアライズを通す
型解析だけでなく、配列化、JSON化、リフレクション、ログ出力など、実際にそのオブジェクトを扱う処理を確認する。
5. Eloquentモデルへの適用は最後に回す
ORMが管理する属性とPHPのプロパティを混ぜる前に、アクセサーやキャストとの責務を整理する。置き換えではなく、どの層が値を変換するのかを決めるところから始める。
6. チーム内の規約を決める
軽い正規化はフック、状態遷移はメソッド、外部通信は禁止、といった基準を共有する。個人の好みだけで導入すると、クラスごとに流儀が変わってしまう。
この手順は地味だが、地味な作業ほど炎上を防ぐ。新機能の導入で一番困るのは、導入した本人しか仕組みを理解していない状態だ。
どこまでコードで解決し、どこから運用で受けるか
プロパティフックに限らず、新しい言語機能を導入するときは、コードで解決できる範囲と運用で管理する範囲を分けたほうがよい。
コードで解決しやすいのは、値の形式やアクセス権限だ。空白の除去、型の変換、読み取りと書き込みの可視性、単純な派生値の計算。このあたりはプロパティフックの責務として整理しやすい。
一方で、次のような問題はコードだけで完全に解決しにくい。
- どの値を変更してよいかという業務ルール
- 変更時に誰へ通知するか
- 古いAPIをいつまで維持するか
- Laravelやパッケージの更新時期
- PHPの対応バージョンをどこまで上げるか
- チームが新しい構文を理解できるか
これらは設計方針、レビュー、テスト、リリース手順で支えるしかない。プロパティフックを導入したからといって、開発者がプロパティの裏側を意識しなくてよくなるわけではない。
むしろ、見た目が簡単になった分だけ、処理の責務を文書化しておく意味は大きい。$user->name が単なる文字列なのか、正規化処理を通る値なのか、アクセス時に計算される仮想プロパティなのか。コードを開けば分かるとしても、レビュー時に説明できる設計であることが望ましい。
導入を見送るべきサイン
プロパティフックを使わない判断も、十分に合理的だ。特に次のような状況では、従来のメソッドや既存フレームワーク機能を維持したほうがよい。
- 対象プロジェクトが複数のPHPバージョンをサポートしている
- 静的解析やコード生成の対応が不安定である
- プロパティへの代入が状態遷移や副作用を意味する
- Eloquentの属性処理と責務が重なる
- 外部パッケージがリフレクションやマジックメソッドに依存している
- チーム内で新構文の読み方が共有されていない
- 書き換えによるメリットより、互換性確認のコストが大きい
新機能は、使わないと遅れているように感じる。しかし、実務では「使わない判断を説明できること」のほうが重要な場合もある。PHP 8.4へ更新したからといって、すべてのクラスを8.4らしく書き換える必要はない。
PHP 8.4のプロパティフックを採用する基準
PHP 8.4のプロパティフックは、ゲッター・セッターの終わりを告げる機能ではない。値へのアクセスを、より直接的に表現するための選択肢だ。
単純な変換や仮想プロパティでは、従来よりもコードが読みやすくなる。インターフェースでプロパティの契約を表現できる点も、値を中心に設計するクラスでは有効だ。非対称可視性と組み合わせれば、外部からは読み取り専用に見せつつ、内部では更新できるモデルも作れる。
ただし、Eloquentの属性処理、アクセサー、キャスト、シリアライズ、静的解析との関係は慎重に扱う必要がある。特にLaravelのモデルを直接置き換える発想は、言語機能とORMの責務を混同しやすい。
判断基準をまとめるなら、次のようになる。
- 値の正規化や軽い変換ならプロパティフックを検討する
- 重い処理や外部通信を隠すならメソッドを使う
- 状態遷移を表すなら、意味のある操作メソッドを優先する
- ORMが値を管理しているなら、既存機能との責務を先に整理する
- 性能改善だけを目的に導入しない
- PHPや周辺ツールの対応範囲を確認してから段階的に使う
結局のところ、プロパティフックは「ゲッター・セッターを消すための新機能」ではなく、「値の境界を、クラスの設計に合わせて表現するための新しい道具」だ。
便利な道具ではある。だが、道具が増えたからといって、設計の迷いまで消えるわけではない。コードで解決すべき変換処理はフックへ寄せ、業務上の意味や副作用はメソッドと運用で明示する。その線引きさえ守れば、PHP 8.4のプロパティフックは、無理に既存コードを壊さずとも十分に役立つ。
Related reading: Laravel・PHP開発を選ぶときの確認ポイント and PestとPHPUnitの選択基準:Laravel開発でテスト移行を進めるべきかの判断ライン.