Completed
Push — master ( b0df54...0c9057 )
by Ben
10s
created

Context.php ➔ phptal_path()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * PHPTAL templating engine
4
 *
5
 * PHP Version 5
6
 *
7
 * @category HTML
8
 * @package  PHPTAL
9
 * @author   Laurent Bedubourg <[email protected]>
10
 * @author   Kornel Lesiński <[email protected]>
11
 * @license  http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
12
 * @version  SVN: $Id$
13
 * @link     http://phptal.org/
14
 */
15
16
/**
17
 * This class handles template execution context.
18
 * Holds template variables and carries state/scope across macro executions.
19
 *
20
 */
21
class PHPTAL_Context
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
22
{
23
    public $repeat;
24
    public $_xmlDeclaration;
25
    public $_docType;
26
    private $_nothrow;
27
    private $_slots = array();
28
    private $_slotsStack = array();
29
    private $_parentContext = null;
30
    private $_globalContext = null;
31
    private $_echoDeclarations = false;
32
33
    public function __construct()
34
    {
35
        $this->repeat = new stdClass();
36
    }
37
38
    public function __clone()
39
    {
40
        $this->repeat = clone $this->repeat;
41
    }
42
43
    /**
44
     * will switch to this context when popContext() is called
45
     *
46
     * @return void
47
     */
48
    public function setParent(PHPTAL_Context $parent)
49
    {
50
        $this->_parentContext = $parent;
51
    }
52
53
    /**
54
     * set stdClass object which has property of every global variable
55
     * It can use __isset() and __get() [none of them or both]
56
     *
57
     * @return void
58
     */
59
    public function setGlobal(stdClass $globalContext)
60
    {
61
        $this->_globalContext = $globalContext;
62
    }
63
64
    /**
65
     * save current execution context
66
     *
67
     * @return Context (new)
68
     */
69
    public function pushContext()
70
    {
71
        $res = clone $this;
72
        $res->setParent($this);
73
        return $res;
74
    }
75
76
    /**
77
     * get previously saved execution context
78
     *
79
     * @return Context (old)
80
     */
81
    public function popContext()
82
    {
83
        return $this->_parentContext;
84
    }
85
86
    /**
87
     * @param bool $tf true if DOCTYPE and XML declaration should be echoed immediately, false if buffered
88
     */
89
    public function echoDeclarations($tf)
90
    {
91
        $this->_echoDeclarations = $tf;
92
    }
93
94
    /**
95
     * Set output document type if not already set.
96
     *
97
     * This method ensure PHPTAL uses the first DOCTYPE encountered (main
98
     * template or any macro template source containing a DOCTYPE.
99
     *
100
     * @param bool $called_from_macro will do nothing if _echoDeclarations is also set
101
     *
102
     * @return void
103
     */
104 View Code Duplication
    public function setDocType($doctype,$called_from_macro)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
105
    {
106
        // FIXME: this is temporary workaround for problem of DOCTYPE disappearing in cloned PHPTAL object (because clone keeps _parentContext)
107
        if (!$this->_docType) {
108
            $this->_docType = $doctype;
109
        }
110
111
        if ($this->_parentContext) {
112
            $this->_parentContext->setDocType($doctype, $called_from_macro);
113
        } else if ($this->_echoDeclarations) {
114
            if (!$called_from_macro) {
115
                echo $doctype;
116
            } else {
117
                throw new PHPTAL_ConfigurationException("Executed macro in file with DOCTYPE when using echoExecute(). This is not supported yet. Remove DOCTYPE or use PHPTAL->execute().");
118
            }
119
        }
120
        else if (!$this->_docType) {
121
            $this->_docType = $doctype;
122
        }
123
    }
124
125
    /**
126
     * Set output document xml declaration.
127
     *
128
     * This method ensure PHPTAL uses the first xml declaration encountered
129
     * (main template or any macro template source containing an xml
130
     * declaration)
131
     *
132
     * @param bool $called_from_macro will do nothing if _echoDeclarations is also set
133
     *
134
     * @return void
135
     */
136 View Code Duplication
    public function setXmlDeclaration($xmldec, $called_from_macro)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
137
    {
138
        // FIXME
139
        if (!$this->_xmlDeclaration) {
140
            $this->_xmlDeclaration = $xmldec;
141
        }
142
143
        if ($this->_parentContext) {
144
            $this->_parentContext->setXmlDeclaration($xmldec, $called_from_macro);
145
        } else if ($this->_echoDeclarations) {
146
            if (!$called_from_macro) {
147
                echo $xmldec."\n";
148
            } else {
149
                throw new PHPTAL_ConfigurationException("Executed macro in file with XML declaration when using echoExecute(). This is not supported yet. Remove XML declaration or use PHPTAL->execute().");
150
            }
151
        } else if (!$this->_xmlDeclaration) {
152
            $this->_xmlDeclaration = $xmldec;
153
        }
154
    }
155
156
    /**
157
     * Activate or deactivate exception throwing during unknown path
158
     * resolution.
159
     *
160
     * @return void
161
     */
162
    public function noThrow($bool)
163
    {
164
        $this->_nothrow = $bool;
165
    }
166
167
    /**
168
     * Returns true if specified slot is filled.
169
     *
170
     * @return bool
171
     */
172
    public function hasSlot($key)
173
    {
174
        return isset($this->_slots[$key]) || ($this->_parentContext && $this->_parentContext->hasSlot($key));
175
    }
176
177
    /**
178
     * Returns the content of specified filled slot.
179
     *
180
     * Use echoSlot() whenever you just want to output the slot
181
     *
182
     * @return string
183
     */
184
    public function getSlot($key)
185
    {
186
        if (isset($this->_slots[$key])) {
187
            if (is_string($this->_slots[$key])) {
188
                return $this->_slots[$key];
189
            }
190
            ob_start();
191
            call_user_func($this->_slots[$key][0], $this->_slots[$key][1], $this->_slots[$key][2]);
192
            return ob_get_clean();
193
        } else if ($this->_parentContext) {
194
            return $this->_parentContext->getSlot($key);
195
        }
196
    }
197
198
    /**
199
     * Immediately echoes content of specified filled slot.
200
     *
201
     * Equivalent of echo $this->getSlot();
202
     *
203
     * @return string
204
     */
205
    public function echoSlot($key)
206
    {
207
        if (isset($this->_slots[$key])) {
208
            if (is_string($this->_slots[$key])) {
209
                echo $this->_slots[$key];
210
            } else {
211
                call_user_func($this->_slots[$key][0], $this->_slots[$key][1], $this->_slots[$key][2]);
212
            }
213
        } else if ($this->_parentContext) {
214
            return $this->_parentContext->echoSlot($key);
215
        }
216
    }
217
218
    /**
219
     * Fill a macro slot.
220
     *
221
     * @return void
222
     */
223
    public function fillSlot($key, $content)
224
    {
225
        $this->_slots[$key] = $content;
226
        if ($this->_parentContext) {
227
            // Works around bug with tal:define popping context after fillslot
228
            $this->_parentContext->_slots[$key] = $content;
229
        }
230
    }
231
232
    public function fillSlotCallback($key, $callback, $_thistpl, $tpl)
233
    {
234
        assert('is_callable($callback)');
235
        $this->_slots[$key] = array($callback, $_thistpl, $tpl);
236
        if ($this->_parentContext) {
237
            // Works around bug with tal:define popping context after fillslot
238
            $this->_parentContext->_slots[$key] = array($callback, $_thistpl, $tpl);
239
        }
240
    }
241
242
    /**
243
     * Push current filled slots on stack.
244
     *
245
     * @return void
246
     */
247
    public function pushSlots()
248
    {
249
        $this->_slotsStack[] =  $this->_slots;
250
        $this->_slots = array();
251
    }
252
253
    /**
254
     * Restore filled slots stack.
255
     *
256
     * @return void
257
     */
258
    public function popSlots()
259
    {
260
        $this->_slots = array_pop($this->_slotsStack);
261
    }
262
263
    /**
264
     * Context setter.
265
     *
266
     * @return void
267
     */
268
    public function __set($varname, $value)
269
    {
270
        if (preg_match('/^_|\s/', $varname)) {
271
            throw new PHPTAL_InvalidVariableNameException('Template variable error \''.$varname.'\' must not begin with underscore or contain spaces');
272
        }
273
        $this->$varname = $value;
274
    }
275
276
    /**
277
     * @return bool
278
     */
279
    public function __isset($varname)
280
    {
281
        // it doesn't need to check isset($this->$varname), because PHP does that _before_ calling __isset()
282
        return isset($this->_globalContext->$varname) || defined($varname);
283
    }
284
285
    /**
286
     * Context getter.
287
     * If variable doesn't exist, it will throw an exception, unless noThrow(true) has been called
288
     *
289
     * @return mixed
290
     */
291
    public function __get($varname)
292
    {
293
        // PHP checks public properties first, there's no need to support them here
294
295
        // must use isset() to allow custom global contexts with __isset()/__get()
296
        if (isset($this->_globalContext->$varname)) {
297
            return $this->_globalContext->$varname;
298
        }
299
300
        if (defined($varname)) {
301
            return constant($varname);
302
        }
303
304
        if ($this->_nothrow) {
305
            return null;
306
        }
307
308
        throw new PHPTAL_VariableNotFoundException("Unable to find variable '$varname' in current scope");
309
    }
310
311
    /**
312
     * helper method for PHPTAL_Context::path()
313
     *
314
     * @access private
315
     */
316
    private static function pathError($base, $path, $current, $basename)
317
    {
318
        if ($current !== $path) {
319
            $pathinfo = " (in path '.../$path')";
320
        } else $pathinfo = '';
321
322
        if (!empty($basename)) {
323
            $basename = "'" . $basename . "' ";
324
        }
325
326
        if (is_array($base)) {
327
            throw new PHPTAL_VariableNotFoundException("Array {$basename}doesn't have key named '$current'$pathinfo");
328
        }
329
        if (is_object($base)) {
330
            throw new PHPTAL_VariableNotFoundException(ucfirst(get_class($base))." object {$basename}doesn't have method/property named '$current'$pathinfo");
331
        }
332
        throw new PHPTAL_VariableNotFoundException(trim("Attempt to read property '$current'$pathinfo from ".gettype($base)." value {$basename}"));
333
    }
334
335
    /**
336
     * Resolve TALES path starting from the first path element.
337
     * The TALES path : object/method1/10/method2
338
     * will call : $ctx->path($ctx->object, 'method1/10/method2')
339
     *
340
     * This function is very important for PHPTAL performance.
341
     *
342
     * This function will become non-static in the future
343
     *
344
     * @param mixed  $base    first element of the path ($ctx)
345
     * @param string $path    rest of the path
346
     * @param bool   $nothrow is used by phptal_exists(). Prevents this function from
347
     * throwing an exception when a part of the path cannot be resolved, null is
348
     * returned instead.
349
     *
350
     * @access private
351
     * @return mixed
352
     */
353
    public static function path($base, $path, $nothrow=false)
354
    {
355
        if ($base === null) {
356
            if ($nothrow) return null;
357
            PHPTAL_Context::pathError($base, $path, $path, $path);
358
        }
359
360
        $chunks  = explode('/', $path);
361
        $current = null;
362
        $prev    = null;
363
        for ($i = 0; $i < count($chunks); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
364
            if ($current != $chunks[$i]) { // if not $i-- before continue;
365
                $prev    = $current;
366
                $current = $chunks[$i];
367
            }
368
369
            // object handling
370
            if (is_object($base)) {
371
372
                // look for method. Both method_exists and is_callable are required because of __call() and protected methods
373
                if (method_exists($base, $current) && is_callable(array($base, $current))) {
374
                    $base = $base->$current();
375
                    continue;
376
                }
377
378
                // look for property
379
                if (property_exists($base, $current)) {
380
                    $base = $base->$current;
381
                    continue;
382
                }
383
384
                if ($base instanceof ArrayAccess && $base->offsetExists($current)) {
385
                    $base = $base->offsetGet($current);
386
                    continue;
387
                }
388
389
                if (($current === 'length' || $current === 'size') && $base instanceof Countable) {
390
                    $base = count($base);
391
                    continue;
392
                }
393
394
                // look for isset (priority over __get)
395
                if (method_exists($base, '__isset')) {
396
                    if ($base->__isset($current)) {
397
                        $base = $base->$current;
398
                        continue;
399
                    }
400
                }
401
                // ask __get and discard if it returns null
402
                elseif (method_exists($base, '__get')) {
403
                    $tmp = $base->$current;
404
                    if (null !== $tmp) {
405
                        $base = $tmp;
406
                        continue;
407
                    }
408
                }
409
410
                // magic method call
411
                if (method_exists($base, '__call')) {
412
                    try
413
                    {
414
                        $base = $base->__call($current, array());
415
                        continue;
416
                    }
417
                    catch(BadMethodCallException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
418
                }
419
420
                if (is_callable($base)) {
421
                    $base = phptal_unravel_closure($base);
422
                    $i--;
423
                    continue;
424
                }
425
426
                if ($nothrow) {
427
                    return null;
428
                }
429
430
                PHPTAL_Context::pathError($base, $path, $current, $prev);
431
            }
432
433
            // array handling
434
            if (is_array($base)) {
435
                // key or index
436
                if (array_key_exists((string)$current, $base)) {
437
                    $base = $base[$current];
438
                    continue;
439
                }
440
441
                // virtual methods provided by phptal
442
                if ($current == 'length' || $current == 'size') {
443
                    $base = count($base);
444
                    continue;
445
                }
446
447
                if ($nothrow)
448
                    return null;
449
450
                PHPTAL_Context::pathError($base, $path, $current, $prev);
451
            }
452
453
            // string handling
454
            if (is_string($base)) {
455
                // virtual methods provided by phptal
456
                if ($current == 'length' || $current == 'size') {
457
                    $base = strlen($base);
458
                    continue;
459
                }
460
461
                // access char at index
462
                if (is_numeric($current)) {
463
                    $base = $base[$current];
464
                    continue;
465
                }
466
            }
467
468
            // if this point is reached, then the part cannot be resolved
469
470
            if ($nothrow)
471
                return null;
472
473
            PHPTAL_Context::pathError($base, $path, $current, $prev);
474
        }
475
476
        return $base;
477
    }
478
}
479
480
/**
481
 * @see PHPTAL_Context::path()
482
 * @deprecated
483
 */
484
function phptal_path($base, $path, $nothrow=false)
485
{
486
    return PHPTAL_Context::path($base, $path, $nothrow);
487
}
488
489
/**
490
 * helper function for chained expressions
491
 *
492
 * @param mixed $var value to check
493
 * @return bool
494
 * @access private
495
 */
496
function phptal_isempty($var)
497
{
498
    return $var === null || $var === false || $var === ''
499
           || ((is_array($var) || $var instanceof Countable) && count($var)===0);
500
}
501
502
/**
503
 * helper function for conditional expressions
504
 *
505
 * @param mixed $var value to check
506
 * @return bool
507
 * @access private
508
 */
509
function phptal_true($var)
510
{
511
    $var = phptal_unravel_closure($var);
512
    return $var && (!$var instanceof Countable || count($var));
513
}
514
515
/**
516
 * convert to string and html-escape given value (of any type)
517
 *
518
 * @access private
519
 */
520
function phptal_escape($var, $encoding)
521
{
522
    if (is_string($var)) {
523
        return htmlspecialchars($var, ENT_QUOTES, $encoding);
524
    }
525
    return htmlspecialchars(phptal_tostring($var), ENT_QUOTES, $encoding);
526
}
527
528
/**
529
 * convert anything to string
530
 *
531
 * @access private
532
 */
533
function phptal_tostring($var)
534
{
535
    if (is_string($var)) {
536
        return $var;
537
    } elseif (is_bool($var)) {
538
        return (int)$var;
539
    } elseif (is_array($var)) {
540
        return implode(', ', array_map('phptal_tostring', $var));
541
    } elseif ($var instanceof SimpleXMLElement) {
542
543
        /* There is no sane way to tell apart element and attribute nodes
544
           in SimpleXML, so here's a guess that if something has no attributes
545
           or children, and doesn't output <, then it's an attribute */
546
547
        $xml = $var->asXML();
548
        if ($xml[0] === '<' || $var->attributes() || $var->children()) {
549
            return $xml;
550
        }
551
    }
552
    return (string)phptal_unravel_closure($var);
553
}
554
555
/**
556
 * unravel the provided expression if it is a closure
557
 *
558
 * This will call the base expression and its result
559
 * as long as it is a Closure.  Once the base (non-Closure)
560
 * value is found it is returned.
561
 *
562
 * This function has no effect on non-Closure expressions
563
 */
564
function phptal_unravel_closure($var)
565
{
566
    while (is_object($var) && is_callable($var)) {
567
        $var = call_user_func($var);
568
    }
569
    return $var;
570
}
571