Tuesday, May 31, 2011

PHP empty() function only checks variable

I want to blog this funny issue for a long time but always forgot:PHP's empty() function only works on variables, not on expression, not on the value returned by magic method __get

So let's see how funny empty() works.

class EmptyTest
{    
    public function getCategory() {
        return 'human';
    }
}

$test = new EmptyTest();
//expect to get false, but,,
var_dump(empty($test->getCategory()));

We will actually get "Fatal error: Can't use method return value in write context...". So to fix this issue, we have to do:

$category = $obj->getCategory();
//that will work as expected
var_dump(empty($category));

If that is the whole story, fine. At least we get fatal error and know what is wrong. Unfortunately, that is not the worst part of empty() yet. Let's check this:

class EmptyTest
{
    private $properties = array();
    
    public function __get($key) {
        return isset($this->properties[$key]) ? $this->properties[$key] : null;
    }
    
    public function __set($key, $value) {
        $this->properties[$key] = $value;
    }
}
$test = new EmptyTest();
$test->name = 'henry';
//expect to get false, but,,,
//we will get true!
var_dump(empty($test->name));

As you will see, we get true!!! For me, nothing could be worse than that. PHP just works as usual, doesn't complain anything, but simply gives us an unexpected result. It is like a logic bomb is placed in your application. Still, the easy solution is: 
$name = $test->name;
var_dump(empty($test->name));

Let's dig deepr about empty() and magic __get. Why it doesn't work as expected? Change our code:
class EmptyTest
{
    private $properties = array();
    
    public function __get($key) {
        echo '__get is invoked<br>';
        return isset($this->properties[$key]) ? $this->properties[$key] : null;
    }
    
    public function __set($key, $value) {
        $this->properties[$key] = $value;
    }
}
$test = new EmptyTest();
$test->name = 'henry';
var_dump(empty($test->name));

It turns out that the __get method is not even invoked! Change our code again:
class EmptyTest
{
    private $properties = array();
    
    public function __get($key) {
        echo '__get is invoked<br>';
        return isset($this->properties[$key]) ? $this->properties[$key] : null;
    }

    public function __isset($name) {
        echo '__isset is invoked<br>';
        return true;
    }
    
    public function __set($key, $value) {
        $this->properties[$key] = $value;
    }
}
$test = new EmptyTest();
$test->name = 'henry';
var_dump(empty($test->name)); 

Woho! This time, we get exactly what we expect! And we find that empty() invokes __isset method first. It tries to check if the method it is calling has been set or not! if it is set, it continues to invoke __get method.

Why empty() function works in such a funny manner? I have no idea. Even if I went to check its C source code, I'm afraid I still cannot understand why it is designed and implemented in this way.

No comments: