Extending PHP 5.3 Closures with Serialization and Reflection[CP]

from:[url]http://www.htmlist.com/development/extending-php-5-3-closures-with-serialization-and-reflection/[/url]

By Jeremy Lindblom
On January 28th, 2010

PHP 5.3 has brought with it some powerful and much-needed features like late static bindings, namespaces, and closures (also referred to as anonymous functions and lambda functions). Anyone who is experienced with JavaScript or who has worked with programming languages like Scheme or Lisp should realize the value that anonymous functions can bring to PHP. The PHP Manual explains closures like this:

Anonymous functions, also known as closures, allow the creation of functions which have no specified name. They are most useful as the value of callback parameters, but they have many other uses. Closures can also be used as the values of variables; PHP automatically converts such expressions into instances of the Closure internal class.

PHP has very few predefined classes that are part of the core language, so naturally I was intrigued by the Closure class. The PHP Manual has this to say about the class:

The predefined final class Closure was introduced in PHP 5.3.0. It is used for internal implementation of anonymous functions. The class has a constructor forbidding the manual creation of the object (issues E_RECOVERABLE_ERROR) and the __invoke() method with the calling magic.

The invoke magic method is also a new feature in PHP 5.3. It is called when an object is used in the context of a function (e.g. $object($parameter);). Since Closure objects will be used like functions, this is a critical feature of the Closure object. The Closure class may be perfectly equipped to act like an anonymous function, but it does not provide any extra utility beyond that. A var_dump() of a closure will reveal the functions parameters, but there is no way to get any other information about the Closure (like the actual code of the function). Trying to serialize the Closure throws an Exception and json_encode() just returns an empty JSON string. To make matters worse, the Closure class is final, so there is no way to extend it.

That simply wasn’t going to cut it for me. I wanted to make my own Closure class that was at least able to do the following:

Invoke the Closure (with __invoke()) just like the PHP Closure class
Retrieve the actual code of the Closure
Retrieve detailed data about the Closure’s parameters
Retrieve the names and values of any variables inherited from the Closure’s parent’s scope (with the use construct)
Serialize and unserialize the Closure

I decided to play around with the Reflection API to see what kind of information I could get from the Closure. A Closure is a function, so using the ReflectionFunction class provides the same information about Closures as it does about any other functions. PHP 5.3 also added the isClosure() method to the ReflectionFunction (in case you weren’t convinced that reflection would be helpful). After some tinkering, it became apparent that I would be able to accomplish all of my desires. I will walk you through the construction of my “SuperClosure” class and explain how the creative use of Reflection allows us to find ways around the problems normally blocking the ability to have my desired features.
1. Grant the ability to invoke the Closure

The first goal was to create the basic SuperClosure class that both encapsulates and allows invocation of the Closure. To do this I wrote a simple class with a constructor and the magic __invoke() method. I also included an accessor method (getter) for the actual Closure. In the constructor I created an instance of the ReflectionFunction class and stored it as a class member that helped allow invocation of the closure with a variable number of arguments. It also helped me accomplish other things later. The following code shows the basic class upon which I will be building. The complete will be shown at the end of the article.
view sourceprint?

01.class SuperClosure {
02.
03.protected $closure = NULL;
04.protected $reflection = NULL;
05.
06.public function __construct($function)
07.{
08.if ( ! $function instanceOf Closure)
09.throw new InvalidArgumentException();
10.
11.$this->closure = $function;
12.$this->reflection = new ReflectionFunction($function);
13.}
14.
15.public function __invoke()
16.{
17.$args = func_get_args();
18.return $this->reflection->invokeArgs($args);
19.}
20.
21.public function getClosure()
22.{
23.return $this->closure;
24.}
25.}

The __invoke() method allows the SuperClosure object to be used exactly as if it was a Closure object. I had to recreate this functionality for the SuperClosure class since I was not able to extend the Closure to begin with. In order to pass arguments through to the real Closure, I used a combination of the func_get_args() function and the invokeArgs() method of ReflectionFunction to ensure that any variable number of arguments could be used. I could have also used call_user_func_array() function, but I prefer Reflection, and since I already had an instance of the ReflectionFunction, the invokeArgs() method seemed like a better choice.
2. Retrieve the actual code of the function

