📚

PHPのempty()が__getだけでは正しく動作しない理由

に公開

PHPで__getマジックメソッドを実装したクラスに対してempty()を使うと、予想していない結果になることがあります。

コード例

<?php

class Accessor
{
    public function __construct(private $target) {}
    
    public function __get($key)
    {
        return $this->target->$key;
    }
}

$school = new stdClass();
$school->name = "Tokyo";

$accessor = new Accessor($school);

var_dump($accessor->name); // "Tokyo"
var_dump(empty($accessor->name)); // true 注意

上記の場合Tokyoという文字列が出力されるが、empty()trueになります。

なぜこうなるのか

なぜかというと、下記です。

__isset() は、 isset() あるいは empty() をアクセス不能(protected または private)または存在しないプロパティに対して実行したときに起動します。
https://www.php.net/__get#language.oop5.overloading.members

つまり、empty()はアクセス不能なプロパティに対して内部的に__isset()を呼び出します。__isset()が定義されていない場合、プロパティは「存在しない」と判断され、empty()は常にtrue(空)を返します

解決策: __isset()も実装する

上記のコードを修正するならこのようにできます。

<?php

class Accessor
{
    public function __construct(private $target) {}
    
    public function __get($key)
    {
        return $this->target->$key;
    }
    
    public function __isset($key)
    {
        return isset($this->target->$key);
    }
}

$school = new stdClass();
$school->name = "Tokyo";

$accessor = new Accessor($school);

var_dump($accessor->name); // "Tokyo"
var_dump(empty($accessor->name)); // falseに変わる

考えてみて

いろいろ考えてみて、__get()を使いたいユースケースにもよるが、
個人的にはPHP 8.1以降であれば、public readonlyでいいのかなと思ったがどうなのだろう。

参考:
https://www.php.net/__get#language.oop5.overloading.members

Discussion