1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace Patron;
13
14 use ICanBoogie\Debug;
15 use ICanBoogie\Exception;
16
17 class TextHole
18 {
19 public function __construct()
20 {
21 $this->context = $this->contextInit();
22 }
23
24 protected $functions = array();
25
26 public function addFunction($name, $callback)
27 {
28
29
30
31
32 $this->functions[$name] = $callback;
33 }
34
35 public function findFunction($name)
36 {
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
53
54
55
56
57
58
59
60 if (isset($this->functions[$name]))
61 {
62 return $this->functions[$name];
63 }
64
65 $try = 'ICanBoogie\\' . $name;
66
67 if (function_exists($try))
68 {
69 return $try;
70 }
71
72 $try = 'ICanBoogie\I18n\\' . $name;
73
74 if (function_exists($try))
75 {
76 return $try;
77 }
78
79
80
81
82
83 $try = 'wd_' . str_replace('-', '_', $name);
84
85 if (function_exists($try))
86 {
87 return $try;
88 }
89
90 $try = 'Patron\\' . $name;
91
92 if (function_exists($try))
93 {
94 return $try;
95 }
96 }
97
98 99 100 101 102 103 104
105
106 public $context = array();
107
108 protected function contextInit()
109 {
110 return array
111 (
112 '$server' => &$_SERVER,
113 '$request' => &$_REQUEST
114 );
115 }
116
117 118 119 120 121 122 123
124
125
126
127
128
129 static private function dotsToBrackets($string)
130 {
131
132
133
134
135
136 $parts = explode('.', $string);
137
138 $rc = NULL;
139
140 foreach ($parts as $part)
141 {
142 $rc .= is_numeric($part) ? "[$part]" : "['$part']";
143 }
144
145 return $rc;
146 }
147
148 149 150 151 152 153 154
155
156 public function publish($template, $bind=null, array $options=array())
157 {
158 if ($bind !== null)
159 {
160 $this->context['this'] = $bind;
161 }
162
163 foreach ($options as $option => $value)
164 {
165 switch ((string) $option)
166 {
167 168 169 170 171 172 173
174 case 'variables':
175 {
176 $this->context = array_merge($this->context, $value);
177 }
178 break;
179
180 default:
181 {
182 Debug::trigger('Suspicious option: %option :value', array('%option' => $option, ':value' => $value));
183 }
184 break;
185 }
186 }
187
188 return $this->resolve($template);
189 }
190
191 protected function resolve($text)
192 {
193 $text = preg_replace_callback
194 (
195 '#\#\{(?!\s)([^\}]+)\}#', array($this, 'resolve_callback'), $text
196 );
197
198 return $text;
199 }
200
201 protected function resolve_callback($matches)
202 {
203 $expression = $matches[1];
204
205 $rc = $this->evaluate($expression);
206
207 if (is_object($rc))
208 {
209 if (!method_exists($rc, '__toString'))
210 {
211 $this->error('%expression was resolved to an object of the class %class', array('%expression' => $expression, '%class' => get_class($rc)));
212 }
213
214 $rc = (string) $rc;
215 }
216 else if (is_array($rc))
217 {
218 $this->error('%expression was resolved to an array with the following keys: :keys', array('%expression' => $expression, ':keys' => implode(', ', array_keys($rc))));
219 }
220
221 return $rc;
222 }
223
224 225 226
227
228 const TOKEN_TYPE = 'type';
229 const TOKEN_TYPE_FUNCTION = 'function';
230 const TOKEN_TYPE_IDENTIFIER = 'identifier';
231 const TOKEN_VALUE = 'value';
232 const TOKEN_ARGS = 'args';
233 const TOKEN_ARGS_EVALUATE = 'args-evaluate';
234
235 static protected function parseExpression_callback($str)
236 {
237 $str .= '.';
238
239 $length = strlen($str);
240
241 $quote = null;
242 $quote_closed = null;
243 $part = null;
244 $escape = false;
245
246 $function = null;
247 $args = array();
248 $args_evaluate = array();
249 $args_count = 0;
250
251 $parts = array();
252
253 for ($i = 0 ; $i < $length ; $i++)
254 {
255 $c = $str{$i};
256
257 if ($escape)
258 {
259 $part .= $c;
260
261 $escape = false;
262
263 continue;
264 }
265
266 if ($c == '\\')
267 {
268
269
270 $escape = true;
271
272 continue;
273 }
274
275 if ($c == '"' || $c == '\'' || $c == '`')
276 {
277 if ($quote && $quote == $c)
278 {
279
280
281 $quote = null;
282 $quote_closed = $c;
283
284 if ($function)
285 {
286 continue;
287 }
288 }
289 else if (!$quote)
290 {
291
292
293 $quote = $c;
294
295 if ($function)
296 {
297 continue;
298 }
299 }
300 }
301
302 if ($quote)
303 {
304 $part .= $c;
305
306 continue;
307 }
308
309
310
311
312
313 if ($c == '.')
314 {
315
316
317
318
319
320 if (strlen($part))
321 {
322 $parts[] = array
323 (
324 self::TOKEN_TYPE => self::TOKEN_TYPE_IDENTIFIER,
325 self::TOKEN_VALUE => $part
326 );
327 }
328
329 $part = null;
330
331 continue;
332 }
333
334 if ($c == '(')
335 {
336
337
338 $function = $part;
339
340 $args = array();
341 $args_count = 0;
342
343 $part = null;
344
345 continue;
346 }
347
348 if (($c == ',' || $c == ')') && $function)
349 {
350
351
352 if ($part !== null)
353 {
354 if ($quote_closed == '`')
355 {
356
357
358 $args_evaluate[] = $args_count;
359 }
360
361 if (!$quote_closed)
362 {
363
364
365
366
367
368 $part_back = $part;
369
370 switch ($part)
371 {
372 case 'true':
373 case 'TRUE':
374 {
375 $part = true;
376 }
377 break;
378
379 case 'false':
380 case 'FALSE':
381 {
382 $part = false;
383 }
384 break;
385
386 case 'null':
387 case 'NULL':
388 {
389 $part = null;
390 }
391 break;
392
393 default:
394 {
395 if (is_numeric($part))
396 {
397 $part = (int) $part;
398 }
399 else if (is_float($part))
400 {
401 $part = (float) $part;
402 }
403 else
404 {
405 $part = constant($part);
406 }
407 }
408 break;
409 }
410
411
412 }
413
414 $args[] = $part;
415 $args_count++;
416
417 $part = null;
418 }
419
420 $quote_closed = null;
421
422 if ($c != ')')
423 {
424 continue;
425 }
426 }
427
428 if ($c == ')' && $function)
429 {
430
431
432 $parts[] = array
433 (
434 self::TOKEN_TYPE => self::TOKEN_TYPE_FUNCTION,
435 self::TOKEN_VALUE => $function,
436 self::TOKEN_ARGS => $args,
437 self::TOKEN_ARGS_EVALUATE => $args_evaluate
438 );
439
440 continue;
441 }
442
443 if ($c == ' ' && $function)
444 {
445 continue;
446 }
447
448 $part .= $c;
449 }
450
451 return $parts;
452 }
453
454 protected static $function_chain_cache;
455 protected static $function_chain_cache_usage;
456
457 protected function parseExpression($expression)
458 {
459 $parsed = null;
460
461 if (isset(self::$function_chain_cache[$expression]))
462 {
463 $parsed = self::$function_chain_cache[$expression];
464 }
465
466 if (!$parsed)
467 {
468 $parsed = self::parseExpression_callback($expression);
469
470 self::$function_chain_cache[$expression] = $parsed;
471 }
472
473 if (empty(self::$function_chain_cache_usage[$expression]))
474 {
475 self::$function_chain_cache_usage[$expression] = 0;
476 }
477
478 self::$function_chain_cache_usage[$expression]++;
479
480 return $parsed;
481 }
482
483 public function evaluate($expression, $silent=false)
484 {
485 if (!$expression)
486 {
487 $this->error('Empty expression');
488
489 return;
490 }
491
492 $value = $this->context;
493 $previous_identifier = 'context';
494 $work_expression = $expression;
495
496
497
498
499
500 if ($expression{0} == '@')
501 {
502
503
504
505
506 if (!isset($this->context['this']))
507 {
508 $this->error
509 (
510 'Using <q>this</q> property when no <q>this</q> is defined: %identifier', array
511 (
512 '%identifier' => $expression
513 )
514 );
515
516 return;
517 }
518
519 $value = $this->context['this'];
520 $previous_identifier = 'this';
521 $work_expression = substr($expression, 1);
522 }
523 else if ($expression{0} == '$')
524 {
525 $value = $GLOBALS;
526 $previous_identifier = 'globals';
527 $work_expression = substr($expression, 1);
528
529 if (substr($work_expression, 0, 7) == 'request')
530 {
531 $value = $_REQUEST;
532 $previous_identifier = 'request';
533 $work_expression = substr($expression, 8);
534 }
535 else if (substr($work_expression, 0, 6) == 'server')
536 {
537 $value = $_SERVER;
538 $previous_identifier = 'server';
539 $work_expression = substr($expression, 7);
540 }
541 else if (substr($work_expression, 0, 6) == 'shared')
542 {
543 $value = $this->context_shared;
544 $previous_identifier = 'shared';
545 $work_expression = substr($expression, 7);
546 }
547 }
548
549
550
551
552
553 $parts = self::parseExpression($work_expression);
554
555 foreach ($parts as $part)
556 {
557 $identifier = $part[self::TOKEN_VALUE];
558
559 switch ($part[self::TOKEN_TYPE])
560 {
561 case self::TOKEN_TYPE_IDENTIFIER:
562 {
563 if (!is_array($value) && !is_object($value))
564 {
565 $this->error
566 (
567 'Unexpected variable type: %type (%value) for %identifier in expression %expression, should be either an array or an object', array
568 (
569 '%type' => gettype($value),
570 '%value' => $value,
571 '%identifier' => $identifier,
572 '%expression' => $expression
573 )
574 );
575
576 return;
577 }
578
579 $exists = false;
580
581 if (is_array($value))
582 {
583 $exists = array_key_exists($identifier, $value);
584 }
585 else
586 {
587 $exists = property_exists($value, $identifier);
588
589 if (!$exists && method_exists($value, 'has_property'))
590 {
591 $exists = $value->has_property($identifier);
592 }
593 else
594 {
595 if (!$exists && method_exists($value, 'offsetExists'))
596 {
597 $exists = $value->offsetExists($identifier);
598 }
599
600 if (!$exists && method_exists($value, '__get'))
601 {
602 $exists = true;
603 }
604 }
605 }
606
607 if (!$exists)
608 {
609 if (!$silent)
610 {
611 $this->error
612 (
613 '%identifier of expression %expression does not exists in %var (defined: :keys) in: !value', array
614 (
615 '%identifier' => $identifier,
616 '%expression' => $expression,
617 '%var' => $previous_identifier,
618 ':keys' => implode(', ', array_keys((array) $value)),
619 '!value' => $value
620 )
621 );
622 }
623
624 return;
625 }
626
627 $value = (is_array($value) || method_exists($value, 'offsetExists')) ? $value[$identifier] : $value->$identifier;
628 $previous_identifier = $identifier;
629 }
630 break;
631
632 case self::TOKEN_TYPE_FUNCTION:
633 {
634 $method = $identifier;
635 $args = $part[self::TOKEN_ARGS];
636 $args_evaluate = $part[self::TOKEN_ARGS_EVALUATE];
637
638 if ($args_evaluate)
639 {
640 $this->error('we should evaluate %eval', array('%eval' => $args_evaluate));
641 }
642
643
644
645
646
647 if (is_object($value) && method_exists($value, $method))
648 {
649 $value = call_user_func_array(array($value, $method), $args);
650
651 break;
652 }
653
654
655
656
657
658
659 $callback = $this->findFunction($method);
660
661
662
663
664
665
666 if (!$callback)
667 {
668 if (is_string($value))
669 {
670 if (function_exists('str' . $method))
671 {
672 $callback = 'str' . $method;
673 }
674 else if (function_exists('str_' . $method))
675 {
676 $callback = 'str_' . $method;
677 }
678 }
679 else if (is_array($value) || is_object($value))
680 {
681 if (function_exists('ICanBoogie\array_' . $method))
682 {
683 $callback = 'ICanBoogie\array_' . $method;
684 }
685 else if (function_exists('array_' . $method))
686 {
687 $callback = 'array_' . $method;
688 }
689 }
690 }
691
692
693
694
695
696 if (!$callback)
697 {
698 if (function_exists($method))
699 {
700 $callback = $method;
701 }
702 }
703
704 if (!$callback)
705 {
706 if (is_object($value) && method_exists($value, '__call'))
707 {
708 $value = call_user_func_array(array($value, $method), $args);
709
710 break;
711 }
712 }
713
714
715
716
717
718 if (!$callback)
719 {
720 $this->error
721 (
722 'Unknown method: %method for: %expression', array
723 (
724 'method' => $method,
725 'expression' => $expression
726 )
727 );
728
729 return;
730 }
731
732
733
734
735
736 array_unshift($args, $value);
737
738 if (PHP_MAJOR_VERSION > 5 || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 2))
739 {
740 if ($callback == 'array_shift')
741 {
742 $value = array_shift($value);
743 }
744 else
745 {
746 $value = call_user_func_array($callback, $args);
747 }
748 }
749 else
750 {
751 $value = call_user_func_array($callback, $args);
752 }
753 }
754 break;
755 }
756 }
757
758 return $value;
759 }
760
761 public function error($message, array $args=array())
762 {
763 Debug::trigger($message, $args);
764 }
765 }