Secondly, I added code that allowed the SuperClosure class to find and store the actual code defining the closure. This feature is actually the key to doing the serialization later and is the most complicated part of the program. To accomplish this portion of the program, I used the instance of ReflectionFunction from Step 1 and the SplFileObject class, an SPL class which provides a nice object-oriented interface for dealing with files. The getFileName(), getStartLine(), and getEndLine() methods of the ReflectionFunction class allowed me to retrieve all the information I needed to find the source code of the Closure function. Using the SplFileObject to open and read from the source file and in combination with some string manipulation, I was able to obtain the closure’s source code. The following code shows the protected _fetchCode() method used to parse the closure’s code out of its source file:
view sourceprint?

01.protected function _fetchCode()
02.{
03.// Open file and seek to the first line of the closure
04.$file = new SplFileObject($this->reflection->getFileName());
05.$file->seek($this->reflection->getStartLine()-1);
06.
07.// Retrieve all of the lines that contain code for the closure
08.$code = '';
09.while ($file->key() < $this->reflection->getEndLine())
10.{
11.$code .= $file->current();
12.$file->next();
13.}
14.
15.// Only keep the code defining that closure
16.$begin = strpos($code, 'function');
17.$end = strrpos($code, '}');
18.$code = substr($code, $begin, $end - $begin + 1);
19.
20.return $code;
21.}

The only limitations with the current version of this function are that you cannot have multiple closures on a single line and you cannot use the word “function” anywhere besides the actual closure’s declaration.
3. Retrieve detailed data about the Closure’s parameters

Retrieving information about the closure’s parameters was as simple as adding a method that simply returns the result of the getParameters() method the SuperClosure’s instance of ReflectionFunction. That’s all. The getParameters() method returns ReflectionParameter objects which have several methods for getting information about the parameters including their names, values, default values, and more.
4. Retrieve the names and values of any variables inherited from the Closure’s parent’s scope

One of the biggest problems I had was retrieving the names and values of the variables added to the Closure’s scope with the use construct. There isn’t a documented way of doing this as far as I know, but I was able to figure out a way to do it after playing around some more with the ReflectionFunction class (it is just so helpful). These variables are actually included in the results of the getStaticVariables() method. If the closure declares any variables with the static keyword, these will also be included in the results. To get around this I added the _fetchUsedVariables() method that uses a combination of the getStaticVariables() method and some string manipulation of the Closure’s code (from Step 2) to find only the variables that were inherited from the parent’s scope. The following code shows the _fetchUsedVariables() method:
view sourceprint?

01.protected function _fetchUsedVariables()
02.{
03.// Make sure the use construct is actually used
04.$use_index = stripos($this->code, 'use');
05.if ( ! $use_index)
06.return array();
07.
08.// Get the names of the variables inside the use statement
09.$begin = strpos($this->code, '(', $use_index) + 1;
10.$end = strpos($this->code, ')', $begin);
11.$vars = explode(',', substr($this->code, $begin, $end - $begin));
12.
13.// Get the static variables of the function via reflection
14.$static_vars = $this->reflection->getStaticVariables();
15.
16.// Only keep the variables that appeared in both sets
17.$used_vars = array();
18.foreach ($vars as $var)
19.{
20.$var = trim($var, ' $&');
21.$used_vars[$var] = $static_vars[$var];
22.}
23.
24.return $used_vars;
25.}

5. Grant the ability to serialize and unserialize the Closure

Finally, I implemented the serialization capabilities. Since an actual closure object cannot be serialized (it gives a fatal error), I had to be creative to come up with a way to do my own serialization. The code I wrote for Step 2 and Step 4 made this possible. The sleep magic method allows you to hook into the serialization process, so I used this method to prepare my SuperClosure class for serialization. In order for my class to be serialized, I needed to first stop the Closure object and the ReflectionFunction object representing the Closure from being serialized. The __sleep() method should return an array of the class members you are serializing, so I simply left those variables out of the list. Because I was serializing the Closure’s code and static variables, it made possible in the __wakeup() magic method to recreate the Closure object using the code. To do this I had to use the extract() function on my array of used variables to import them back into the scope. Then I performed an eval() operation on the Closure’s code in order to recreate the Closure. eval() is always a risky operation, but in this case it makes sense. If you are planning to use this code for any of your own projects, I urge you to take precautions. The following code shows the complete SuperClosure class with the additions of the __sleep() and __wakeup() methods.
view sourceprint?

