【PHP8.2】動的プロパティが禁止される

[ゆるふわPHP絶対殺すマン](https://qiita.com/rana_kualu/items/c1b5397f9232d4c38f91#%E6%84%9F%E6%83%B3)である[Nikita Popov](https://github.com/nikic/)が、いよいよ本気を出してきました。


```php
$dt = new DateTime();
$dt->hoge = 1;
var_dump($dt->hoge); // 1
```

PHPでは未定義のプロパティに値を突っ込むと、特に何の抵抗もなくプロパティが生えます。
しかし、他の多くの言語ではこのような動作にならず、未定義プロパティを突っ込もうとするとエラーになります。
ということでこれを禁止しようというRFCが提出されました。

以下は[Deprecate dynamic properties](https://wiki.php.net/rfc/deprecate_dynamic_properties)のRFCの紹介です。


# PHP RFC: Deprecate dynamic properties

## Introduction

宣言されていないプロパティに書き込みを行うと、PHPでは何もエラーが出ずに動的にプロパティが作成されます。
現代のコードにおいて、これが意図的に行われることはほとんどありません。
このRFCでは、動的プロパティを非推奨とし、将来的に削除することを提案します。

```php
class User {
    public $name;
}
 
$user = new User;
$user->name = "foo"; // OK

// プロパティ名を間違えた
$user->nane = "foo";
// PHP <= 8.1: 黙って$user->naneが作られる
// PHP    8.2: E_DEPRECATEは出るけど$user->naneは作られる
// PHP    9.0: Exception
```

ここで言う動的プロパティとは、クラス内で宣言されていないプロパティのことであり、アクセスする方法には関係ありません。
たとえば`$user->{'na' . 'me'}`は今後も正しいプロパティアクセスのままです。

## Proposal

`stdClass`を継承していないクラスに動的にプロパティを作成することは、PHP8.2では非推奨にします。
PHP9では`ErrorException`となります。
使用するプロパティは全てクラス内で宣言しなければなりません。

```php
class Foo {}
$foo = new Foo;
 
// Deprecated: Creation of dynamic property Foo::$bar is deprecated
$foo->bar = 1;
 
// ↑で既にプロパティbarができたので、こっちはE_DEPRECATEDは出ない
$foo->bar = 2;
```

ただし、`stdClass`を継承したクラスは引き続き動的プロパティをサポートします。

```php
$obj = (object) []; // = new stdClass;

// エラー出ない
$obj->foo = 1;
 
class myStdClass extends stdClass {}
$obj2 = new myStdClass;

// エラー出ない
$obj2->bar = 1;
```

`stdClass`オブジェクトは、動的プロパティを保持することを目的としてよく使用されます。
従って`extends stdClass`は、動的プロパティの使用を目的とした移行手段として今後も提供されます。

また、`__get()/__set()`が定義されている場合の未定義プロパティへのアクセスは、動的プロパティとはみなされません。
以下の例では非推奨の警告は発生しません。

```php
class ArrayLikeObject {
    private array $data = [];
    public function &__get($name) { return $this->data[$name]; }
    public function __isset($name, $value) { return isset($this->data[$name]; }
    public function __set($name, $value) { $this->data[$name] = $value; }
    public function __unset($name) { unset($this->data[$name]; }
}

$obj = new ArrayLikeObject;

// ArrayLikeObject::__set()が呼ばれるので、E_DEPRECATEDにはならない
$obj->foo = 1;
```

## Backward Incompatible Changes

動的プロパティのサポートを取りやめることは、下位互換性を大きく損ないます。
モダンなPHPコードは一貫してプロパティ宣言が使われていますが、レガシーコードは必ずしもそうではありません。

動的プロパティの警告に遭遇した場合、回避するためにできることはいくつかあります。
もっとも単純で一般的な解決策は、プロパティ宣言を追加することです。

```php
class Test {
    public $value; // ←これを追加するだけ

    public function __construct($value) {
        $this->value = $value;
    }
}
```

意図的にプロパティ宣言を持たないクラスの場合は、マジックメソッド`__get()/__set()`を実装するか、`stdClass`を継承するか、`ARRAY_AS_PROPS`モードの[ArrayObject](https://www.php.net/manual/ja/class.arrayobject.php)を継承するか、で対応できます。

`__get()/__set()`の継承が最もコントロールしやすくなります。
stdClassの継承は、PHPエンジンの最適化された動的プロパティ処理の恩恵を受けることができ、また`foreach`や`property_exists()`等の動作が現在の動作に最も近いものになります。

自身が知らないオブジェクトに情報を紐付けたいことがあります。
これまでは、この目的のために動的プロパティが使われていました。
かわりにWeakMapを使って邪魔にならない方法で情報を保存することができます。

```php
class Test {
    private WeakMap $extraInfo;
 
    public function addExtraInfo(object $obj) {
        // こっちのかわりに
        $obj->extraInfo = ...;
        // こっちを使う
        $this->extraInfo[$obj] = ...;
    }
}
```

稀に、動的プロパティが遅延初期化に使われることがあります。
たとえばSymfonyにおいて[Constraint::$groups](https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Validator/Constraint.php)プロパティは宣言されておらず、`__get()`が呼ばれたときに動的に生成されます。
この場合は、プロパティ宣言したあとにコンストラクタで削除することで対応できます。

```php
abstract class Constraint {
    public $groups;
 
    public function __construct() {
        unset($this->groups);
    }
 
    public function __get($name) {
        // groupsへの初回アクセス時に呼ばれる
        $this->groups = ...;
    }
}
```

一度宣言したプロパティは、`unset()`しても動的プロパティにならず、再び代入してもE_DEPRECATEDが出ることはありません。

## Discussion

### Alternative opt-in to dynamic properties

このRFCでは、引き続き動的プロパティを使い続ける方法として`extends stdClass`を提案しています。

他にはインターフェイス`SupportsDynamicProperties`やアトリビュート`#[SupportsDynamicProperties]`、トレイト`use DynamicProperties`などを提供するという手段も考えられます。
`extends stdClass`を選択した理由は、特別なサポートを追加することなく動作するからです。

stdClassに動的プロパティのサポートを提供する必要があることは明らかです。
またリスコフの置換原則に伴い、子クラスはこの動作を継承する必要があります。
従って、いずれにおいても`extends stdClass`は確実に動作します。
課題は、それに加えて他の手段を提供するかどうかです。
`stdClass`であれば、PHPの古いバージョンにおいてもpolyfillが必要ないことが利点です。

逆に、`stdClass`に`__get()/__set()`を実装することで動的プロパティをサポートすることもできます。
現在はそうなっていませんが、将来全ての動的プロパティのサポートが廃止されたときには、そのようにすることが可能です。

かわりにインターフェイスやアトリビュートを使用した場合は、PHPエンジンは将来においても動的プロパティをサポートし続けなければならなくなります。

` __get()/__set()`を実装したトレイトを提供することは可能です。
しかし、そのようなトレイトはユーザランドで実装したものと変わらず、PHPエンジンの最適化の恩恵も受けることができません。
自前で実装すれば、さほど多くない量のコードでより正確に動的プロパティを制御することができます。


### Opt-out of dynamic properties instead

[Locked Classes](https://wiki.php.net/rfc/locked-classes)のRFCでは、この問題領域に対して別のアプローチを取っています。
特定のクラスに対して動的プロパティを提供するのではなく、特定のクラスに対して動的プロパティを禁止しています。

現代のコードは動的プロパティを禁止することがデフォルトであり、動的プロパティを必要とするクラスが例外であるため、これが正しい戦略だとは思えません。
また、Locked Classesのアプローチでは、アプリやライブラリの所有者が個別に対応していく必要があります。

### Internal impact

内部的には既に`ZEND_ACC_NO_DYNAMIC_PROPERTIES`フラグが存在しており、動的プロパティを禁止することは簡単です。
移行中は`ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES`フラグをstdClassに追加し、これが無いクラスにはE_DEPRECATEDを発生させます。

動的プロパティが完全に禁止された際には大きな変更が必要です。
仮想マシンやデフォルトのオブジェクトハンドラから動的プロパティのサポートを削除します。
かわりに`stdClass`にカスタムのオブジェクトハンドラを実装します

オブジェクトは`properties`メンバを保持しなくなるため、全てのオブジェクトサイズが8バイト小さくなります。
`get_properties()`ハンドラは廃止され、全てのプロパティを検索するコードは`properties_table`をループすることになります。
たとえばオブジェクトをforeachした場合、動的プロパティテーブルを検索する必要がなくなり、かわりにプロパティスロットを見るだけでよくなります。



# [メーリングリスト](https://externals.io/message/115800)

「動的プロパティを使ってるコードがたくさんあって、このコードを修正する最終手段となるから100%支持する。」
「stdClassを使うことに懸念がある。名前のわりに特に基底クラスとかではないのに、後から見た開発者が基底クラスと勘違いしたりするかもしれない。あと既に継承していると追加が難しい。」
「というか`class Foo extends stdClass`と書かれても何の意味があるのか普通の人はわからない。`Foo extends DynamicObject`とかのほうがわかりやすい。」
「extendsじゃなくて`use stdClassTrait`みたいなのにしてほしい。」
「stdClassと同じ働きをするトレイトをRFCに明記しておけばレガシーコードの対応に役立つだろう。」
「Entity APIやViewで動的プロパティを多用しているDrupalが死ぬ。悪い習慣であることは確かだろうけど、50万以上のDrupalサイトで実際にそれが使われている。」
「WeakMapはPHP8.0で実装されたばっかりなので、これを代替策にされても過去バージョンの人は困る。」
「RFCでは`意図的に行われることはほとんどない`ってあるけど、これはつまり`意図せず行われていることは多々ある`ってことだよね。」
「最終目標は`動的プロパティは明示的にアノテーションを必要とする。アノテーションはコアでサポートする。`と`動的プロパティを完全に削除する。コアには宣言されたプロパティと__get/__setのみがある`どちらでしょう。前者は妥当だけど後者はPHPにとってはやりすぎだと思います。」


# 感想

いくらなんでも副作用が大きすぎるので、さすがにマイナーバージョンで採用するレベルの話ではない気がします。
PHP5時代のコードは確実に全滅しますし、PHP7・8のライブラリやフレームワークでも未定義プロパティを使っているものなんていくらでもありますからね。
だいたい[一回で書き捨てるコード](https://qiita.com/rana_kualu/items/38df213f5e30326a81a8)とかに一々プロパティなんか書いてられるかって話ですよ。
やるにしてもPHP9でE_DEPRECATE、PHP10でExceptionくらいのスケールで考えた方がいいのではないかと思います。

また、stdClassはPHPにおいてはこれまで特に意味のなかったクラスなので、それに後から意味を乗せるのはどうなんだろうか。

さすがにNikitaの提案とはいえ、MLではこのRFCには抵抗のある人も多いようです。
全面賛成の人も多少はいますが、全体的には、基本的には賛成なんだけど、と歯切れの悪いかんじです。
影響の大きさを考えると致し方ないところでしょう。

果たして今後、このRFCはいったいどのようになるでしょうか。