PHP8.1から、クラスオブジェクトのプロパティを読み取り専用にできるようになりました。
プロパティ変数名の左に readonly キーワードを付けるだけ。
簡単そうに見えますが(じっさいに簡単。)、いろいろと制約があります。サンプルを交えながら見ていきましょう。
読み取り専用プロパティの定義
読み取り専用のプロパティはかんたんです。
<?php
class Test
{
public readonly string $prop;
public function __construct( string $prop ) {
$this->prop = $prop;
}
}
$test = new Test('obj1');
var_dump($test->prop);
// 読み取り専用プロパティ値の変更。エラーになる。
$test->prop = 'obj2';
PHP Fatal error: Uncaught Error: Cannot modify readonly property Test::$prop in /home/vagrant/php-test.php:16
Stack trace:
#0 {main}
thrown in /home/vagrant/php-test.php on line 16
読み取り専用プロパティで値を変更しようとするとError例外がスローされます。
使い方の説明はこれだけ。簡単すぎて屁が出そう。
(『屁が出そう』に意味はない。ちょっとふざけたかっただけ。そこは突っ込まないで。)
ただし、readonlyにはいろいろな制約があります。ひとつずつ見ていきましょう。
クラス内で変更するとどうなる?
上記のサンプルでは、クラスオブジェクトの外から値を変更しようとしました。今度はクラス内部で変更してみましょう。
class Test
{
private readonly string $prop;
public function __construct( string $prop ) {
$this->prop = $prop;
$this->prop = $prop; // <--- 2度目の変更
}
}
PHP Fatal error: Uncaught Error: Cannot modify readonly property Test::$prop in /home/vagrant/php-test.php:26
Stack trace:
#0 /home/vagrant/php-test/php8-test.php(30): Test->__construct()
#1 {main}
thrown in /home/vagrant/php-test.php on line 26
クラスオブジェクト内部で変更してもエラーになります。当たり前ですね?
ちなみに、読み取り専用プロパティはpublic以外でも使えます。
プロパティの型宣言をしないと使えない。
readonly キーワードは、型宣言をしたプロパティじゃないと使えません。
class Test
{
public readonly $prop;
public function __construct( $prop ) {
$this->prop = $prop;
}
}
PHP Fatal error: Readonly property Test::$prop must have type in /home/vagrant/php-test.php on line 35
エラー例外はスローされません。つまり文法エラー。readonly キーワードの右隣りには必ず型が必要です。
ちなみに、型宣言したくない場合は、mixed型が使えます。
class Test
{
public readonly mixed $prop;
public function __construct( $prop ) {
$this->prop = $prop;
}
}
$test = new Test('obj1');
var_dump($test->prop);
string(4) "obj1"
変数の型の厳密さはPHP8のひとつの方向性です。ゆるい型で作るのはそろそろやめましょう。
staticプロパティでは使えない。
readonly は、staticキーワードとは併用できません。
class Test
{
public static readonly mixed $prop;
public function __construct( $prop ) {
$this->prop = $prop;
}
}
PHP Fatal error: Static property Test::$prop cannot be readonly in /home/vagrant/php-test.php on line 49
これも文法エラー。
なんでダメなんだろう。オブジェクトのメモリの静的領域でできない理由が分からない。
同じクラスのインスタンスで共通のreadonlyプロパティは必要ないじゃん、ってことらしい。たしかに定数と変わらない気がする。
仕様なんだからしょうがない。インスタンス上の固有プロパティしか使えません。
外からは初期値を入れられない。
readonlyプロパティは、クラスオブジェクト内だけでしか値を入れられません。
class Test
{
public readonly mixed $prop;
}
$test = new Test();
$test->prop = 'initial value';
PHP Fatal error: Uncaught Error: Cannot initialize readonly property Test::$prop from global scope in /home/vagrant/php-test.php:64
Stack trace:
#0 {main}
thrown in /home/vagrant/php-test.php on line 64
これはエラー例外がスローされるので文法エラーではありません。
クラス作成時はエラーにならず、じっさいにクラスを使ったときに初めてエラーになります。
readonlyは、初期値を入れる処理は必ずクラス内で入れろよ、ということ。
デフォルト値は入れられない。
クラス内で初期値を入れることは分かりましたが、デフォルト値は入れられません。
class Test
{
public readonly mixed $prop = 'initial';
}
PHP Fatal error: Readonly property Test::$prop cannot have default value in /home/vagrant/php-test.php on line 70
文法エラーになります。
これは納得。デフォルト値を入れるならそれは定数でいいじゃんってなるから。
unset() は初期値を入れる前なら使えるが。
unset() は、readonlyプロパティ値を入れる前なら実行することができます。
class Test
{
public readonly mixed $prop;
public function __construct( $prop ) {
unset($this->prop);
$this->prop = $prop;
}
}
$test = new Test('obj1');
var_dump($test->prop);
class Test
{
public readonly mixed $prop;
public function __construct( $prop ) {
$this->prop = $prop;
unset($this->prop);
}
}
$test = new Test('obj1');
var_dump($test->prop);
PHP Fatal error: Uncaught Error: Cannot unset readonly property Test::$prop in /home/vagrant/php-test/php8-test.php:96
Stack trace:
#0 /home/vagrant/php-test.php(100): Test->__construct()
#1 {main}
thrown in /home/vagrant/php-test.php on line 96
unset() は事実上、使えません。初期値を入れる前に実行する意味がないから。
PHP公式ドキュメント
読み取り専用プロパティのオブジェクトの内部だけは変更できる。
読み取り専用プロパティは初期値を入れると、その後はまったく変更できないというわけではありません。
プロパティ値がクラスオブジェクトのときだけは、そのオブジェクトの内部を変更できます。
class Test
{
public readonly mixed $prop;
public $name = 'name';
public function __construct( $prop ) {
$this->prop = $prop;
}
}
$test = new Test(new Test('child'));
$test->prop->name = 'change !!!'; // <--- オブジェクト内部は変更できる。
var_dump($test->prop);
object(Test)#2 (2) {
["prop"]=>
string(5) "child"
["name"]=>
string(10) "change !!!"
}
それなら、配列の一部は変更できるんじゃないかと思いがちですが、それはできません。
class Test
{
public readonly mixed $prop;
public function __construct( $prop ) {
$this->prop = $prop;
}
}
$arr = [
'name' => 'name',
'value' => '1',
];
$test = new Test($arr);
$test->prop['value'] = 'change !!!';
var_dump($test->prop);
PHP Fatal error: Uncaught Error: Cannot modify readonly property Test::$prop in /home/vagrant/php-test.php:134
Stack trace:
#0 {main}
thrown in /home/vagrant/php-test.php on line 134
もちろんですが、いくらクラスオブジェクトでも、プロパティ値そのものは変更できません。
class Test
{
public readonly mixed $prop;
public $name = 'name';
public function __construct( $prop ) {
$this->prop = $prop;
}
}
$test = new Test(new Test('child'));
$test->prop->name = 'change !!!'; // <--- オブジェクト内部は変更できる。
$test->prop = new Test('child change'); // <--- オブジェクトそのものは変更できない。
var_dump($test->prop);
PHP Fatal error: Uncaught Error: Cannot modify readonly property Test::$prop in /home/vagrant/php-test.php:149
Stack trace:
#0 {main}
thrown in /home/vagrant/php-test.php on line 149
PHP8.0以下の下位互換はない。
PHP8.1で追加された新機能なので、当然ですがPHP8.0以下では使えません。
class Test
{
public readonly int $prop;
public function __construct( $prop ) {
$this->prop = $prop;
}
}
PHP Parse error: syntax error, unexpected 'int' (T_STRING), expecting variable (T_VARIABLE) in /home/vagrant/php-test.php on line 154
PHP Parse error: syntax error, unexpected identifier "int", expecting variable in /home/vagrant/php-test.php on line 154
もちろんですが、どちらも文法エラーです。まったく動きません。