001.class SuperClosure {
002.
003.protected $closure = NULL;
004.protected $reflection = NULL;
005.protected $code = NULL;
006.protected $used_variables = array();
007.
008.public function __construct($function)
009.{
010.if ( ! $function instanceOf Closure)
011.throw new InvalidArgumentException();
012.
013.$this->closure = $function;
014.$this->reflection = new ReflectionFunction($function);
015.$this->code = $this->_fetchCode();
016.$this->used_variables = $this->_fetchUsedVariables();
017.}
018.
019.public function __invoke()
020.{
021.$args = func_get_args();
022.return $this->reflection->invokeArgs($args);
023.}
024.
025.public function getClosure()
026.{
027.return $this->closure;
028.}
029.
030.protected function _fetchCode()
031.{
032.// Open file and seek to the first line of the closure
033.$file = new SplFileObject($this->reflection->getFileName());
034.$file->seek($this->reflection->getStartLine()-1);
035.
036.// Retrieve all of the lines that contain code for the closure
037.$code = '';
038.while ($file->key() < $this->reflection->getEndLine())
039.{
040.$code .= $file->current();
041.$file->next();
042.}
043.
044.// Only keep the code defining that closure
045.$begin = strpos($code, 'function');
046.$end = strrpos($code, '}');
047.$code = substr($code, $begin, $end - $begin + 1);
048.
049.return $code;
050.}
051.
052.public function getCode()
053.{
054.return $this->code;
055.}
056.
057.public function getParameters()
058.{
059.return $this->reflection->getParameters();
060.}
061.
062.protected function _fetchUsedVariables()
063.{
064.// Make sure the use construct is actually used
065.$use_index = stripos($this->code, 'use');
066.if ( ! $use_index)
067.return array();
068.
069.// Get the names of the variables inside the use statement
070.$begin = strpos($this->code, '(', $use_index) + 1;
071.$end = strpos($this->code, ')', $begin);
072.$vars = explode(',', substr($this->code, $begin, $end - $begin));
073.
074.// Get the static variables of the function via reflection
075.$static_vars = $this->reflection->getStaticVariables();
076.
077.// Only keep the variables that appeared in both sets
078.$used_vars = array();
079.foreach ($vars as $var)
080.{
081.$var = trim($var, ' $&');
082.$used_vars[$var] = $static_vars[$var];
083.}
084.
085.return $used_vars;
086.}
087.
088.public function getUsedVariables()
089.{
090.return $this->used_variables;
091.}
092.
093.public function __sleep()
094.{
095.return array('code', 'used_variables');
096.}
097.
098.public function __wakeup()
099.{
100.extract($this->used_variables);
101.
102.eval('$_function = '.$this->code.';');
103.if (isset($_function) AND $_function instanceOf Closure)
104.{
105.$this->closure = $_function;
106.$this->reflection = new ReflectionFunction($_function);
107.}
108.else
109.throw new Exception();
110.}
111.}

Conclusion

With some cleverness and reflection, I was able to create the SuperClosure class and extend the Closure’s normal capabilities. Although my class does not enhance the typical use of a PHP Closure it does make it possible to serialize and transport a closure and provides an interface for examining the parameters and inherited variables. This means that we can do:
view sourceprint?
1.$closure = new SuperClosure(
2.function($num1, $num2) {return $num1 + $num2;}
3.);
4.$serialized_closure = serialize($closure);
5.$unserialized_closure = unserialize($serialized_closure);
6.echo $unserialized_closure(1, 5);

without worrying about errors. Also, it would technically be possible to send closures through a remote service.

From what I have read in the PHP Manual, this class may have to be changed in the future. The Manual states this regarding anonymous functions:

Anonymous functions are currently implemented using the Closure class. This is an implementation detail and should not be relied upon.

So according to this, future versions of PHP might implement closures differently, and my class would have to be rewritten.

If you would like a copy of the code from this article (and an example of its use) you can find it on my Github account.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值