ゆるふわPHP絶対殺すマンであるNikita Popovが、いよいよ本気を出してきました。
$dt = new DateTime();
$dt->hoge = 1;
var_dump($dt->hoge); // 1
PHPでは未定義のプロパティに値を突っ込むと、特に何の抵抗もなくプロパティが生えます。
しかし、他の多くの言語ではこのような動作にならず、未定義プロパティを突っ込もうとするとエラーになります。
ということでこれを禁止しようというRFCが提出されました。
以下はDeprecate dynamic propertiesのRFCの紹介です。
PHP RFC: Deprecate dynamic properties
Introduction
宣言されていないプロパティに書き込みを行うと、PHPでは何もエラーが出ずに動的にプロパティが作成されます。
現代のコードにおいて、これが意図的に行われることはほとんどありません。
このRFCでは、動的プロパティを非推奨とし、将来的に削除することを提案します。
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
となります。
使用するプロパティは全てクラス内で宣言しなければなりません。
class Foo {}
$foo = new Foo;
// Deprecated: Creation of dynamic property Foo::$bar is deprecated
$foo->bar = 1;
// ↑で既にプロパティbarができたので、こっちはE_DEPRECATEDは出ない
$foo->bar = 2;
ただし、stdClass
を継承したクラスは引き続き動的プロパティをサポートします。
$obj = (object) []; // = new stdClass;
// エラー出ない
$obj->foo = 1;
class myStdClass extends stdClass {}
$obj2 = new myStdClass;
// エラー出ない
$obj2->bar = 1;
stdClass
オブジェクトは、動的プロパティを保持することを目的としてよく使用されます。
従ってextends stdClass
は、動的プロパティの使用を目的とした移行手段として今後も提供されます。
また、__get()/__set()
が定義されている場合の未定義プロパティへのアクセスは、動的プロパティとはみなされません。
以下の例では非推奨の警告は発生しません。
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コードは一貫してプロパティ宣言が使われていますが、レガシーコードは必ずしもそうではありません。
動的プロパティの警告に遭遇した場合、回避するためにできることはいくつかあります。
もっとも単純で一般的な解決策は、プロパティ宣言を追加することです。
class Test {
public $value; // ←これを追加するだけ
public function __construct($value) {
$this->value = $value;
}
}
意図的にプロパティ宣言を持たないクラスの場合は、マジックメソッド__get()/__set()
を実装するか、stdClass
を継承するか、ARRAY_AS_PROPS
モードのArrayObjectを継承するか、で対応できます。
__get()/__set()
の継承が最もコントロールしやすくなります。
stdClassの継承は、PHPエンジンの最適化された動的プロパティ処理の恩恵を受けることができ、またforeach
やproperty_exists()
等の動作が現在の動作に最も近いものになります。
自身が知らないオブジェクトに情報を紐付けたいことがあります。
これまでは、この目的のために動的プロパティが使われていました。
かわりにWeakMapを使って邪魔にならない方法で情報を保存することができます。
class Test {
private WeakMap $extraInfo;
public function addExtraInfo(object $obj) {
// こっちのかわりに
$obj->extraInfo = ...;
// こっちを使う
$this->extraInfo[$obj] = ...;
}
}
稀に、動的プロパティが遅延初期化に使われることがあります。
たとえばSymfonyにおいてConstraint::$groupsプロパティは宣言されておらず、__get()
が呼ばれたときに動的に生成されます。
この場合は、プロパティ宣言したあとにコンストラクタで削除することで対応できます。
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の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した場合、動的プロパティテーブルを検索する必要がなくなり、かわりにプロパティスロットを見るだけでよくなります。
メーリングリスト
「動的プロパティを使ってるコードがたくさんあって、このコードを修正する最終手段となるから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のライブラリやフレームワークでも未定義プロパティを使っているものなんていくらでもありますからね。
だいたい一回で書き捨てるコードとかに一々プロパティなんか書いてられるかって話ですよ。
やるにしてもPHP9でE_DEPRECATE、PHP10でExceptionくらいのスケールで考えた方がいいのではないかと思います。
また、stdClassはPHPにおいてはこれまで特に意味のなかったクラスなので、それに後から意味を乗せるのはどうなんだろうか。
さすがにNikitaの提案とはいえ、MLではこのRFCには抵抗のある人も多いようです。
全面賛成の人も多少はいますが、全体的には、基本的には賛成なんだけど、と歯切れの悪いかんじです。
影響の大きさを考えると致し方ないところでしょう。
果たして今後、このRFCはいったいどのようになるでしょうか。