Project

General

Profile

1
<?php
2

    
3
/*
4
 * This file is part of Twig.
5
 *
6
 * (c) 2009 Fabien Potencier
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
/**
13
 * Stores the Twig configuration.
14
 *
15
 * @author Fabien Potencier <fabien@symfony.com>
16
 */
17
class Twig_Environment
18
{
19
    const VERSION = '1.13.1';
20

    
21
    protected $charset;
22
    protected $loader;
23
    protected $debug;
24
    protected $autoReload;
25
    protected $cache;
26
    protected $lexer;
27
    protected $parser;
28
    protected $compiler;
29
    protected $baseTemplateClass;
30
    protected $extensions;
31
    protected $parsers;
32
    protected $visitors;
33
    protected $filters;
34
    protected $tests;
35
    protected $functions;
36
    protected $globals;
37
    protected $runtimeInitialized;
38
    protected $extensionInitialized;
39
    protected $loadedTemplates;
40
    protected $strictVariables;
41
    protected $unaryOperators;
42
    protected $binaryOperators;
43
    protected $templateClassPrefix = '__TwigTemplate_';
44
    protected $functionCallbacks;
45
    protected $filterCallbacks;
46
    protected $staging;
47

    
48
    /**
49
     * Constructor.
50
     *
51
     * Available options:
52
     *
53
     *  * debug: When set to true, it automatically set "auto_reload" to true as
54
     *           well (default to false).
55
     *
56
     *  * charset: The charset used by the templates (default to UTF-8).
57
     *
58
     *  * base_template_class: The base template class to use for generated
59
     *                         templates (default to Twig_Template).
60
     *
61
     *  * cache: An absolute path where to store the compiled templates, or
62
     *           false to disable compilation cache (default).
63
     *
64
     *  * auto_reload: Whether to reload the template is the original source changed.
65
     *                 If you don't provide the auto_reload option, it will be
66
     *                 determined automatically base on the debug value.
67
     *
68
     *  * strict_variables: Whether to ignore invalid variables in templates
69
     *                      (default to false).
70
     *
71
     *  * autoescape: Whether to enable auto-escaping (default to html):
72
     *                  * false: disable auto-escaping
73
     *                  * true: equivalent to html
74
     *                  * html, js: set the autoescaping to one of the supported strategies
75
     *                  * PHP callback: a PHP callback that returns an escaping strategy based on the template "filename"
76
     *
77
     *  * optimizations: A flag that indicates which optimizations to apply
78
     *                   (default to -1 which means that all optimizations are enabled;
79
     *                   set it to 0 to disable).
80
     *
81
     * @param Twig_LoaderInterface $loader  A Twig_LoaderInterface instance
82
     * @param array                $options An array of options
83
     */
84
    public function __construct(Twig_LoaderInterface $loader = null, $options = array())
85
    {
86
        if (null !== $loader) {
87
            $this->setLoader($loader);
88
        }
89

    
90
        $options = array_merge(array(
91
            'debug'               => false,
92
            'charset'             => 'UTF-8',
93
            'base_template_class' => 'Twig_Template',
94
            'strict_variables'    => false,
95
            'autoescape'          => 'html',
96
            'cache'               => false,
97
            'auto_reload'         => null,
98
            'optimizations'       => -1,
99
        ), $options);
100

    
101
        $this->debug              = (bool) $options['debug'];
102
        $this->charset            = strtoupper($options['charset']);
103
        $this->baseTemplateClass  = $options['base_template_class'];
104
        $this->autoReload         = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
105
        $this->strictVariables    = (bool) $options['strict_variables'];
106
        $this->runtimeInitialized = false;
107
        $this->setCache($options['cache']);
108
        $this->functionCallbacks = array();
109
        $this->filterCallbacks = array();
110

    
111
        $this->addExtension(new Twig_Extension_Core());
112
        $this->addExtension(new Twig_Extension_Escaper($options['autoescape']));
113
        $this->addExtension(new Twig_Extension_Optimizer($options['optimizations']));
114
        $this->extensionInitialized = false;
115
        $this->staging = new Twig_Extension_Staging();
116
    }
117

    
118
    /**
119
     * Gets the base template class for compiled templates.
120
     *
121
     * @return string The base template class name
122
     */
123
    public function getBaseTemplateClass()
124
    {
125
        return $this->baseTemplateClass;
126
    }
127

    
128
    /**
129
     * Sets the base template class for compiled templates.
130
     *
131
     * @param string $class The base template class name
132
     */
133
    public function setBaseTemplateClass($class)
134
    {
135
        $this->baseTemplateClass = $class;
136
    }
137

    
138
    /**
139
     * Enables debugging mode.
140
     */
141
    public function enableDebug()
142
    {
143
        $this->debug = true;
144
    }
145

    
146
    /**
147
     * Disables debugging mode.
148
     */
149
    public function disableDebug()
150
    {
151
        $this->debug = false;
152
    }
153

    
154
    /**
155
     * Checks if debug mode is enabled.
156
     *
157
     * @return Boolean true if debug mode is enabled, false otherwise
158
     */
159
    public function isDebug()
160
    {
161
        return $this->debug;
162
    }
163

    
164
    /**
165
     * Enables the auto_reload option.
166
     */
167
    public function enableAutoReload()
168
    {
169
        $this->autoReload = true;
170
    }
171

    
172
    /**
173
     * Disables the auto_reload option.
174
     */
175
    public function disableAutoReload()
176
    {
177
        $this->autoReload = false;
178
    }
179

    
180
    /**
181
     * Checks if the auto_reload option is enabled.
182
     *
183
     * @return Boolean true if auto_reload is enabled, false otherwise
184
     */
185
    public function isAutoReload()
186
    {
187
        return $this->autoReload;
188
    }
189

    
190
    /**
191
     * Enables the strict_variables option.
192
     */
193
    public function enableStrictVariables()
194
    {
195
        $this->strictVariables = true;
196
    }
197

    
198
    /**
199
     * Disables the strict_variables option.
200
     */
201
    public function disableStrictVariables()
202
    {
203
        $this->strictVariables = false;
204
    }
205

    
206
    /**
207
     * Checks if the strict_variables option is enabled.
208
     *
209
     * @return Boolean true if strict_variables is enabled, false otherwise
210
     */
211
    public function isStrictVariables()
212
    {
213
        return $this->strictVariables;
214
    }
215

    
216
    /**
217
     * Gets the cache directory or false if cache is disabled.
218
     *
219
     * @return string|false
220
     */
221
    public function getCache()
222
    {
223
        return $this->cache;
224
    }
225

    
226
     /**
227
      * Sets the cache directory or false if cache is disabled.
228
      *
229
      * @param string|false $cache The absolute path to the compiled templates,
230
      *                            or false to disable cache
231
      */
232
    public function setCache($cache)
233
    {
234
        $this->cache = $cache ? $cache : false;
235
    }
236

    
237
    /**
238
     * Gets the cache filename for a given template.
239
     *
240
     * @param string $name The template name
241
     *
242
     * @return string The cache file name
243
     */
244
    public function getCacheFilename($name)
245
    {
246
        if (false === $this->cache) {
247
            return false;
248
        }
249

    
250
        $class = substr($this->getTemplateClass($name), strlen($this->templateClassPrefix));
251

    
252
        return $this->getCache().'/'.substr($class, 0, 2).'/'.substr($class, 2, 2).'/'.substr($class, 4).'.php';
253
    }
254

    
255
    /**
256
     * Gets the template class associated with the given string.
257
     *
258
     * @param string  $name  The name for which to calculate the template class name
259
     * @param integer $index The index if it is an embedded template
260
     *
261
     * @return string The template class name
262
     */
263
    public function getTemplateClass($name, $index = null)
264
    {
265
        return $this->templateClassPrefix.md5($this->getLoader()->getCacheKey($name)).(null === $index ? '' : '_'.$index);
266
    }
267

    
268
    /**
269
     * Gets the template class prefix.
270
     *
271
     * @return string The template class prefix
272
     */
273
    public function getTemplateClassPrefix()
274
    {
275
        return $this->templateClassPrefix;
276
    }
277

    
278
    /**
279
     * Renders a template.
280
     *
281
     * @param string $name    The template name
282
     * @param array  $context An array of parameters to pass to the template
283
     *
284
     * @return string The rendered template
285
     */
286
    public function render($name, array $context = array())
287
    {
288
        return $this->loadTemplate($name)->render($context);
289
    }
290

    
291
    /**
292
     * Displays a template.
293
     *
294
     * @param string $name    The template name
295
     * @param array  $context An array of parameters to pass to the template
296
     */
297
    public function display($name, array $context = array())
298
    {
299
        $this->loadTemplate($name)->display($context);
300
    }
301

    
302
    /**
303
     * Loads a template by name.
304
     *
305
     * @param string  $name  The template name
306
     * @param integer $index The index if it is an embedded template
307
     *
308
     * @return Twig_TemplateInterface A template instance representing the given template name
309
     */
310
    public function loadTemplate($name, $index = null)
311
    {
312
        $cls = $this->getTemplateClass($name, $index);
313

    
314
        if (isset($this->loadedTemplates[$cls])) {
315
            return $this->loadedTemplates[$cls];
316
        }
317

    
318
        if (!class_exists($cls, false)) {
319
            if (false === $cache = $this->getCacheFilename($name)) {
320
                eval('?>'.$this->compileSource($this->getLoader()->getSource($name), $name));
321
            } else {
322
                if (!is_file($cache) || ($this->isAutoReload() && !$this->isTemplateFresh($name, filemtime($cache)))) {
323
                    $this->writeCacheFile($cache, $this->compileSource($this->getLoader()->getSource($name), $name));
324
                }
325

    
326
                require_once $cache;
327
            }
328
        }
329

    
330
        if (!$this->runtimeInitialized) {
331
            $this->initRuntime();
332
        }
333

    
334
        return $this->loadedTemplates[$cls] = new $cls($this);
335
    }
336

    
337
    /**
338
     * Returns true if the template is still fresh.
339
     *
340
     * Besides checking the loader for freshness information,
341
     * this method also checks if the enabled extensions have
342
     * not changed.
343
     *
344
     * @param string    $name The template name
345
     * @param timestamp $time The last modification time of the cached template
346
     *
347
     * @return Boolean true if the template is fresh, false otherwise
348
     */
349
    public function isTemplateFresh($name, $time)
350
    {
351
        foreach ($this->extensions as $extension) {
352
            $r = new ReflectionObject($extension);
353
            if (filemtime($r->getFileName()) > $time) {
354
                return false;
355
            }
356
        }
357

    
358
        return $this->getLoader()->isFresh($name, $time);
359
    }
360

    
361
    public function resolveTemplate($names)
362
    {
363
        if (!is_array($names)) {
364
            $names = array($names);
365
        }
366

    
367
        foreach ($names as $name) {
368
            if ($name instanceof Twig_Template) {
369
                return $name;
370
            }
371

    
372
            try {
373
                return $this->loadTemplate($name);
374
            } catch (Twig_Error_Loader $e) {
375
            }
376
        }
377

    
378
        if (1 === count($names)) {
379
            throw $e;
380
        }
381

    
382
        throw new Twig_Error_Loader(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
383
    }
384

    
385
    /**
386
     * Clears the internal template cache.
387
     */
388
    public function clearTemplateCache()
389
    {
390
        $this->loadedTemplates = array();
391
    }
392

    
393
    /**
394
     * Clears the template cache files on the filesystem.
395
     */
396
    public function clearCacheFiles()
397
    {
398
        if (false === $this->cache) {
399
            return;
400
        }
401

    
402
        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->cache), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
403
            if ($file->isFile()) {
404
                @unlink($file->getPathname());
405
            }
406
        }
407
    }
