Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHP 8.4の新機能「プロパティフック」から学ぶオブジェクト指向設計とリスコフの置換原則

PHP 8.4の新機能「プロパティフック」から学ぶオブジェクト指向設計とリスコフの置換原則

PHP Conference Japan 2025
2025-06-28 14:20 -
トラック1 - 1F 大展示 / レギュラートーク(25分)
#phpcon #track1

https://fortee.jp/phpcon-2025/proposal/1358c8dc-52af-479c-b50b-89dd21056841

Avatar for 武田 憲太郎

武田 憲太郎

June 28, 2025
Tweet

More Decks by 武田 憲太郎

Other Decks in Programming

Transcript

  1. 基本的な例 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 4 • インスタンスにセットした値が ⾃動的に書き換わっている •

    プロパティ宣⾔と共にメソッド のようなものが書かれている class User { public function __construct(string $name) { $this->name = $name; } public string $name { get { return ucfirst($this->name); } set(string $value) { $this->name = strtolower($value); } } } $user = new User('ILIJA'); echo $user->name . "¥n"; // Ilija var_dump($user); // object(User)#1 (1) { // ["name"]=> // string(5) "ilija" // }
  2. 基本的な例 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 5 class User { public

    function __construct(string $name) { $this->name = $name; } public string $name { get { return ucfirst($this->name); } set(string $value) { $this->name = strtolower($value); } } } $user = new User('ILIJA'); echo $user->name . "¥n"; // Ilija var_dump($user); // object(User)#1 (1) { // ["name"]=> // string(5) "ilija" // } • 値の書き込みは set フックを 経由する • 値の読み込みは get フックを 経由する
  3. なぜこの機能が必要なのか? 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 6 動機: 簡潔なコードでオブジェクトを安全に扱いたい 1. プロパティの⼊出⼒を制限しオブジェクトを安全に扱いたい

    • 🏛 private プロパティと getFoo(), setFoo($value) • 🔥 ⼤量のボイラープレート 2. 🎉 PHP 7.4: 型付きプロパティ • 😄 型として表現可能なプロパティは setFoo($value) を使う必要がなくなった • 😅 public フィールドは外部からの再代⼊を防げない • 😰 再代⼊を防ぐために getFoo() が必要になる本末転倒 3. 🎉 PHP 8.1: readonly プロパティ • 🚀 public フィールドだけで不変なオブジェクトを作れるようになった
  4. なぜこの機能が必要なのか? 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 7 • 書き⽅を統⼀できない • 半径:

    $circle->radius • ⾯積: $circle->getArea() • 円周: $circle->getCircumference() • 仕様追加時の変更箇所 • 🙆 半径を変更すれば他は⾃動で計算 • ❌ 「⾯積を上書き」という追加要件 • ❌ 「半径も計算」する追加要件 • 書き⽅を統⼀し仕様追加も容易にする • 予め getFoo(), setFoo($value) で統⼀ • 5年前の書き⽅へ逆戻り • $circle->getRadiius() class Circle { public function __construct( public float $radius ) { } public function getArea(): float { return pi() * $this->radius ** 2; } public function getCircumference(): float { return 2 * pi() * $this->radius; } }
  5. なぜこの機能が必要なのか? 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 8 従来の動的プロパティ • __get() に

    switch 相当の コードがひたすら続く • typoされた $name が⼊⼒ される可能性 • エラーハンドリングはユー ザーの責任 • 静的解析の恩恵を受けられ ない class Circle { public function __get(string $name): mixed { if ($name === 'area') { return pi() * $this->radius ** 2; } if ($name === 'circumference') { return 2 * pi() * $this->radius; } // 全ての動的プロパティを一箇所で扱う // ここに到達したらどうする? } }
  6. なぜこの機能が必要なのか? 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 9 •「プロパティ」に統⼀ • $circle->radius •

    $circle->area • $circle->circumference •静的解析を利⽤可能 •仕様追加も容易 • set を後から実装すれば良い class Circle { public function __construct( public float $radius ) { } public float $area { get { return pi() * $this->radius ** 2; } } public float $circumference { get { return 2 * pi() * $this->radius; } } }
  7. 多くのフレームワークに既にある機能 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 10 •$posts というプロパティは User クラスにはない

    • あるのは posts() メソッド (リレーションシップ) •Laravelがリレーションシッ プを「フック」として呼び 出し「投稿⼀覧」を取得 class User extends Model { public function posts() { return $this->hasMany(Post::class); } } class PostController { public function index(User $user) { return $user->posts; } }
  8. 多くのフレームワークに既にある機能 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 11 • Eloquentの「アクセサ」「ミューテータ」はプロパティフックとほ ぼ同じ機能 •

    注: 計算結果オブジェクトのキャッシュなどEloquentに特化した機能も追 加で実装されている • 上の実装は冒頭のプロパティフックの例とほぼ等価 class User extends Model { protected function Name(): Attribute { return Attribute::make( get: fn (string $value) => ucfirst($value), set: fn (string $value) => strtolower($value), ); } } $user->name で 読み書き可能
  9. Vue.js 算出プロパティ 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 12 <template> <p>私の名前は: {{

    formattedName }} です</p> </template> <script> export default { props: { name: { type: String, required: true }, }, computed: { formattedName() { return this.name.charAt(0).toUpperCase() + this.name.slice(1).toLowerCase(); }, }, }; </script> <self-introduction name="ILIJA" /> <!-- <p>私の名前は: Ilija です</p> --> テンプレートからは 単純に変数を参照 裏側で get フックが呼ばれる フック通過後の値をレンダ
  10. JavaScript ゲッター‧セッター 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 13 @Component({ selector: 'self-introduction',

    template: `<p>私の名前は: {{ name }} です</p>`, }) export class SelfIntroductionComponent { private _name = ''; @Input() set name(value: string) { this._name = value.charAt(0).toUpperCase() + value.slice(1).toLowerCase(); } get name(): string { return this._name; } } <self-introduction [name]="'ILIJA'"></self-introduction> <!-- <p>私の名前は: Ilija です</p> --> コンポーネント初期化時に set フックを通る
  11. PHP RFC: Property hooks 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 14 •

    ⼀⾒シンプルな機能だがRFCは⻑⼤ • 発展的な使い⽅での動作を全て網羅 • 配列アクセス • シリアライズ‧デリジアライズ • 抽象プロパティ • 継承と変性 これらを読み解くことが オブジェクト指向や型システムの 理解につながる (以下、スクロール30画⾯分)
  12. set の型の拡張 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 15 class User {

    public DateTime $birthDate { set(DateTimeInterface|Stringable|Closure|int|float|string $value) { if($value instanceof Closure) { $value = $value(); } if($value instanceof DateTime) { $this->birthDate = DateTime::createFromInterface($value); return; } if(is_int($value) || is_float($value)) { $this->birthDate = DateTime::createFromTimestamp($value) ->setTime(0, 0); return; } if($value instanceof Stringable) { $value = $value->__toString(); } $this->birthDate = new DateTime ($value) ->setTime(0, 0); } } } $user = new User(); $user->birthDate = '2006-01-02'; var_dump($user->birthDate); $user->birthDate = 1136160000; var_dump($user->birthDate); $user->birthDate = new DateTimeImmurable('2006-01-02'); var_dump($user->birthDate); $user->birthDate = fn() => '2006-01-02'; var_dump($user->birthDate); $user->birthDate = new class { public function __toString(): string { return '2006-01-02'; } }; var_dump($user->birthDate); • プロパティの型は DateTime • 代⼊時はそれ以外の型も受け⼊れる これらのvar_dumpは全て 等値な値を返す
  13. アロー関数と引数の省略 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 16 class User { public

    string $name { get { return ucfirst($this->name); } set(string $value) { $this->name = strtolower($value); } } } class User { public string $name { get => ucfirst($this->name); set(string $value) => strtolower($value); } } class User { public string $name { get => ucfirst($this->name); set => strtolower($value); } } ⼀⽂の場合アロー関数で短縮可能 setの型が変わらない場合引数を省略可能 最も柔軟に実装できる書き⽅
  14. 仮想プロパティとバックドプロパティ 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 17 class Circle { public

    function __construct( public float $radius ) { } public float $area { get => pi() * $this->radius ** 2; } public float $circumference { get => 2 * pi() * $this->radius; } } $circle = new Circle(5); var_dump($circle); // object(Circle)#1 (1) { // ["radius"]=> // float(5) // } var_dump($circle->area); // float(78.53981633974483) var_dump($circle->circumference); // float(31.41592653589793) 実体を持つ = バックドプロパティ 実体を持たない = 仮想プロパティ
  15. 仮想プロパティの条件 バックドプロパティ: •$name のフックで •$this->name を参照する 仮想プロパティ • $area のフックで

    • $this->area を参照しない 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 18 class User { public string $name { get => ucfirst($this->name); } } class Circle { public float $area { get => pi() * $this->radius ** 2; } } フックの中で⾃⾝で同名の実体を正確に参照していればバックドプロパティ
  16. 誤った仮想プロパティによるエラー 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 19 • $this->foo を「正確に」参照していないため仮想プロパティ •

    仮想プロパティなので $this->>foo には実体が無い • 存在しないもの参照しようとしエラー class Example { public string $foo { get { $temp = 'foo'; return $this->$temp; // 展開すると $this->foo } } } new Example()->foo; // Uncaught Error: Must not read from virtual property Example::$foo
  17. &get によるリファレンスの取得 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 20 • 取得を &get

    とすればリファレンスを⼊⼿できる • リファレンスを経由し get から set できる class User { public ?string $name = null { &get { return $this->name; } } } $user = new User(); $name =& $user->name; $name = 'Gina'; var_dump($user); // object(User)#1 (1) { // ["name"]=> // &string(4) "Gina"
  18. &get と set の併⽤ 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 21 •

    バックドプロパティでの &get と set の併⽤は禁⽌ • 仮想プロパティでは併⽤できる(set する実体が無いため) class User { public ?string $name = null { &get { return $this->name; } set(?string $value) { $this->name = strtolower($value); } } } // Fatal error: // Get hook of backed property User::name with set hook may not return by reference
  19. コンストラクタプロモーションとの併⽤ 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 22 class User { private

    string $name; public function __construct( string $name, ) { $this->name = $name; } } class User { public function __construct( private string $name, ) { } } class User { public function __construct( public string $name { get => ucfirst($this->name); set => strtolower($value); } ) { } } 型宣⾔を コンストラクタへ移動 移動した先へ 同じようにフックを定義
  20. コンストラクタプロモーションと型拡張 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 23 class User { public

    function __construct( public DateTime $birthDate { set(DateTimeInterface|Stringable|string|Closure|int|float $value) { // 省略 } } ) { } } $user = new User(new DateTime ('2006-01-02')); // 問題なし $user->birthDate = '2006-01-02'; // 問題なし $user = new User('2006-01-02'); // User::__construct(): // Argument #1 ($birthDate) must be of type DateTime, string given 広げたのはあくまで set の型 コンストラクタの型は DateTime のまま
  21. コンストラクタプロモーションと型拡張 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 24 コンストラクタの型から set の型へ •

    「広げる」ことは可能 • 「狭める」「変更する」のは不可能 class User { public DateTime $birthDate { set(string $value) { $this->birthDate = new DateTime ($value) ->setTime(0, 0); } } } // Type of parameter $value of hook User::$birthDate::set // must be compatible with property type set の型を「変更した」場合 型エラー
  22. 「整数」を継承し「⾃然数」を作成 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 26 • set フックでのバリデーションで値を制限 •

    ここではPHP上の型は変わらない(共に int 型) class Integer { public function __construct( public int $value, ) { } } $integer = new Integer(-1); // 問題なし class NaturalNumber extends Integer { public int $value { set { assert($value > 0); $this->value = $value; } } } $naruralNumber = new NaturalNumber(-1); // Uncaught AssertionError: // assert($value > 0)
  23. 「数」を継承し「整数」を作成 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 27 class Number { public

    function __construct( public int|float $value ) { } } class Integer extends Number { public int|float $value { set { assert(floor($value) === (float)$value); $this->value = $value; } } } $integer = new Integer(-1); // 問題なし $integer = new Integer(-1.1); // Uncaught AssertionError: // assert(floor($value) === (double)$value) ¥PHPStan¥dumpType(new Integer(1)->value) // int|float int に変更したい 変更できない(何故?) Type of Integer::$value must be int|float (as in class Number)
  24. 仮想プロパティと継承先の型変更 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 28 class Number { public

    function __construct( protected int|float $_value ) { } public int|float $value { get => $this->_value; } } var_dump(new Number(42.23)->value); // float(42.23) ¥PHPStan¥dumpType(new Number()->value); // Dumped type: float|int class Integer extends Number { public int $value { get => (int)$this->_value; } } var_dump(new Integer(42.23)->value); // int(42) ¥PHPStan¥dumpType(new Integer()->value); // Dumped type: int 仮想プロパティにすれば 変更できる(何故?) この理由を紐解けば リスコフの置換原則を理解できる
  25. リスコフの置換原則‧共変と反変 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 31 • 反変: (⼊⼒‧引数 の型を)狭めてはいけない

    • 共変: (出⼒‧返却 の型を)広げてはいけない • 不変: 反変かつ共変 = 型を狭めても広げてもいけない = 変更してはいけない
  26. 現実世界の例で考える「毎朝の散歩」 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 33 1. 幾つかの100円⽟と10円⽟を持ち出発 2. ⾃販機で150円のペットボトル飲料(500ml)を買う

    • 売り切れが多くいつも同じ飲料を変えるとは限らない • 必ず何か買う。「買わない」という選択肢は無しとする 3. 買った飲料をペットボトルホルダー(500ml⽤)に⼊ れて出発 「⾃販機」が交換されることになったとして このワークフローが「壊れる or 壊れない」条件を考える
  27. ⼊⼒型の変更 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 35 元の⼊⼒型: list<硬貨<100円|10円>> $coins •

    🙆 飲料が値下げされた。150円から100円になった。 • 価格は変わったが型(受け⼊れる硬貨の種類)は変わっていない。 • ❌ 値下げに伴い⾃動販売機が「100円⽟専⽤」になった。 • 10円⽟10枚を持ってきていたケース。 • 「⼊⼒を狭める」の例: list<硬貨<100円>> $coins • 🙆 ⾃動販売機が硬貨に加えQRコード決済にも対応した。 • 100円⽟や10円⽟はそのまま使える。 • 「⼊⼒を広げる」の例: list<硬貨<100円|10円>>|QRコード決済 $coins • ❌ ⾃動販売機がQRコード決済専⽤になった。 • 100円⽟や10円⽟はもう使えない。 • 互換性のない型: QRコード決済 $coins
  28. 出⼒型の変更 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 36 元の出⼒型: ペットボトル<500ml> • 🙆

    取り扱う飲料の種類が増えた • 飲料の種類は増えても出⼒型(飲料のパッケージ形態)は変わっていない • ❌ 1lのペットボトル飲料も扱うようになった • ペットボトルホルダー(500ml⽤)に⼊らない • 「出⼒を広げる」の例: ペットボトル<500ml|1l> • ❌ ⽸を扱うようになった • ペットボトルホルダー(500ml⽤)に⼊らない • ⽸「も」扱う → 「出⼒を広げる」に相当: ペットボトル<500ml>|缶 • ⽸「だけ」扱う → 互換性のない型: ⽸
  29. 反変‧共変とは 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 37 ⾔葉の意味: How 反変: (⼊⼒‧引数

    の型を)狭めてはいけない 共変: (出⼒‧返却 の型を)広げてはいけない 設計の意味: Why • ❌: わざわざ守らなければならない⾯倒なルール • ✅: 実装を破壊的変更から守る安全装置 • 👍: ⾔葉の難しさに⼾惑うが考え⽅は簡単
  30. 余談: Animal メンタルモデルの問題点 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 38 • PHPマニュアル

    ❌ 反変性とは、親クラスのものよりも、より 抽象的な、広い型を引数に指定することを許 すものです。 • リスコフの置換原則 - Wikipedia ❌ 事前条件(preconditions)を、派⽣型で 強めることはできない。派⽣型では同じか弱 められる。 abstract class Animal { abstract public function eat(PetFood $food); } class Cat extends Animal { public function eat(CatFood $food) { // } } class Kitten extends Cat { public function eat(Milk $food) { // } } 例題で頻出する Animal, Dog, Cat は リスコフの置換原則に全く適合しない
  31. プロパティフックで型変更を⾏える理由 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 39 $value は: • 仮想プロパティ

    • フックのは⾃⾝の実体を参照しない • set操作を持たない • getしか実装していない • 仮想プロパティなのでデフォルトの set 操 作(単なる代⼊)も無い • 「出⼒」しかない • 以上より、共変である • = 型を「狭める」ことができる class Number { public function __construct( protected int|float $_value ) { } public int|float $value { get => $this->_value; } } class Integer extends Number { public int $value { get => (int)$this->_value; } } 狭める
  32. まとめ 2025-06-28 PHP 8.4の新機能「プロパティフック」から学ぶ オブジェクト指向設計とリスコフの置換原則 41 • プロパティフックの紹介 • 他の⾔語やフレームワークでは既に広く使われている機能

    • 「基本形」はシンプルなのできっと簡単に使える • PHPの型システムとの適合のため⼀部難解な箇所もある • ほとんどは「継承」に関するものなので、それを避ければ⼤丈夫 • 継承とリスコフの置換原則‧共変性と反変性 • 反変: (⼊⼒‧引数 の型を)狭めてはいけない • 共変: (出⼒‧返却 の型を)広げてはいけない • 「守るべき⾯倒なルール」ではなく「実装を破壊的変更から守る安全装置」 • プロパティフックによる型システムのパラダイムシフト • プロパティが変性(共変性‧反変性)を持てるようになった • 「抽象プロパティ」が可能になった