【PHP Next】非対称な可視性を導入したい

Swiftでは読み込みと書き込みで[異なる可視性を設定することができます](https://qiita.com/codelynx/items/43e1c4f176730d952d13)。

```swift
private (set) public var count: Int = 0 // 読み込みはpublic、書き込みはprivate
```

そんなわけで節操のないPHPは、この機能もPHPに取り込もうと企んでいます。
以下は該当のRFC、[Asymmetric Visibility](https://wiki.php.net/rfc/asymmetric-visibility)の紹介です。


# Asymmetric Visibility

## Introduction

PHPには、前からプロパティの可視性を制御する機能があります。
しかし、その機能はgetとset両方に対して常に同じです。

本RFCでは、プロパティの読み取り操作と書き込み操作に対して異なる可視性を与えることを提案します。
具体的にはほぼSwiftから拝借しています。

## Proposal

プロパティの書き込み操作の可視性を宣言するための新しい`set`構文を提供します。

```php
class Foo
{
    public private(set) string $bar;
}
```

このプロパティ`$bar`は、読み込みはpublicスコープですが、書き込みはprivateスコープです。
また`protected(set)`とすればprotectedスコープになります。

このプロパティの挙動は、可視性が制限されていること以外、通常のプロパティと全く同じです。

プロパティのset可視性は、コンストラクタ引数昇格と共に使用することもできます。

```php
class Foo
{
    public function __construct(
        public private(set) string $bar,
    ) {}
}
```

### References

set可視性が指定されたプロパティへのリファレンスは、書き込みが可能なスコープからしか取得できません。
別の言い方をすると、リファレンスは読み込みの可視性ではなく、書き込みの可視性に従います。

```php
class Foo
{
    public protected(set) int $bar = 0;
 
    public function test() {
        // OK 同クラスなのでprivateは見える
        $bar = &$this->bar;
        $bar++;
    }
}
 
class Baz extends Foo
{
    public function stuff() {
        // OK 子クラスなのでprotectedは見える
        $bar = &$this->bar;
        $bar++;
    }
}
 
$foo = new Foo();

// OK
$foo->test();
 
// これもOK
$baz = new Baz();
$baz->stuff();

// NG privateには外からアクセスできない
$bar = &$foo->bar;
```

### Object properties

対象のプロパティがオブジェクトである場合、set可視性はオブジェクトの変更にのみ適用されます。
オブジェクト自体には影響を与えません。
これはreadonlyプロパティの挙動とも一致します。

```php
class Bar
{
    public string $name = 'beep';
}
 
class Foo
{
    public private(set) Bar $bar;
}
 
$f = new Foo();
 
// OK これは許される
$f->bar->name = 'boop';
 
// NG Foo::$barは外から見れない
$f->bar = new Bar();
```


### Permitted visibility

getとsetの可視性が異なる場合、setの可視性はgetより狭くなければなりません。
すなわち、setに設定可能な可視性は`protected`と`private`だけです。
getの可視性が`protected`である場合、setに設定可能な可視性は`private`だけです。
違反した場合はコンパイルエラーになります。


### Interaction with __set

クラスにマジックメソッド`__set`が定義されているときに、不正なスコープからアクセスがあった場合は`__set`が呼び出されます。
これは別の目的でクラスに`__set`を実装したときに、なるべく想定外の動作をしないためのものです。
これはreadonlyの実装においてもそうなっています。

従って、非対称な可視性についても同じ動作にします。
読み込みの可視性が一致しない場合は`__set`が呼び出されます。
書き込みの可視性が一致しない場合はエラーになります。

```php
class Foo {
    protected private(set) string $prot;
    public private(set) string $pub;

    public function __set($name, $value) {
        var_dump($name, $value);
    }
}

$foo = new Foo();

$foo->prot = 'Foo'; // __setが呼ばれる

$foo->pub = 'Foo'; // エラーになる
```


### Relationship with readonly

`public private(set)`は一見`readonly`と同じに見えるかもしれませんが、実際は少々異なります。
`readonly`は`public`からの書き込みは許されず、`private`からの書き込みは一度だけ可能です。
`public private(set)`は`public`からの書き込みは許されず、`private`からの書き込みは無制限に可能です。

ひとつのプロパティに`readonly`と非対称な可視性の両方を使用することはできません。


### Typed properties

非対称な可視性は、型が明示されたプロパティにのみ適用可能です。
これは主に実装の複雑さに起因する制限です。

値が不明なプロパティにはmixed型を設定できるため、実用上の問題にはならないでしょう。


### Reflection

ReflectionPropertyに2つのメソッド、`isProtectedSet():bool`と`isPrivateSet():bool`が追加されます。
意味は自明でしょう。

```php
class Test
{
    public string $open;
    public protected(set) string $restricted;
}
 
$rClass = new ReflectionClass(Test::class);
 
$rOpen = $rClass->getProperty('open');
print $rOpen->isProtectedSet() ? 'Yep' : 'Nope'; // Nope
 
$rRestricted = $rClass->getProperty('open');
print $rRestricted->isProtectedSet() ? 'Yep' : 'Nope'; // Yep
```

また定数`ReflectionProperty::IS_PROTECTED_SET`と`ReflectionProperty::IS_PRIVATE_SET`が追加されます。
これらの扱いは`ReflectionProperty::getModifiers()`から返される他の可視性修飾子と同様です。

## Backward Incompatible Changes

後方互換性のない変更はありません。


## Proposed PHP Version(s)

PHP8.3？

## Future Scope

この節は将来の展望であり、本RFCには含まれていません。

### Alternate operations

現時点では、スコープに設定可能な操作は書き込みと読み込みだけです。
今後、他にも以下のような操作が追加されるかもしれません。

・`init`。`__construct`・`__clone`・`__unserialize`といった初期化処理からのみ設定可能なプロパティ。
・`once`。一度だけ書き込み可能。`public private(once)`は`readonly`と完全に同一ですが、`public protected(once)`は子クラスからも書き込み可能です。
・`unset`。プロパティのunsetを可能にします。

### Additional visibility

パッケージレベルの可視性などが導入された場合、`public package(set)`などが使用可能になるでしょう。

### Property accessors

非対称の可視性については、以前から[Property Accessors](https://wiki.php.net/rfc/property_accessors)のRFCとして提案されていました。
該当のRFCはC#の構文をモデルとして書かれています。

いっぽう本RFCはSwiftの構文を借用していますが、これはC#スタイルに比べて2つの利点があります。
・全ての可視性が一か所に固まっているため、混乱が少ない。
・プロパティアクセサのRFCが直面している問題は影響しない。

このRFCは、将来的なプロパティアクセサの導入を排除したり制限したりするものではありません。


## References

[Swiftの同機能](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html)。


# 感想

かつてNikitaがPHP8.1への導入を目指していたものの[諦めた](https://externals.io/message/114264)[Property Accessors](https://wiki.php.net/rfc/property_accessors)構文の代替品です。
RFCでは排除しないと言ってますが、ほぼ同じ機能なのでもし本RFCが採択されたとしたらプロパティアクセサを導入する意味はほとんどなくなるでしょう。

先日提出されたばっかりのRFCなのでまだどうなるかわかりませんが、個人的にはプロパティアクセサも含めて[readonly](https://qiita.com/rana_kualu/items/ae96400a0a6981eca64a)でほぼカバーできる内容だと思っているので、本RFCの需要が何処まであるかはよくわかりません。

あと`private public(set)`とは定義できません。
書けるけど読めないプロパティにどんな意味があるかはわかりませんが。