408

    
409
    /**
410
     * Gets the Lexer instance.
411
     *
412
     * @return Twig_LexerInterface A Twig_LexerInterface instance
413
     */
414
    public function getLexer()
415
    {
416
        if (null === $this->lexer) {
417
            $this->lexer = new Twig_Lexer($this);
418
        }
419

    
420
        return $this->lexer;
421
    }
422

    
423
    /**
424
     * Sets the Lexer instance.
425
     *
426
     * @param Twig_LexerInterface A Twig_LexerInterface instance
427
     */
428
    public function setLexer(Twig_LexerInterface $lexer)
429
    {
430
        $this->lexer = $lexer;
431
    }
432

    
433
    /**
434
     * Tokenizes a source code.
435
     *
436
     * @param string $source The template source code
437
     * @param string $name   The template name
438
     *
439
     * @return Twig_TokenStream A Twig_TokenStream instance
440
     */
441
    public function tokenize($source, $name = null)
442
    {
443
        return $this->getLexer()->tokenize($source, $name);
444
    }
445

    
446
    /**
447
     * Gets the Parser instance.
448
     *
449
     * @return Twig_ParserInterface A Twig_ParserInterface instance
450
     */
451
    public function getParser()
452
    {
453
        if (null === $this->parser) {
454
            $this->parser = new Twig_Parser($this);
455
        }
456

    
457
        return $this->parser;
458
    }
