🙄

[PHP8.4より]getter/setterについてのメモ

2024/11/26に公開

何故 getter/setter が必要か

  • カプセル化: プロパティを直接公開せず、メソッドを通じてアクセスすることで、クラスの内部状態を隠蔽し、外部からの不正なアクセスや変更を防ぎます。
  • 検証: setter メソッド内で値の検証を行うことができます。これにより、不正な値がプロパティに設定されるのを防ぎます。
  • 柔軟性: 将来的にプロパティの取得や設定に追加のロジックが必要になった場合、getter や setter メソッドを変更するだけで済みます。直接プロパティにアクセスしている場合、すべてのアクセス箇所を修正する必要があります。
  • デバッグ: getter や setter メソッドを使用することで、プロパティのアクセスや変更時にログを記録したり、デバッグ情報を出力したりすることが容易になります。

getter/setterで実現したいこと

  • setter 状態を変更したいけど、むやみやたらにプロパティを変更したくない、イミュータブルに実現したい
  • getter 値を取得したいけど、冗長に宣言したくない、けど一貫性も意識したい
  • インスタンスの状態を正常にするためにも、getter/setterにロジック(バリデーション等)が必要

PHP8.4まで

8.4までのPHPでは微妙に細かい設定ができない。以下のパターン①から④の実装に落ち着くのかも?と考えています。

パターン①: readonlyを使用する

readonlyプロパティを使用することで、クラスのプロパティが初期化後に変更されないことを保証します。これにより、冗長なgetter/setterメソッドを避けることができます。readonlyプロパティは、コンストラクタでのみ値を設定でき、その後は変更できません。これにより、クラスの設計がシンプルになり、意図しないプロパティの変更を防ぐことができます。
しかし、readonlyは一度限りの書き込みを許しているため、constructor内でのsetが強要されます。プロパティが多いと、通常ならメソッド単位でまとめられるプロパティへのsetが一見ではわからず、冗長性を感じることになる。

<?php

class User
{
    public readonly string $name;
    public readonly int $age;

    public function __construct(string $name, int $age)
    {
        $this->name = $name;
        $this->age = $age;
    }
}

$user = new User('John Doe', 30);
echo $user->name; // John Doe
echo $user->age;  // 30

// $user->name = 'Jane Doe'; // エラー: readonlyプロパティは変更できません

パターン②: Tell, Don't Askに従いロジックで固める

オブジェクトに対して直接操作を指示し、内部状態を問い合わせることを避ける設計です。これにより、オブジェクトの内部状態を隠蔽し、操作の一貫性を保つことができます。

注: PHPのgetter/setterをどうするかというより、getterで取得する値を最終どうしたいかをメソッドにまとめましょうね的な位置付けなので、少し毛並みは違うかもしれません。

<?php

class Product
{
    private float $price;
    private string $name;

    public function __construct(string $name, float $price)
    {
        $this->name = $name;
        $this->price = $price;
    }

    public function increasePrice(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Increase amount must be positive');
        }
        $this->price += $amount;
    }

    public function decreasePrice(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Decrease amount must be positive');
        }
        if ($amount > $this->price) {
            throw new InvalidArgumentException('Decrease amount exceeds current price');
        }
        $this->price -= $amount;
    }
}

$product = new Product('Sample Product', 100.0);
$product->increasePrice(50.0);
$product->decreasePrice(30.0);

パターン③: getter/setterを全て記述する

一貫性を保つために、すべてのプロパティに対してgetter/setterを記述します。これにより、クラスの使用方法が統一され、コードの可読性と保守性が向上します。
しかし、その分冗長性がとんでもないことになる。

<?php

class Product
{
    private string $name;
    private float $price;

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function getPrice(): float
    {
        return $this->price;
    }

    public function setPrice(float $price): void
    {
        if ($price < 0) {
            throw new InvalidArgumentException('Price cannot be negative');
        }
        $this->price = $price;
    }
}

$product = new Product();
$product->setName('Sample Product');
$product->setPrice(100.0);
echo $product->getName();  // Sample Product
echo $product->getPrice(); // 100.0

パターン④: ロジックが必要なgetter/setterにだけロジックを追加

必要な場合にのみgetter/setterにロジックを追加し、シンプルなクラス設計を維持します。これにより、プロパティの値を設定する際に必要な検証や変換を行うことができます。
しかし、使う側がgetterの有無を意識する必要があり、getterの作り方も明文化していないと作り手によってずれが発生する恐れがある。

<?php

class Product
{
    private float $price;
    public readonly string $name;

    public function __construct(string $name, float $price)
    {
        $this->setName($name);
        $this->setPrice($price);
    }

    private function setName(string $name): void
    {
        $this->name = $name;
    }

    private function setPrice(float $price): void
    {
        if ($price < 0) {
            throw new InvalidArgumentException('Price cannot be negative');
        }
        $this->price = $price;
    }

    public function getPrice(): float
    {
        // 価格に税金を加算して返す
        return $this->price * 1.1;
    }
}

$product = new Product('Sample Product', 100.0);
echo $product->name;  // Sample Product
echo $product->getPrice(); // 110.0 (税金を加算した価格)

PHP8.4以降

PHP8.4より前では、上記、どれかのアプローチを行う必要があり、どうしても歪なgetter/setterになっていたのですが、PHP8.4で実装されたプロパティフック非対称可視性がこれらを全て解決し、綺麗にすることが可能となりそうです。

https://www.php.net/releases/8.4/ja.php

プロパティフックと非対称可視性を使用した時に起こること

  • 全てのgetter/setterを書く必要がなくなり、ロジックが必要なプロパティだけをプロパティフックで実装することが可能
    • 冗長性の回避
  • 各プロパティはpublicなgetが可能で、privateなsetができるようになり、また、一貫性を保つために宣言していたgetterは不要となり、プロパティを直接取得できるようになった
    • 一貫性の確保
<?php

class Product
{
    public private(set) string $name;

    public private(set) float $price
    {
        get => $this->price * 1.1;
        set (float $price) {
            if ($price < 0) {
                throw new InvalidArgumentException('Price cannot be negative');
            }
            $this->price = $price;
        }
    }

    public function __construct(string $name, float $price)
    {
        $this->name = $name;
        $this->price = $price;
    }
}
?>

$product = new Product('Sample Product', 100.0);
echo $product->name;  // Sample Product
echo $product->price; // 110.0 (税金を加算した価格)

まだ登場したてなので色々と勘違いしているかもですが、個人的にはgetter/setterの実装に関しての回答として確固たるものになりそうです。

補足. PHP8.4を試したい方へ

軽くPHP8.4.1を試したい方は、以下のアプローチが簡単かと思います。

docker pull php:8.4.1-cli
docker run -it --rm php:8.4.1-cli

- `-it` : コンテナに対話モードで接続。
- `--rm` : コンテナ終了時に自動削除。
- `php:8.4.1-cli` : 使用するイメージ名。

Discussion