PHPのプロパティフックについて

採用はこちら

PHPのプロパティフックは、プロパティの読み取り時や書き込み時に処理を差し込める仕組みです。

PHP 8.4で導入され、単純な getter / setter メソッドを多用せずに、プロパティアクセスの形を保ったまま振る舞いを定義できるようになりました。

たとえば、値を代入するときに自動で整形したり、読み出すときに計算結果を返したりできます。

そのため、値の正規化、簡単なバリデーション、派生値の公開といった用途で使いやすい機能です。

目次

基本構文

プロパティフックでは、getset を使って読み書き時の処理を定義します。

class User
{
    public string $name {
        get {
            return $this->name;
        }
        set(string $value) {
            $this->name = trim($value);
        }
    }
}

短く書ける場合は、矢印構文も使えます。

class User
{
    public string $name {
        set => trim($value);
    }
}

この例では、$name に値を代入すると、自動的に trim() が適用されてから保存されます。

できること

プロパティフックでできることは、大きく分けると次の3つです。

値の整形

代入された値を自動で整形できます。

class User
{
    public string $email {
        set => strtolower(trim($value));
    }
}

これにより、呼び出し側で毎回 trim()strtolower() を書かなくても、常に同じルールで保存できます。

値の検証

代入時に不正な値を拒否できます。

class Product
{
    public int $price {
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('price must be >= 0');
            }
            $this->price = $value;
        }
    }
}

このようにすると、負の価格が代入された時点で例外を投げられます。

計算結果を返す

保存されている値ではなく、読み取り時に計算した結果を返せます。

class Rectangle
{
    public function __construct(
        public int $width,
        public int $height,
    ) {}

    public int $area {
        get => $this->width * $this->height;
    }
}

この area は実際に保存している値ではなく、アクセスのたびに計算されます。

backed property と virtual property

プロパティフックでは、backed propertyvirtual property の区別があります。

backed property

実際に値を保持するプロパティです。

フックの中で、そのプロパティ自身を \$this->プロパティ名 の形で そのまま 参照すると、backed property として扱われます。

class Example
{
    public string $foo = 'default' {
        get => $this->foo . '!';
        set => strtolower($value);
    }
}

この場合、$foo には内部的な保存領域があります。

virtual property

実体の保存領域を持たず、getset の処理だけで成立するプロパティです。

たとえば、面積やフルネームのような派生値に向いています。

class Person
{
    public string $fullName {
        get => $this->firstName . ' ' . $this->lastName;
    }

    public function __construct(
        public string $firstName,
        public string $lastName,
    ) {}
}

fullName は値を保存していません。

読み取り時にその場で組み立てています。

get または set を省略した場合

backed property では、get または set のどちらか一方だけを書くことができます。

その場合、省略した側は通常のプロパティと同じ動作になります。

class User
{
    public string $name {
        set => trim($value);
    }
}

この例では、書き込み時だけフックが動き、読み取りは普通のプロパティアクセスです。

一方、virtual property には backing value がありません。

そのため、定義していない操作を行うとエラーになります。

たとえば get だけ定義した virtual property に代入することはできません。

set の型について

set フックの引数型は、プロパティの型と同じか、より広い型を指定できます。

つまり、外部からは広めに受け取り、内部では厳密な型に揃える、という設計ができます。

class Person
{
    public string $name {
        set(string|\Stringable $value) {
            $this->name = (string)$value;
        }
    }
}

この場合、最終的に保存されるのは string ですが、代入時には Stringable な値も受け取れます。

constructor property promotion との関係

constructor property promotion と組み合わせて使うこともできます。

ただし、ここには注意点があります。

set フックで広い型を受け取るようにしていても、constructor promotion を使った場合のコンストラクタ引数の型は、プロパティ宣言側の型になります。

class Example
{
    public function __construct(
        public DateTimeInterface $created {
            set(string|DateTimeInterface $value) {
                if (is_string($value)) {
                    $value = new DateTimeImmutable($value);
                }
                $this->created = $value;
            }
        }
    ) {}
}

このように書いても、コンストラクタ引数として文字列を直接渡せるわけではありません。

コンストラクタでは DateTimeInterface 型が要求されます。

スコープの扱い

フックの中のコードは、そのオブジェクト自身のスコープで動きます。

そのため、private メソッドや private プロパティにもアクセスできます。

class Person
{
    public string $phone {
        set => $this->normalizePhone($value);
    }

    private function normalizePhone(string $value): string
    {
        $value = preg_replace('/\D+/', '', $value);

        if (strlen($value) !== 11) {
            throw new InvalidArgumentException('invalid phone');
        }

        return $value;
    }
}

また、フックの中から別のフック付きプロパティにアクセスした場合は、その相手側のフックも通常どおり動作します。

参照返しと配列の注意点

プロパティフックは通常のプロパティアクセスに見えても、内部では getset が介在します。