459

    
460
    /**
461
     * Sets the Parser instance.
462
     *
463
     * @param Twig_ParserInterface A Twig_ParserInterface instance
464
     */
465
    public function setParser(Twig_ParserInterface $parser)
466
    {
467
        $this->parser = $parser;
468
    }
469

    
470
    /**
471
     * Parses a token stream.
472
     *
473
     * @param Twig_TokenStream $tokens A Twig_TokenStream instance
474
     *
475
     * @return Twig_Node_Module A Node tree
476
     */
477
    public function parse(Twig_TokenStream $tokens)
478
    {
479
        return $this->getParser()->parse($tokens);
480
    }
481

    
482
    /**
483
     * Gets the Compiler instance.
484
     *
485
     * @return Twig_CompilerInterface A Twig_CompilerInterface instance
486
     */
487
    public function getCompiler()
488
    {
489
        if (null === $this->compiler) {
490
            $this->compiler = new Twig_Compiler($this);
491
        }
492

    
493
        return $this->compiler;
494
    }
495

    
496
    /**
497
     * Sets the Compiler instance.
498
     *
499
     * @param Twig_CompilerInterface $compiler A Twig_CompilerInterface instance
500
     */
501
    public function setCompiler(Twig_CompilerInterface $compiler)
502
    {
503
        $this->compiler = $compiler;
504
    }
