Thought Labs Blog

PHP's mystical __set_state method

Posted by Cappy Popp on Feb 2, 2008 8:57:00 AM

php

PHP 4.x introduced magic methods with the __serialize and __unserialize methods. Since then many have been added. Love them or hate them as a language abomination magic methods are here to stay. Most are well-documented but one, the __set_state() magic method, is not. I hope to change that a bit.

First, a simple description of magic methods is in order. Basically these are special callback functions that are called by PHP in specific circumstances; they usually allow you to do custom processing before or after a certain PHP library function call occurs. For example, when serializing objects, PHP will attempt to call the magic method/member function __sleep() prior to serialization. This is to allow the object to do any last minute clean-up, etc. prior to being serialized. Likewise, when the object is restored using unserialize() the __wakeup() magic method/member function is called. When trying to call a non-existent method on an object instance, PHP will attempt to call the magic method __call on that instance.

Before I go further, I want to address the problem of magic method performance in PHP. Yes, it has been well-documented that using these methods can be 10 – 20 times slower than using a normal method. However, under normal usage patterns no one should be able to detect such performance bottlenecks. Sure, if you use __call 10 million times, you are certainly going to see a hit. Chances are if your PHP code is under tight performance constraints, well, you most likely should not be using PHP to begin with.

So, let's get back to our buddy __set_state. What does it do? Like the __toString() magic method it is used as a formatting callback when your object instance is asked to be converted to a string (note that before PHP 5.2 only a call to echo() or print() resulted in a call to __toString.) The difference is in what event forces the callback. __set_state is called in response to an instance of your object being passed to the var_export function, while __toString is called otherwise. What's the difference? var_export is very different from the other debugging output functions (print_r/var_dump) in that it always must output PARSEABLE PHP CODE. The issue here is that var_export takes an optional second parameter – a boolean that determines whether after the variable passed as the first argument is returned as a string instead of output.

A quick example is in order. What do you think is output from the following code?

class Dummy {
  private $value1_;
  private $value2_;

  function __construct() {
    $this->value1_ = 100;
    $this->value2_ = "100";
  }
}

$aDummy = new Dummy();
var_export($aDummy);

Here's the output (on screen):

  Dummy::__set_state(array( 'value1_' => 100, 'value2_' => '100', ))

Now we're getting somewhere. var_export must print out valid PHP code, remember? And this is valid PHP code. It's referring to a static __set_state method in our Dummy class. Well, then you should be able to execute it. The PHP eval function evaluates the string argument passed to it as valid PHP code so we can use it to test this code. Let's replace that var_export call with this line:

  eval(var_export($aDummy, true) . ';'));

What's the output? This is:

Fatal error: Call to undefined method Dummy::__set_state() in D:\src\phptests\__set_state.php(21) : eval()'d code on line 1

As you can see, var_export cannot produce executable PHP code for the Dummy type without a __set_state method defined. We need to provide one. var_export takes one argument, and it must be an array. It will contain key-value pairs of the properties or fields of the instance on which it is called.

class Dummy {
  private $value1_;
  private $value2_;

  function __construct() {
    $this->value1_ = 100;
    $this->value2_ = "100";
  }

  static function __set_state(array $array) {
    foreach($array as $k => $v) {
      echo("$k ==> $v <br/>");
    }
  }
}

Now the output is this:

  value1_ ==> 100
  value2_ ==> 100

Better.

But what do you have to put in your __set_state method? That's completely up to you. It just should produce valid PHP code. It's also important to remember that if you're using some 3rd party logging, tracing, or debugging package that it might call var_export on your instance so it's best to be sure. Personally, I think the best thing to do is to basically treat __set_state as another creation method (a copy constructor for those of you with C++ backgrounds). That way if someone requests the evaluation mode in var_export they get expected behavior. Let's change __set_state in our example to reflect this idea:

static function __set_state(array $array) {
  $tmp = new Dummy();
  $tmp->value1_ = $array['value1_'];
  $tmp->value2_ = $array['value2_'];
  return $tmp;
}

Hopefully that has alleviated some of the confusion around this method. Enjoy!

Topics: Programming

Subscribe to Blog Articles by Email

Follow Us