【PHP8.1】読めるけど書けないプロパティが作れるようになる

`新潟`←ちょっとこれを読んでみてください。

読めましたね。

ではちょっと画面から目を離して、`niigata`←これを漢字で書いてみてください。

8割くらいの人は書けなかったんじゃないかなと思います。

ということで、`読めるけど書けない漢字`になぞらえて、`読めるけど書けないプロパティ`は[新潟アクセス修飾子](https://www.google.com/search?q=%E6%96%B0%E6%BD%9F%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E4%BF%AE%E9%A3%BE%E5%AD%90)と呼ばれています。

```php
	echo $object->property; // できる
	$object->property = 1; // できない
```

もちろんこの呼称はネタですが、でも実際protectedやprivateを使う理由の98%は『読まれてもいいけど書き込ませたくない』だけじゃないですか。
そのためだけにいちいち値を出し入れするだけの`setter/getter`を作ったり`__set/__get`あたりでなんか技巧を凝らしたりとか無駄なことをやっていたわけですが、ついにPHP本体でこの制御が可能になります。

以下は[Readonly properties 2.0](https://wiki.php.net/rfc/readonly_properties_v2)のRFCの紹介です。

# Readonly properties 2.0

## Introduction

このRFCでは、初期化した後のプロパティ変更を防ぐ`readonly`プロパティ修飾子を導入します。

バリューオブジェクトはしばしばimmutableです。
コンストラクタでいちど初期化され、その後は変更できないようにされます。
PHPでは現在、この制約を強制する方法がありません。
今のところもっとも近い制約は、`private`で宣言して`public`なgetterを公開することです。

```php
class User {
    public function __construct(
        private string $name
    ) {}
 
    public function getName(): string {
        return $this->name;
    }
}
```

実際にプロパティが読み取り専用になるわけではありませんが、修正が必要な範囲を一か所に限定することができます。
しかし残念ながらgetterを定義する必要があるため、人間工学的によろしくない構造となります。

読み取り専用のプロパティをファーストクラスでサポートすることによって、`public`かつ`readonly`なプロパティを直接公開できるようになります。
外部からの干渉によってimmutableが破られる心配はありません。

```php
class User {
    public function __construct(
        public readonly string $name
    ) {}
}
```

## Proposal

`readonly`プロパティ修飾子は、同じスコープから一度だけ初期化することができます。

```php
class Test {
    public readonly string $prop;
 
    public function __construct(string $prop) {
        // OK
        $this->prop = $prop;
    }
}
 
$test = new Test("foobar");
// 読み込みはOK
var_dump($test->prop); // string(6) "foobar"
 
// 書き込みはNG
$test->prop = "foobar"; // Error: Cannot modify readonly property Test::$prop
```

クラス外からの初期化は許されません。

```php
class Test {
    public readonly string $prop;
}
 
$test = new Test;
$test->prop = "foobar"; // Error: Cannot initialize readonly property Test::$prop from global scope
```

単純な代入だけではなく、以下のような変更、参照操作も全て禁止されます。

```php
class Test {
    public function __construct(
        public readonly int $i = 0,
        public readonly array $ary = [],
    ) {}
}

$test = new Test; // ↓は全てNG
$test->i += 1;
$test->i++;
++$test->i;
$test->ary[] = 1;
$test->ary[0][] = 1;
$ref =& $test->i;
$test->i =& $ref;
byRef($test->i);
foreach ($test as &$prop);
```

ただし、オブジェクトの内部値は変更可能です。

```php
class Test {
    public function __construct(public readonly object $obj) {}
}
 
$test = new Test(new stdClass);

$test->obj->foo = 1; // これはOK

$test->obj = new stdClass; // オブジェクト自体の変更はNG
```

### Restrictions

`readonly`修飾子は、型付きプロパティにのみ適用可能です。
理由として、型付きではないプロパティは暗黙のデフォルト値nullを持っているので、これが初期化と判定されてしまうためです。

型制約を付けたくないプロパティに`readonly`修飾子を適用したいときは、PHP8.0で導入された[mixed型](https://qiita.com/rana_kualu/items/4685f2c7693ce0394fe8)を使うことができます。

```php
class Test {
    public readonly mixed $prop;
}
```

代替案として、`readonly`修飾子がついた方のついていないプロパティには暗黙のデフォルト値nullを適用しないという案もありました。
しかしこちらは暗黙のデフォルト値に関するルールが複雑になってしまい混乱するため、単純に禁止することとしました。

また、`readonly`修飾子のついたプロパティにデフォルト値を指定することはできません。

```php
class Test {
    public readonly int $prop = 42; // Fatal error: Readonly property Test::$prop cannot have default value
}
```

これは単なる定数であり、特に有用なものではありません。
将来的にプロパティデフォルト値として式が許可されるようになれば、`readonly`修飾子のデフォルト値も有用になるかもしれません。

[コンストラクタ引数昇格](https://qiita.com/rana_kualu/items/122a1da3b6089b125cb0)におけるプロパティデフォルト値は、プロパティではなく引数に適用されます。

```php
class Test {
    public function __construct(
        public readonly int $prop = 0,
    ) {}
}
 
// ↑は↓と同じ
 
class Test {
    public readonly int $prop;
 
    public function __construct(int $prop = 0) {
        $this->prop = $prop;
    }
}
```

このプロパティはデフォルト値を持っていないため、コンストラクタで初期化することができます。

`readonly static`プロパティはサポートされていません。
これが有用であるかは疑問であり、現時点ではこの機能をサポートする価値はないと考えています。


### Inheritance

継承時に、`readonly`プロパティをついていないプロパティで上書きしたり、その逆を行うことはできません。
以下の例は両方とも禁止されます。

```php
// 通常プロパティからreadonlyに
class A {
    public int $prop;
}
class B extends A {
    public readonly int $prop; // Illegal: readwrite -> readonly
}

// readonlyから通常プロパティに
class A {
    public readonly int $prop;
}
class B extends A {
    public int $prop; // Illegal: readonly -> readwrite
}
```

子クラスでの`readonly`追加を禁止する必要があるのは明らかです。
なぜならば、親クラスで実行した操作がエラーになる可能性があるからです。

子クラスで`readonly`を解除すると、親クラスが想定していた不変性が破壊されます。
従って、`readonly`修飾子は継承時に追加も削除もできません。


```php
class A {
    public readonly int $prop;
}
class B extends A {
    public readonly int $prop;
}
```

上記例では、AとB両方でプロパティ宣言されており、どちらのクラスからでも初期化が可能です。

同じプロパティが2つのトレイトからインポートされる場合、`readonly`も一致しないといけません。

```php
trait T1 {
    public readonly int $prop;
}
trait T2 {
    public int $prop;
}
class C {
    use T1, T2; // Illegal: Conflicting properties.
}
```

`readonly`プロパティの型は不変です。
共変であると考えることもまあできます。

```php
class A {
    public readonly int|float $prop;
}
class B extends A {
    public readonly int $prop;
}
```

共変はプロパティの読み込みには有効ですが、初期化の場合はAとB両方から初期化可能可能なため、あまり意味がありません。
将来的に共変セマンティクスの緩和を考慮する可能性はあります。

### Unset

一度初期化されると削除はできません。

```php
class Test {
    public readonly int $prop;
 
    public function __construct() {
        $this->prop = 1;
        unset($this->prop); // Error: Cannot unset readonly property Test::$prop
    }
}
```

ただし、初期化する前であれば削除することが可能です。
その後は通常のプロパティ同様、マジックメソッド`__get`等が反応するようになり、遅延初期化などを行えるようになります。

```php
class Test {
    public readonly int $prop;
 
    public function __construct() {
        unset($this->prop);
    }
 
    public function __get($name) {
        if ($name === 'prop') {
            $this->prop = $this->lazilyComputeProp();
        }
        return $this->$name;
    }
}
```

### Reflection

`ReflectionProperty::isReadOnly()`が追加され、プロパティが`readonly`か否かを確認できるようになります。
`ReflectionProperty::getModifiers()`が、`ReflectionProperty::IS_READONLY`フラグを返すようになります。

`ReflectionProperty::setValue()`は、同じスコープからしか初期化できないという制限を回避し、初期化することができます。
ただし、既に初期化されたプロパティを変更することはできません。

### Serialization

`readonly`はシリアライズに影響を与えません。
`__unserialize()`はコンストラクタを実行しないため、`readonly`プロパティは初期化されていない状態となり、`__unserialize()`で任意に初期化することができます。

同様に`ReflectionClass::newInstanceWithoutConstructor()`などでコンストラクタを回避した場合は、`readonly`プロパティは初期化されません。

## Rationale

このRFCで導入される`readonly`プロパティは、クラスの内部と外部両方に、強力な不変性の保証を提供します。
一度初期化されたプロパティは、いかなる方法を用いても変更することはできません。
間にどのような処理を行おうと、`readonly`プロパティは常に同じ値を返します。

```php
class Test {
    public readonly string $prop;
 
    public function method(Closure $fn) {
        $prop = $this->prop;
        $fn(); // ここにどんな中身を書いたとしても
        $prop2 = $this->prop;
        assert($prop === $prop2); // 必ずtrue
    }
}
```

この保証は、一部のユースケースについては強力すぎるかもしれません。
たとえば、外部からは読み込み専用だが、内部からは書き込み可能なプロパティがほしいといった場合です。
これはオブジェクトの生存期間中にプロパティ値が変更される可能性があるため、`readonly`よりは弱い保証となります。
いずれの形式も状況に応じて有用であり、`readonly`が追加されたからといって、そのようなプロパティの追加が妨げられるわけではありません。

該当のケースの使用例を挙げておきます。

```php
class Point {
    public function __construct(
        public readonly float $x,
        public readonly float $y,
        public readonly float $z,
    ) {}
 
    public function withX(float $x): static {
        // これは動く
        return new static($x, $this->y, $this->z);
        
        // こっちは動かない
        $clone = clone $this;
        $clone->x = $x;
        return $clone;
    }
}
```

`clone`での実装は、`clone`したオブジェクトのプロパティ`$x`は既に初期化済みのため、エラーになります。


## Backward Incompatible Changes

`readonly`キーワードが予約語になります。

読み込み可能なプロパティは全て書き込み可能でもあるという前提で書かれている既存のコードは動作しなくなる可能性があります。


## Vote

投票期間は2021/07/01から2021/07/15まで、投票者の2/3の賛成で受理されます。

2021/07/12時点では賛成36反対11の賛成多数であり、おそらく受理されます。


# 感想

コンストラクタで一度だけ書き込んで、その後は読み取り専用にしたい、という極めてよくある要望が、ようやく言語レベルでサポートされます。

`const`だとスカラー式しか書けないので`new HOGE()`とか書けない、`private`・`protected`は外から見れないので使い勝手が悪い、`public`は好き勝手されるといった具合で、ちょうどいいレベルの制限がありませんでした。
今後は`readonly`を使うことで、プロパティが変な値で上書きされてる！とか気にしなくてよくなります。

[コンストラクタ引数昇格](https://qiita.com/rana_kualu/items/122a1da3b6089b125cb0)やデフォルトnewあたりと組み合わせることで、とてもいいかんじのオブジェクト初期化が書けるようになるでしょう。


# 始まる前に終わったかもしれないプロパティアクセサ構文

実は似たようなRFCとしてもうひとつ[Property Accessors](https://wiki.php.net/rfc/property_accessors)がありまして、どちらかというとこっちのほうに注目していたんですよね。
こちらは`readonly`の完全上位互換であり、さらなる高度な制御をかますことが可能になります。

```php
class Test {
    public int $prop {
        get { return 42; }
        private set
    }
}
```

[C#のプロパティアクセサ](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/properties)とだいたい同じ構文です。

しかしまあ実際、プロパティアクセサ構文のユースケースの9割は`readonly`なわけですよ。
従って`readonly`が実装された後になってから、それほど使われない機能を新たな構文を突っ込んでまでProperty Accessorsを投入するのかと考えると、このRFCの行き先には怪しいものがありますね。

怪しいというか、元々Property AccessorsのRFCが先にあったんだけど、このRFCは色々と問題があったので機能限定版として`readonly`が作られた、というのが実際の経緯です。
何が問題って、Property Accessorsはエッジケースがすごいことになっていて、相当に巨大で複雑な規模のものになってしまっているのです。
どのくらいかって[あのNikitaが弱音を漏らすくらい](https://externals.io/message/114264)。

> このRFCの提案と実装に力を注いできたんだけど、この方向性が正しいのか不安になってきました。
> 当初の予定よりスコープを縮小したにも関わらず、それでも予想していたよりもはるかに複雑で、さらにまだ気付いていない副作用もあるかもしれません。
> その複雑さに見合う価値があるか疑問です。

そんなわけで10割のユースケースを満たす完璧かつ複雑な機能ではなく、9割のユースケースを満たす使いやすく簡潔な機能が実装されることになりました。
妥当な落としどころではないでしょうか。