505

    
506
    /**
507
     * Compiles a Node.
508
     *
509
     * @param Twig_NodeInterface $node A Twig_NodeInterface instance
510
     *
511
     * @return string The compiled PHP source code
512
     */
513
    public function compile(Twig_NodeInterface $node)
514
    {
515
        return $this->getCompiler()->compile($node)->getSource();
516
    }
517

    
518
    /**
519
     * Compiles a template source code.
520
     *
521
     * @param string $source The template source code
522
     * @param string $name   The template name
523
     *
524
     * @return string The compiled PHP source code
525
     */
526
    public function compileSource($source, $name = null)
527
    {
528
        try {
529
            return $this->compile($this->parse($this->tokenize($source, $name)));
530
        } catch (Twig_Error $e) {
531
            $e->setTemplateFile($name);
532
            throw $e;
533
        } catch (Exception $e) {
534
            throw new Twig_Error_Runtime(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $name, $e);
535
        }
536
    }
537

    
538
    /**
539
     * Sets the Loader instance.
540
     *
541
     * @param Twig_LoaderInterface $loader A Twig_LoaderInterface instance
542
     */
543
    public function setLoader(Twig_LoaderInterface $loader)
544
    {
545
        $this->loader = $loader;
546
    }
547

    
548
    /**
549
     * Gets the Loader instance.
550
     *
551
     * @return Twig_LoaderInterface A Twig_LoaderInterface instance
552
     */
553
    public function getLoader()
554
    {
555
        if (null === $this->loader) {
556
            throw new LogicException('You must set a loader first.');
557
        }
558

    
559
        return $this->loader;
560
    }
561

    
562
    /**
563
     * Sets the default template charset.
564
     *
565
     * @param string $charset The default charset
566
     */
567
    public function setCharset($charset)
568
    {
569
        $this->charset = strtoupper($charset);
570
    }
571

    
572
    /**
573
     * Gets the default template charset.
574
     *
575
     * @return string The default charset
576
     */
577
    public function getCharset()
578
    {
579
        return $this->charset;
580
    }
581

    
582
    /**
583
     * Initializes the runtime environment.
584
     */
585
    public function initRuntime()
586
    {
587
        $this->runtimeInitialized = true;
588

    
589
        foreach ($this->getExtensions() as $extension) {
590
            $extension->initRuntime($this);
591
        }
592
    }
593

    
594
    /**
595
     * Returns true if the given extension is registered.
596
     *
597
     * @param string $name The extension name
598
     *
599
     * @return Boolean Whether the extension is registered or not
600
     */
601
    public function hasExtension($name)
602
    {
603
        return isset($this->extensions[$name]);
604
    }
605

    
606
    /**
607
     * Gets an extension by name.
608
     *
609
     * @param string $name The extension name
610
     *
611
     * @return Twig_ExtensionInterface A Twig_ExtensionInterface instance
612
     */
613
    public function getExtension($name)
614
    {
615
        if (!isset($this->extensions[$name])) {
616
            throw new Twig_Error_Runtime(sprintf('The "%s" extension is not enabled.', $name));
617
        }
618

    
619
        return $this->extensions[$name];
620
    }
