1: <?php
2:
3: /**
4: * Picon Framework
5: * http://code.google.com/p/picon-framework/
6: *
7: * Copyright (C) 2011-2012 Martin Cassidy <martin.cassidy@webquub.com>
8:
9: * Picon Framework is free software: you can redistribute it and/or modify
10: * it under the terms of the GNU General Public License as published by
11: * the Free Software Foundation, either version 3 of the License, or
12: * (at your option) any later version.
13:
14: * Picon Framework is distributed in the hope that it will be useful,
15: * but WITHOUT ANY WARRANTY; without even the implied warranty of
16: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17: * General Public License for more details.
18:
19: * You should have received a copy of the GNU General Public License
20: * along with Picon Framework. If not, see <http://www.gnu.org/licenses/>.
21: * */
22: namespace picon;
23:
24: /**
25: * A wrapper for closures enabling them to be serialized.
26: * IMPORTANT NOTE: This is highly experimental and needs further improvment and testing
27: *
28: * A closure is serialized by deconstructing it into its parts, namely the string
29: * that represents its code and the paramaters it uses. The code is extracted
30: * with reflection and SplFileObject, the paramaters are extracted with reflection.
31: *
32: * The code string and paramters are all that is serailized. On unserialize the code
33: * and paramters are used to reconstruct the closure using eval().
34: *
35: * The usage of eval mean that SplFileObject can no longer be used on a reconstructed closure.
36: * However, this is solved by storing the previously extracted code for future use.
37: *
38: * Any closure which is defined within a reconstructed closure can also not have SplFileObject
39: * used on it, as it was also defined by eval()'d code. To prevent this being a problem, all extracted
40: * code is processed so that an closures declared within have their own code isolated so that it may be
41: * used later when it is needed and would otherwise be un-obtainable with SplFileObject.
42: *
43: * @todo imporove so that all type hinting usage of classes within a closure don't need to be fully qualified
44: * @todo test in PHP 5.4.x
45: * @author Martin Cassidy
46: * @package utilities
47: */
48: class SerializableClosure
49: {
50: private $closure;
51: private $code;
52: private $arguments;
53: private $source;
54: private $reflection;
55:
56: public function __construct($closure, $code = null)
57: {
58: $this->validateClosure($closure);
59: $this->reflection = new \ReflectionFunction($closure);
60:
61: if($code==null)
62: {
63: $this->code = $this->fetchCode($this->reflection);
64: $this->prepareCode();
65: }
66: else
67: {
68: $this->code = base64_decode($code);
69: }
70: $this->arguments = $this->fetchUsedVariables($this->reflection, $this->code);
71:
72: if(method_exists($this->reflection, "getClosureThis"))
73: {
74: $this->source = $this->reflection->getClosureThis();
75: }
76: }
77:
78: /**
79: * Extract the code from the callback as a string
80: * @param ReflectionFunction The reflected function of the closure
81: * @return String The code the closure runs
82: */
83: private function fetchCode(\ReflectionFunction $reflection)
84: {
85: $code = null;
86: $file = new \SplFileObject($reflection->getFileName());
87: $file->seek($reflection->getStartLine() - 1);
88:
89: $code = '';
90: while ($file->key() < $reflection->getEndLine())
91: {
92: $code .= $file->current();
93: $file->next();
94: }
95:
96: //@todo this assumes the function will be the only one on that line
97: $begin = strpos($code, 'function');
98: //@todo this assumes the } will be the only one on that line
99: $end = strrpos($code, '}');
100: $code = substr($code, $begin, $end - $begin + 1);
101: return $code;
102: }
103:
104: /**
105: * Performs string analysis to determin if anything needs to be altered
106: * to allow the reconstructed closure to work correctly
107: * @todo replace type hints with fq names
108: */
109: private function prepareCode()
110: {
111: $nested = array();
112: $depth = 0;
113: $closureSelf = false;
114: $delcaration = false;
115: $preparedClosure = "";
116: $codeBlocks = token_get_all("<?php $this->code ?>");
117:
118: foreach($codeBlocks as $c)
119: {
120: $value = '';
121: if(is_array($c))
122: {
123: if($c[0]==T_FUNCTION)
124: {
125: if(!$closureSelf)
126: {
127: $closureSelf = true;
128: }
129: else
130: {
131: $delcaration = true;
132: $index = array_push($nested, array('depth' => $depth, 'content' => ''));
133: $preparedClosure .= 'new picon\SerializableClosure(';
134: }
135: }
136: $value = $c[1];
137: }
138: else
139: {
140: $value = $c;
141:
142: if($c=="{")
143: {
144: $depth++;
145: $delcaration = false;
146: }
147: else if($c=="}")
148: {
149: $depth--;
150: }
151: }
152: $preparedClosure .= $value;
153:
154: foreach($nested as $index => $function)
155: {
156: $nested[$index]['content'] .= $value;
157: if($function['depth']==$depth && !$delcaration)
158: {
159: $preparedClosure .= ', "'.base64_encode($nested[$index]['content']).'")';
160: unset($nested[$index]);
161: }
162: }
163: }
164: $preparedClosure = substr($preparedClosure, 5, strlen($preparedClosure)-7);
165:
166: //function{1}\s*\({1}(\w*\s*&?\${1}\w+)*\){1}
167: $this->code = $preparedClosure;
168: }
169:
170: /**
171: * Extract bound variables
172: * @param ReflectionFunction The reflected function of the closure
173: * @param String The string of code the closure runs
174: * @return Array The variable within the use()
175: */
176: private function fetchUsedVariables(\ReflectionFunction $reflection, $code)
177: {
178: $use_index = stripos($code, 'use');
179: if (!$use_index)
180: {
181: return array();
182: }
183:
184: $begin = strpos($code, '(', $use_index) + 1;
185: $end = strpos($code, ')', $begin);
186: $vars = explode(',', substr($code, $begin, $end - $begin));
187:
188: $static_vars = $reflection->getStaticVariables();
189:
190: $used_vars = array();
191: foreach ($vars as $var)
192: {
193: $var = preg_replace("/\\$|\\&/", "", trim($var));
194: if(is_callable($static_vars[$var]) && !($static_vars[$var] instanceof SerializableClosure))
195: {
196: $used_vars[$var] = new SerializableClosure($static_vars[$var]);
197: }
198: else
199: {
200: $used_vars[$var] = $static_vars[$var];
201: }
202: }
203:
204: return $used_vars;
205: }
206:
207:
208: /**
209: * Validates the closure
210: * @param Closure The closure to validate
211: */
212: private function validateClosure($closure)
213: {
214: if (!isset($closure) || !($closure instanceof \Closure) || !is_callable($closure))
215: {
216: throw new \InvalidArgumentException("Closure was not valid");
217: }
218: }
219:
220: public function __sleep()
221: {
222: return(array('code', 'arguments'));
223: }
224:
225: public function __wakeup()
226: {
227: extract($this->arguments);
228: eval('$closure = '.$this->code.";");
229: $this->closure = $closure;
230:
231: if(method_exists('Closure', 'bind'))
232: {
233: $this->closure = Closure::bind($this->closure, $this->source, get_class($this->source));
234: }
235: $this->reflection = new \ReflectionFunction($this->closure);
236: }
237:
238: public function __invoke()
239: {
240: $args = func_get_args();
241: return $this->reflection->invokeArgs($args);
242: }
243:
244: public function getReflection()
245: {
246: return $this->reflection;
247: }
248: }
249: ?>
250: