1 <?php
2
3 /*
4 * This file is part of the Patron package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Patron;
13
14 use ICanBoogie\Exception;
15 use BlueTihi\Context;
16
17 class EvaluateNode extends ExpressionNode
18 {
19 protected $original_expression;
20 protected $engine_context;
21
22 public function __construct($expression, $escape)
23 {
24 $this->original_expression = $expression;
25
26 parent::__construct($this->tokenize($expression), $escape);
27 }
28
29 private $engine;
30
31 public function __invoke(Engine $engine, $context)
32 {
33 $this->engine = $engine;
34 $this->engine_context = $context;
35
36 return parent::__invoke($engine, $context);
37 }
38
39 protected function render($parts)
40 {
41 $value = $this->engine_context;
42 $expression = $this->original_expression;
43 $silent = false;
44 $previous_identifier = '__context__';
45
46 foreach ($parts as $i => $part)
47 {
48 $identifier = $part[self::TOKEN_VALUE];
49
50 switch ($part[self::TOKEN_TYPE])
51 {
52 case self::TOKEN_TYPE_IDENTIFIER:
53 {
54 if (!is_array($value) && !is_object($value))
55 {
56 throw new \InvalidArgumentException(\ICanBoogie\format
57 (
58 'Unexpected variable type: %type (%value) for %identifier in expression %expression, should be either an array or an object', array
59 (
60 '%type' => gettype($value),
61 '%value' => $value,
62 '%identifier' => $identifier,
63 '%expression' => $expression
64 )
65 ));
66 }
67
68 $exists = false;
69
70 if (is_array($value))
71 {
72 $exists = array_key_exists($identifier, $value);
73 }
74 else
75 {
76 $exists = property_exists($value, $identifier);
77
78 if (!$exists && method_exists($value, 'has_property'))
79 {
80 $exists = $value->has_property($identifier);
81 }
82 else
83 {
84 if (!$exists && method_exists($value, 'offsetExists'))
85 {
86 $exists = $value->offsetExists($identifier);
87 }
88
89 if (!$exists && method_exists($value, '__get'))
90 {
91 $exists = true;
92 }
93 }
94 }
95
96 if (!$exists)
97 {
98 if (!$silent)
99 {
100 throw new Exception
101 (
102 '%identifier of expression %expression does not exists in %var (defined: :keys) in: :value', array
103 (
104 'identifier' => $identifier,
105 'expression' => $expression,
106 'var' => $previous_identifier,
107 'keys' => implode(', ', $value instanceof Context ? $value->keys() : array_keys((array) $value)),
108 'value' => $value
109 )
110 );
111 }
112
113 return;
114 }
115
116 $value = (is_array($value) || method_exists($value, 'offsetExists')) ? $value[$identifier] : $value->$identifier;
117 $previous_identifier = $identifier;
118 }
119 break;
120
121 case self::TOKEN_TYPE_FUNCTION:
122 {
123 $method = $identifier;
124 $args = $part[self::TOKEN_ARGS];
125 $args_evaluate = $part[self::TOKEN_ARGS_EVALUATE];
126
127 if ($args_evaluate)
128 {
129 $this->error('we should evaluate %eval', array('%eval' => $args_evaluate));
130 }
131
132 #
133 # if value is an object, we check if the object has the method
134 #
135
136 if (is_object($value) && method_exists($value, $method))
137 {
138 $value = call_user_func_array(array($value, $method), $args);
139
140 break;
141 }
142
143 #
144 # well, the object didn't have the method,
145 # we check internal functions
146 #
147
148 $callback = $this->findFunction($this->engine, $method);
149
150 #
151 # if no internal function matches, we try string and array functions
152 # depending on the type of the value
153 #
154
155 if (!$callback)
156 {
157 if (is_string($value))
158 {
159 if (function_exists('str' . $method))
160 {
161 $callback = 'str' . $method;
162 }
163 else if (function_exists('str_' . $method))
164 {
165 $callback = 'str_' . $method;
166 }
167 }
168 else if (is_array($value) || is_object($value))
169 {
170 if (function_exists('ICanBoogie\array_' . $method))
171 {
172 $callback = 'ICanBoogie\array_' . $method;
173 }
174 else if (function_exists('array_' . $method))
175 {
176 $callback = 'array_' . $method;
177 }
178 }
179 }
180
181 #
182 # our last hope is to try the function "as is"
183 #
184
185 if (!$callback)
186 {
187 if (function_exists($method))
188 {
189 $callback = $method;
190 }
191 }
192
193 if (!$callback)
194 {
195 if (is_object($value) && method_exists($value, '__call'))
196 {
197 $value = call_user_func_array(array($value, $method), $args);
198
199 break;
200 }
201 }
202
203 #
204 #
205 #
206
207 if (!$callback)
208 {
209 throw new Exception
210 (
211 'Unknown method %method for expression %expression.', array
212 (
213 '%method' => $method,
214 '%expression' => $expression
215 )
216 );
217 }
218
219 #
220 # create evaluation
221 #
222
223 array_unshift($args, $value);
224
225 if (PHP_MAJOR_VERSION > 5 || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 2))
226 {
227 if ($callback == 'array_shift')
228 {
229 $value = array_shift($value);
230 }
231 else
232 {
233 $value = call_user_func_array($callback, $args);
234 }
235 }
236 else
237 {
238 $value = call_user_func_array($callback, $args);
239 }
240 }
241 break;
242 }
243 }
244
245 return $value;
246 }
247
248 const TOKEN_TYPE = 1;
249 const TOKEN_TYPE_FUNCTION = 2;
250 const TOKEN_TYPE_IDENTIFIER = 3;
251 const TOKEN_VALUE = 4;
252 const TOKEN_ARGS = 5;
253 const TOKEN_ARGS_EVALUATE = 6;
254
255 /*
256 * Tokenize Javascript style function chain into an array of identifiers and functions
257 */
258 protected function tokenize($str)
259 {
260 if ($str{0} == '@')
261 {
262 $str = 'this.' . substr($str, 1);
263 }
264
265 $str .= '.';
266
267 $length = strlen($str);
268
269 $quote = null;
270 $quote_closed = null;
271 $part = null;
272 $escape = false;
273
274 $function = null;
275 $args = array();
276 $args_evaluate = array();
277 $args_count = 0;
278
279 $parts = array();
280
281 for ($i = 0 ; $i < $length ; $i++)
282 {
283 $c = $str{$i};
284
285 if ($escape)
286 {
287 $part .= $c;
288
289 $escape = false;
290
291 continue;
292 }
293
294 if ($c == '\\')
295 {
296 //echo "found escape: [$c] @$i<br />";
297
298 $escape = true;
299
300 continue;
301 }
302
303 if ($c == '"' || $c == '\'' || $c == '`')
304 {
305 if ($quote && $quote == $c)
306 {
307 //echo "found closing quote: [$c]<br />";
308
309 $quote = null;
310 $quote_closed = $c;
311
312 if ($function)
313 {
314 continue;
315 }
316 }
317 else if (!$quote)
318 {
319 //echo "found opening quote: [$c]<br />";
320
321 $quote = $c;
322
323 if ($function)
324 {
325 continue;
326 }
327 }
328 }
329
330 if ($quote)
331 {
332 $part .= $c;
333
334 continue;
335 }
336
337 #
338 # we are not in a quote
339 #
340
341 if ($c == '.')
342 {
343 //echo "end of part: [$part]<br />";
344
345 // FIXME: added strlen() because of '0' e.g. items.0.price
346 // could a part be null ??
347
348 if (strlen($part))
349 {
350 $parts[] = array
351 (
352 self::TOKEN_TYPE => self::TOKEN_TYPE_IDENTIFIER,
353 self::TOKEN_VALUE => $part
354 );
355 }
356
357 $part = null;
358
359 continue;
360 }
361
362 if ($c == '(')
363 {
364 //echo "function [$part] begin: @$i<br />";
365
366 $function = $part;
367
368 $args = array();
369 $args_count = 0;
370
371 $part = null;
372
373 continue;
374 }
375
376 if (($c == ',' || $c == ')') && $function)
377 {
378 //echo "function push argument [$part] q=[$quote_closed]<br />";
379
380 if ($part !== null)
381 {
382 if ($quote_closed == '`')
383 {
384 //echo "we should evaluate [$part][$args_count]<br />";
385
386 $args_evaluate[] = $args_count;
387 }
388
389 if (!$quote_closed)
390 {
391 #
392 # end of an unquoted part.
393 # it might be an integer, a float, or maybe a constant !
394 #
395
396 $part_back = $part;
397
398 switch ($part)
399 {
400 case 'true':
401 case 'TRUE':
402 {
403 $part = true;
404 }
405 break;
406
407 case 'false':
408 case 'FALSE':
409 {
410 $part = false;
411 }
412 break;
413
414 case 'null':
415 case 'NULL':
416 {
417 $part = null;
418 }
419 break;
420
421 default:
422 {
423 if (is_numeric($part))
424 {
425 $part = (int) $part;
426 }
427 else if (is_float($part))
428 {
429 $part = (float) $part;
430 }
431 else
432 {
433 $part = constant($part);
434 }
435 }
436 break;
437 }
438
439 //\ICanBoogie\log('part: [\1] == [\2]', $part_back, $part);
440 }
441
442 $args[] = $part;
443 $args_count++;
444
445 $part = null;
446 }
447
448 $quote_closed = null;
449
450 if ($c != ')')
451 {
452 continue;
453 }
454 }
455
456 if ($c == ')' && $function)
457 {
458 //echo "function end: [$part] @$i<br />";
459
460 $parts[] = array
461 (
462 self::TOKEN_TYPE => self::TOKEN_TYPE_FUNCTION,
463 self::TOKEN_VALUE => $function,
464 self::TOKEN_ARGS => $args,
465 self::TOKEN_ARGS_EVALUATE => $args_evaluate
466 );
467
468 continue;
469 }
470
471 if ($c == ' ' && $function)
472 {
473 continue;
474 }
475
476 $part .= $c;
477 }
478
479 return $parts;
480 }
481
482 public function findFunction(Engine $engine, $name)
483 {
484 return $engine->findFunction($name);
485 /*
486 $function = $engine->findFunction($name);
487
488 if ($function)
489 {
490 return $function;
491 }
492
493 $try = 'ICanBoogie\\' . $name;
494
495 if (function_exists($try))
496 {
497 return $try;
498 }
499
500 $try = 'ICanBoogie\I18n\\' . $name;
501
502 if (function_exists($try))
503 {
504 return $try;
505 }
506
507 #
508 # 'wd' pseudo namespace // COMPAT
509 #
510
511 $try = 'wd_' . str_replace('-', '_', $name);
512
513 if (function_exists($try))
514 {
515 return $try;
516 }
517
518 $try = 'Patron\\' . $name;
519
520 if (function_exists($try))
521 {
522 return $try;
523 }
524 */
525 }
526 }