621

    
622
    /**
623
     * Registers an extension.
624
     *
625
     * @param Twig_ExtensionInterface $extension A Twig_ExtensionInterface instance
626
     */
627
    public function addExtension(Twig_ExtensionInterface $extension)
628
    {
629
        if ($this->extensionInitialized) {
630
            throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName()));
631
        }
632

    
633
        $this->extensions[$extension->getName()] = $extension;
634
    }
635

    
636
    /**
637
     * Removes an extension by name.
638
     *
639
     * This method is deprecated and you should not use it.
640
     *
641
     * @param string $name The extension name
642
     *
643
     * @deprecated since 1.12 (to be removed in 2.0)
644
     */
645
    public function removeExtension($name)
646
    {
647
        if ($this->extensionInitialized) {
648
            throw new LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name));
649
        }
650

    
651
        unset($this->extensions[$name]);
652
    }
653

    
654
    /**
655
     * Registers an array of extensions.
656
     *
657
     * @param array $extensions An array of extensions
658
     */
659
    public function setExtensions(array $extensions)
660
    {
661
        foreach ($extensions as $extension) {
662
            $this->addExtension($extension);
663
        }
664
    }
665

    
666
    /**
667
     * Returns all registered extensions.
668
     *
669
     * @return array An array of extensions
670
     */
671
    public function getExtensions()
672
    {
673
        return $this->extensions;
674
    }
675

    
676
    /**
677
     * Registers a Token Parser.
678
     *
679
     * @param Twig_TokenParserInterface $parser A Twig_TokenParserInterface instance
680
     */
681
    public function addTokenParser(Twig_TokenParserInterface $parser)
682
    {
683
        if ($this->extensionInitialized) {
684
            throw new LogicException('Unable to add a token parser as extensions have already been initialized.');
685
        }
686

    
687
        $this->staging->addTokenParser($parser);
688
    }
689

    
690
    /**
691
     * Gets the registered Token Parsers.
692
     *
693
     * @return Twig_TokenParserBrokerInterface A broker containing token parsers
694
     */
695
    public function getTokenParsers()
696
    {
697
        if (!$this->extensionInitialized) {
698
            $this->initExtensions();
699
        }
700

    
701
        return $this->parsers;
702
    }
703

    
704
    /**
705
     * Gets registered tags.
706
     *
707
     * Be warned that this method cannot return tags defined by Twig_TokenParserBrokerInterface classes.
708
     *
709
     * @return Twig_TokenParserInterface[] An array of Twig_TokenParserInterface instances
710
     */
711
    public function getTags()
712
    {
713
        $tags = array();
714
        foreach ($this->getTokenParsers()->getParsers() as $parser) {
715
            if ($parser instanceof Twig_TokenParserInterface) {
716
                $tags[$parser->getTag()] = $parser;
717
            }
718
        }
719

    
720
        return $tags;
721
    }
722

    
723
    /**
724
     * Registers a Node Visitor.
725
     *
726
     * @param Twig_NodeVisitorInterface $visitor A Twig_NodeVisitorInterface instance
727
     */
728
    public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
729
    {
730
        if ($this->extensionInitialized) {
731
            throw new LogicException('Unable to add a node visitor as extensions have already been initialized.', $extension->getName());
732
        }
733

    
734
        $this->staging->addNodeVisitor($visitor);
735
    }
736

    
737
    /**
738
     * Gets the registered Node Visitors.
739
     *
740
     * @return Twig_NodeVisitorInterface[] An array of Twig_NodeVisitorInterface instances
741
     */
742
    public function getNodeVisitors()
743
    {
744
        if (!$this->extensionInitialized) {
745
            $this->initExtensions();
746
        }
747

    
748
        return $this->visitors;
749
    }
750

    
751
    /**
752
     * Registers a Filter.
753
     *
754
     * @param string|Twig_SimpleFilter               $name   The filter name or a Twig_SimpleFilter instance
755
     * @param Twig_FilterInterface|Twig_SimpleFilter $filter A Twig_FilterInterface instance or a Twig_SimpleFilter instance
756
     */
757
    public function addFilter($name, $filter = null)
758
    {
759
        if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) {
760
            throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter');
761
        }
762

    
763
        if ($name instanceof Twig_SimpleFilter) {
764
            $filter = $name;
765
            $name = $filter->getName();
766
        }
767
        
768
        if ($this->extensionInitialized) {
769
            throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name));
770
        }
771
        
772
        $this->staging->addFilter($name, $filter);
