返回

PHP对象属性判空: 避开empty()与__get()的陷阱

php

对象数组属性判断为空的陷阱:empty() 函数在特定场景下的失效分析与解决方案

开发过程经常会遇到需要判断对象属性是否为空的场景,empty() 函数看起来是一个方便的选择。但是 empty() 在处理对象属性,尤其是通过魔术方法 __get() 访问的私有属性时,会遇到失效的问题,导致逻辑判断出现偏差。

现象 empty() 对于 __get() 访问的属性失效

empty() 函数被设计用来检测一个变量是否为空。当直接用于判断对象通过魔术方法 __get() 访问的数组属性时,即使该属性实际上已被赋值并填充了数据,empty() 仍然可能错误地返回 true。下面是问题案例:

// The Class 
class Person{
    private $number;

    public function __construct($num){
        $this->number = $num;
    }

    // this the returns value, even though its a private member
    public function __get($property){
        return intval($this->$property);
    }
}

// The Code    
$person = new Person(5);

if (empty($person->number)){
    echo "its empty";
} else {
    echo "its not empty";
}

这段代码期望的输出是 its not empty。 可得到的是 its empty, 即使 $person 对象的 number 属性被初始化为 5。 实际的运行结果却是 "its empty",产生了预期之外的行为。

原因剖析:empty() 函数与魔术方法 __get() 的交互

出现这种问题原因在于empty() 不是一个普通函数, 是一个语言结构。 这就造成了它与 __get() 魔术方法之间的行为不一致性。
empty() 需要直接操作变量内容进行判断, 不能是右值, 要能操作到变量内容。如果将empty操作看做一个函数。参数是对象成员变量时, 函数调用的时候, 会把函数运算完作为右值传进来。 而这部分操作没有被 __get() 函数捕获。 __get() 访问返回的是计算出来的值。 所以 empty() 认为它是空的。

PHP 文档关于 __get() 部分有详细解释, 文档中提到了关于对象上下文如何工作的相关详细信息:

当在对象上下文中由于语法上的原因必须是某个变量名的情况调用它们时, __isset__unset 会获取他们的预期行为, 在这样的情况下就是 empty()isset() 这两个。

简单地说, empty() 判断的时候 $this->prop 是不可访问的,触发了__get魔术方法,但empty无法处理__get的结果。于是被误判了. 换句话说 __get 只对实际存在的对象变量有效。

解决方案: 避免 empty() 导致的错误判断

为了正确地判断对象数组属性是否为空,我们需要采用一些替代方案。这些方法的核心是确保我们能准确地访问和评估对象的属性值。

方法一: 使用中间变量

将属性值存储到一个临时变量中,然后再进行判断。

操作步骤:

  1. 通过对象的公开方法或属性(如果可用)或直接访问(如果可见性允许),将数组属性的值赋给一个临时变量。
  2. 使用 empty() 函数检查这个临时变量是否为空。

代码示例:

class Person{
    private $number;

    public function __construct($num){
        $this->number = $num;
    }

    public function __get($property){
        return $this->$property;
    }
}

$person = new Person(5);
$temp = $person->number; // 将属性值存储到临时变量中
if (empty($temp)) {
    echo "its empty";
} else {
    echo "its not empty";
}

这样修改后就能正确判断了。这种方式可以解决大部分情况下的问题,也是性能损耗较少的方法。

方法二: 使用 isset() 和数组计数

这个方法包括:

  • 使用 isset() 判断属性是否存在。
  • 如果存在,进一步检查数组的元素数量(针对数组属性)

操作步骤:

  1. 首先检查属性是否已设置并具有元素。
  2. 确认属性的元素数量来区分空数组。

代码示例:

class MyClass {
    public $myArray = ['a', 'b', 'c'];
    private $privateArray = [1,2,3];

     public function __get($name) {
        if (property_exists($this, $name)) {
            return $this->$name;
        }
        return null; // Or handle error as needed
    }
    
    public function __isset($name) {
        return isset($this->$name);
    }

    // Optionally for handling private arrays accessed via __get
    public function getPrivateArray() {
      return $this->privateArray;
    }
}

$obj = new MyClass();
$array = $obj->getPrivateArray();
if (isset($array) && count($array) > 0) {
    echo "Array property is set and not empty.";
} else {
    echo "Array property is either not set or empty.";
}

使用 isset() 保证即使触发了 __get() 魔术方法也能判断变量的存在与否。 此方案更清晰地表达了判断的意图, 避免了直接使用 empty() 可能引起的混淆。 只是这种方案需要对象内部显式的公开方法用于读取数据。

方法三: 实现 __isset() 魔术方法

通过在类中定义 __isset() 魔术方法。 它将与 empty()isset() 一起按预期被调用。

操作步骤:

  1. 在类中添加 __isset() 魔术方法的实现。
  2. __isset() 方法中编写逻辑来正确处理对私有属性是否存在的检查。

代码示例:

class Person {
    private $data = [];

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

    public function __get($property) {
        if (array_key_exists($property, $this->data)) {
            return $this->data[$property];
        }
        return null;
    }

    public function __isset($property) {
        return isset($this->data[$property]);
    }
}

$person = new Person(['number' => 5]);

if (empty($person->number)) {
    echo "its empty";
} else {
    echo "its not empty";
}

如果正确实现了__isset, 当使用empty()检查$person->number时,实际会触发__isset方法。 可以把私有属性判断为空的逻辑委托给它处理。这是较为安全的解决方案,避免外部调用可能引起的逻辑错误。
此方法适用需要对外隐藏内部细节, 采用 __get() 魔术方法访问属性的类。 对已有的代码修改量少,不需要暴露新的方法即可解决 empty() 行为异常的问题。