そのため、参照や配列の部分更新では注意が必要です。

たとえば、配列プロパティに対して一部だけを書き換える処理は、内部的に参照が必要になることがあります。

このようなケースでは、get を参照返しにするために &get を使えます。

class Example
{
    private array $data = [];

    public array $items {
        &get => $this->data;
    }
}

ただし、backed property では &getset を同時に定義できません。

参照経由で値が変更されると、set を通らずに書き換えられてしまうためです。

readonly との関係

プロパティフックは、readonly プロパティとは互換性がありません。

読み取り専用にしたい場合は、property hooks ではなく、可視性の制御を使い分ける設計が必要になります。

たとえば、読み取りは public、書き込みは protected にしたい場合は、asymmetric visibility を使います。

class User
{
    public protected(set) string $name;
}

この書き方では、外部からは読み取れますが、書き込みはクラス自身または継承先に制限されます。

asymmetric visibility との違い

property hooks と asymmetric visibility は、似ているようで役割が異なります。

property hooks

読み書きの挙動を変えるための機能です。

整形、検証、計算などの処理を差し込む用途に向いています。

asymmetric visibility

読み書きできる範囲を変えるための機能です。

外部からの変更を禁止したい場合などに向いています。

つまり、何をするかを決めるのが property hooks で、誰ができるかを決めるのが asymmetric visibility です。

interface と abstract class での利用

PHP 8.4 では、interface や abstract class でもプロパティ要件を宣言できます。

interface での宣言

interface HasName
{
    public string $name { get; }
}

この場合、HasName を実装するクラスは、読み取り可能な name プロパティを持つ必要があります。

class UserA implements HasName
{
    public string $name;
}
class UserB implements HasName
{
    public string $name {
        get => 'Taro';
    }
}

通常のプロパティでも、フック付きプロパティでも要件を満たせます。

abstract class での宣言

abstract class でも、abstract property を宣言できます。

子クラスはそれを通常のプロパティまたはフック付きプロパティで実装できます。

__get() / __set() との違い

プロパティフックと __get() / __set() は似た用途に見えますが、設計上はかなり違います。

__get() / __set()

未定義またはアクセス不能なプロパティへのアクセスを、まとめて処理する仕組みです。

property hooks

個々のプロパティごとに、明示的に読み書きの挙動を定義する仕組みです。

そのため、property hooks のほうが、どのプロパティにどんな処理があるのかをコード上で追いやすく、型宣言とも並べて書けます。

継承時の補足

継承時には、親クラス側の hook やデフォルト動作にアクセスする仕組みもあります。

また、hook 自体に final を付けて、子クラスでの上書きを制限することもできます。

このあたりは、単純な利用ではあまり意識しなくてもよいですが、継承を多用する設計では重要になります。

シリアライズや出力時の注意

プロパティフックは、すべての場面で同じように値が扱われるわけではありません。

シリアライズやデバッグ出力では、hook を通る場合と raw backing value が使われる場合があります。

そのため、見た目上は同じプロパティでも、通常のアクセス時と出力時で扱いが変わることがあります。

この点は、デバッグや保存処理を行うときに意識しておくと安全です。

向いているケース

プロパティフックは、次のような用途に向いています。

  • 代入時の trim()strtolower() などの軽い正規化
  • 単純なバリデーション
  • 派生値の公開
  • 単純な getter / setter の削減

特に、値のルールを「代入地点」に集約したいときに効果的です。

向いていないケース

次のような用途では慎重に使ったほうがよいです。

  • 重い処理を伴う読み書き
  • I/O を伴う副作用の大きい処理
  • 配列や参照を多用する構造
  • 見た目はプロパティなのに、実態が複雑すぎる設計

プロパティアクセスは軽く自然に見えるため、内部処理が重すぎるとコードを読む側に負担をかけます。

実務での考え方

実務では、次のように使い分けると整理しやすくなります。

  • 1行で済む整形は短縮構文で書く
  • 条件分岐や例外処理がある場合はブロック構文で書く
  • 処理が長い場合は private メソッドへ分離する
  • 派生値は virtual property で表現する
  • 書き込み権限の制御は asymmetric visibility で行う

このように役割を分けると、コードの意図が見えやすくなります。

まとめ

PHPのプロパティフックは、プロパティの読み書きに処理を差し込める機能です。

値の整形、検証、派生値の公開といった用途で使いやすく、PHP 8.4 以降のオブジェクト設計で有力な選択肢になります。

理解するときの要点は次の通りです。

  • getset で読み書き時の挙動を定義できる
  • backed property と virtual property の違いを押さえる
  • readonly とは互換性がない
  • asymmetric visibility とは役割が異なる
  • interface や abstract class でもプロパティ要件を表現できる
  • 配列、参照、シリアライズでは注意点がある

ひとことで言えば、プロパティらしい書き心地のまま、メソッド的な制御を加えられる機能です。

以上、PHPのプロパティフックについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次