773
    }
774

    
775
    /**
776
     * Get a filter by name.
777
     *
778
     * Subclasses may override this method and load filters differently;
779
     * so no list of filters is available.
780
     *
781
     * @param string $name The filter name
782
     *
783
     * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exist
784
     */
785
    public function getFilter($name)
786
    {
787
        if (!$this->extensionInitialized) {
788
            $this->initExtensions();
789
        }
790

    
791
        if (isset($this->filters[$name])) {
792
            return $this->filters[$name];
793
        }
794

    
795
        foreach ($this->filters as $pattern => $filter) {
796
            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
797

    
798
            if ($count) {
799
                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
800
                    array_shift($matches);
801
                    $filter->setArguments($matches);
802

    
803
                    return $filter;
804
                }
805
            }
806
        }
807

    
808
        foreach ($this->filterCallbacks as $callback) {
809
            if (false !== $filter = call_user_func($callback, $name)) {
810
                return $filter;
811
            }
812
        }
813

    
814
        return false;
815
    }
816

    
817
    public function registerUndefinedFilterCallback($callable)
818
    {
819
        $this->filterCallbacks[] = $callable;
820
    }
821

    
822
    /**
823
     * Gets the registered Filters.
824
     *
825
     * Be warned that this method cannot return filters defined with registerUndefinedFunctionCallback.
826
     *
827
     * @return Twig_FilterInterface[] An array of Twig_FilterInterface instances
828
     *
829
     * @see registerUndefinedFilterCallback
830
     */
831
    public function getFilters()
832
    {
833
        if (!$this->extensionInitialized) {
834
            $this->initExtensions();
835
        }
836

    
837
        return $this->filters;
838
    }
839

    
840
    /**
841
     * Registers a Test.
842
     *
843
     * @param string|Twig_SimpleTest             $name The test name or a Twig_SimpleTest instance
844
     * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance
845
     */
846
    public function addTest($name, $test = null)
847
    {
848
        if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) {
849
            throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest');
850
        }
851

    
852
        if ($name instanceof Twig_SimpleTest) {
853
            $test = $name;
854
            $name = $test->getName();
855
        }
856
        
857
        if ($this->extensionInitialized) {
858
            throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name));
859
        }
860

    
861
        $this->staging->addTest($name, $test);
862
    }
863

    
864
    /**
865
     * Gets the registered Tests.
866
     *
867
     * @return Twig_TestInterface[] An array of Twig_TestInterface instances
868
     */
869
    public function getTests()
870
    {
871
        if (!$this->extensionInitialized) {
872
            $this->initExtensions();
873
        }
874

    
875
        return $this->tests;
876
    }
877

    
878
    /**
879
     * Gets a test by name.
880
     *
881
     * @param string $name The test name
882
     *
883
     * @return Twig_Test|false A Twig_Test instance or false if the test does not exist
884
     */
885
    public function getTest($name)
886
    {
887
        if (!$this->extensionInitialized) {
888
            $this->initExtensions();
889
        }
890

    
891
        if (isset($this->tests[$name])) {
892
            return $this->tests[$name];
893
        }
894

    
895
        return false;
896
    }
897

    
898
    /**
899
     * Registers a Function.
900
     *
901
     * @param string|Twig_SimpleFunction                 $name     The function name or a Twig_SimpleFunction instance
902
     * @param Twig_FunctionInterface|Twig_SimpleFunction $function A Twig_FunctionInterface instance or a Twig_SimpleFunction instance
903
     */
904
    public function addFunction($name, $function = null)
905
    {
906
        if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) {
907
            throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction');
908
        }
909

    
910
        if ($name instanceof Twig_SimpleFunction) {
911
            $function = $name;
912
            $name = $function->getName();
913
        }
914
        
915
        if ($this->extensionInitialized) {
916
            throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name));
917
        }
918
        
919
        $this->staging->addFunction($name, $function);
920
    }
921

    
922
    /**
923
     * Get a function by name.
924
     *
925
     * Subclasses may override this method and load functions differently;
926
     * so no list of functions is available.
927
     *
928
     * @param string $name function name
929
     *
930
     * @return Twig_Function|false A Twig_Function instance or false if the function does not exist
931
     */
932
    public function getFunction($name)
933
    {
934
        if (!$this->extensionInitialized) {
935
            $this->initExtensions();
936
        }
937

    
938
        if (isset($this->functions[$name])) {
939
            return $this->functions[$name];
940
        }
941

    
942
        foreach ($this->functions as $pattern => $function) {
943
            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
944

    
945
            if ($count) {
946
                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
947
                    array_shift($matches);
948
                    $function->setArguments($matches);
949

    
950
                    return $function;
951
                }
952
            }
953
        }
954

    
955
        foreach ($this->functionCallbacks as $callback) {
956
            if (false !== $function = call_user_func($callback, $name)) {
957
                return $function;
958
            }
959
        }
960

    
961
        return false;
962
    }
963

    
964
    public function registerUndefinedFunctionCallback($callable)
965
    {
966
        $this->functionCallbacks[] = $callable;
967
    }
968

    
969
    /**
970
     * Gets registered functions.
971
     *
972
     * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback.
973
     *
974
     * @return Twig_FunctionInterface[] An array of Twig_FunctionInterface instances
975
     *
976
     * @see registerUndefinedFunctionCallback
977
     */
978
    public function getFunctions()
979
    {
980
        if (!$this->extensionInitialized) {
981
            $this->initExtensions();
982
        }
983

    
984
        return $this->functions;
985
    }
986

    
987
    /**
988
     * Registers a Global.
989
     *
990
     * New globals can be added before compiling or rendering a template;
991
     * but after, you can only update existing globals.
992
     *
993
     * @param string $name  The global name
994
     * @param mixed  $value The global value
995
     */
996
    public function addGlobal($name, $value)
997
    {
998
        if ($this->extensionInitialized || $this->runtimeInitialized) {
999
            if (null === $this->globals) {
1000
                $this->globals = $this->initGlobals();
1001
            }
1002

    
1003
            /* This condition must be uncommented in Twig 2.0
1004
            if (!array_key_exists($name, $this->globals)) {
1005
                throw new LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
1006
            }
1007
            */
1008
        }
1009

    
1010
        if ($this->extensionInitialized || $this->runtimeInitialized) {
1011
            // update the value
1012
            $this->globals[$name] = $value;
1013
        } else {
1014
            $this->staging->addGlobal($name, $value);
1015
        }
1016
    }
1017

    
1018
    /**
1019
     * Gets the registered Globals.
1020
     *
1021
     * @return array An array of globals
1022
     */
1023
    public function getGlobals()
1024
    {
1025
        if (!$this->runtimeInitialized && !$this->extensionInitialized) {
1026
            return $this->initGlobals();
1027
        }
1028

    
1029
        if (null === $this->globals) {
1030
            $this->globals = $this->initGlobals();
1031
        }
1032

    
1033
        return $this->globals;
1034
    }
1035

    
1036
    /**
1037
     * Merges a context with the defined globals.
1038
     *
1039
     * @param array $context An array representing the context
1040
     *
1041
     * @return array The context merged with the globals
1042
     */
1043
    public function mergeGlobals(array $context)
1044
    {
1045
        // we don't use array_merge as the context being generally
1046
        // bigger than globals, this code is faster.
1047
        foreach ($this->getGlobals() as $key => $value) {
1048
            if (!array_key_exists($key, $context)) {
1049
                $context[$key] = $value;
1050
            }
1051
        }
1052

    
1053
        return $context;
1054
    }
1055

    
1056
    /**
1057
     * Gets the registered unary Operators.
1058
     *
1059
     * @return array An array of unary operators
1060
     */
1061
    public function getUnaryOperators()
1062
    {
1063
        if (!$this->extensionInitialized) {
1064
            $this->initExtensions();
1065
        }
1066

    
1067
        return $this->unaryOperators;
1068
    }
1069

    
1070
    /**
1071
     * Gets the registered binary Operators.
1072
     *
1073
     * @return array An array of binary operators
1074
     */
1075
    public function getBinaryOperators()
1076
    {
1077
        if (!$this->extensionInitialized) {
1078
            $this->initExtensions();
1079
        }
1080

    
1081
        return $this->binaryOperators;
1082
    }
1083

    
1084
    public function computeAlternatives($name, $items)
1085
    {
1086
        $alternatives = array();
1087
        foreach ($items as $item) {
1088
            $lev = levenshtein($name, $item);
1089
            if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
1090
                $alternatives[$item] = $lev;
1091
            }
1092
        }
1093
        asort($alternatives);
1094

    
1095
        return array_keys($alternatives);
1096
    }
1097

    
1098
    protected function initGlobals()
1099
    {
1100
        $globals = array();
1101
        foreach ($this->extensions as $extension) {
1102
            $extGlob = $extension->getGlobals();
1103
            if (!is_array($extGlob)) {
1104
                throw new UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', get_class($extension)));
1105
            }
1106

    
1107
            $globals[] = $extGlob;
1108
        }
1109

    
1110
        $globals[] = $this->staging->getGlobals();
1111

    
1112
        return call_user_func_array('array_merge', $globals);
1113
    }
1114

    
1115
    protected function initExtensions()
1116
    {
1117
        if ($this->extensionInitialized) {
1118
            return;
1119
        }
1120

    
1121
        $this->extensionInitialized = true;
1122
        $this->parsers = new Twig_TokenParserBroker();
1123
        $this->filters = array();
1124
        $this->functions = array();
1125
        $this->tests = array();
1126
        $this->visitors = array();
1127
        $this->unaryOperators = array();
1128
        $this->binaryOperators = array();
1129

    
1130
        foreach ($this->extensions as $extension) {
1131
            $this->initExtension($extension);
1132
        }
1133
        $this->initExtension($this->staging);
1134
    }
1135

    
1136
    protected function initExtension(Twig_ExtensionInterface $extension)
1137
    {
1138
        // filters
1139
        foreach ($extension->getFilters() as $name => $filter) {
1140
            if ($name instanceof Twig_SimpleFilter) {
1141
                $filter = $name;
1142
                $name = $filter->getName();
1143
            } elseif ($filter instanceof Twig_SimpleFilter) {
1144
                $name = $filter->getName();
1145
            }
1146

    
1147
            $this->filters[$name] = $filter;
1148
        }
1149

    
1150
        // functions
1151
        foreach ($extension->getFunctions() as $name => $function) {
1152
            if ($name instanceof Twig_SimpleFunction) {
1153
                $function = $name;
1154
                $name = $function->getName();
1155
            } elseif ($function instanceof Twig_SimpleFunction) {
1156
                $name = $function->getName();
1157
            }
1158

    
1159
            $this->functions[$name] = $function;
1160
        }
1161

    
1162
        // tests
1163
        foreach ($extension->getTests() as $name => $test) {
1164
            if ($name instanceof Twig_SimpleTest) {
1165
                $test = $name;
1166
                $name = $test->getName();
1167
            } elseif ($test instanceof Twig_SimpleTest) {
1168
                $name = $test->getName();
1169
            }
1170

    
1171
            $this->tests[$name] = $test;
1172
        }
1173

    
1174
        // token parsers
1175
        foreach ($extension->getTokenParsers() as $parser) {
1176
            if ($parser instanceof Twig_TokenParserInterface) {
1177
                $this->parsers->addTokenParser($parser);
1178
            } elseif ($parser instanceof Twig_TokenParserBrokerInterface) {
1179
                $this->parsers->addTokenParserBroker($parser);
1180
            } else {
1181
                throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances');
1182
            }
1183
        }
1184

    
1185
        // node visitors
1186
        foreach ($extension->getNodeVisitors() as $visitor) {
1187
            $this->visitors[] = $visitor;
1188
        }
1189

    
1190
        // operators
1191
        if ($operators = $extension->getOperators()) {
1192
            if (2 !== count($operators)) {
1193
                throw new InvalidArgumentException(sprintf('"%s::getOperators()" does not return a valid operators array.', get_class($extension)));
1194
            }
1195

    
1196
            $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);
1197
            $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]);
1198
        }
1199
    }
1200

    
1201
    protected function writeCacheFile($file, $content)
1202
    {
1203
        $dir = dirname($file);
1204
        if (!is_dir($dir)) {
1205
            if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
1206
                throw new RuntimeException(sprintf("Unable to create the cache directory (%s).", $dir));
1207
            }
1208
        } elseif (!is_writable($dir)) {
1209
            throw new RuntimeException(sprintf("Unable to write in the cache directory (%s).", $dir));
1210
        }
1211

    
1212
        $tmpFile = tempnam(dirname($file), basename($file));
1213
        if (false !== @file_put_contents($tmpFile, $content)) {
1214
            // rename does not work on Win32 before 5.2.6
1215
            if (@rename($tmpFile, $file) || (@copy($tmpFile, $file) && unlink($tmpFile))) {
1216
                @chmod($file, 0666 & ~umask());
1217

    
1218
                return;
1219
            }
1220
        }
1221

    
1222
        throw new RuntimeException(sprintf('Failed to write cache file "%s".', $file));
1223
    }
1224
}
(4-4/40)