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.15.0';
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 if the original source changed.
65
     *                 If you don't provide the auto_reload option, it will be
66
     *                 determined automatically based 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|false The cache file name or false when caching is disabled
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.hash('sha256', $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
     * @throws Twig_Error_Loader  When the template cannot be found
287
     * @throws Twig_Error_Syntax  When an error occurred during compilation
288
     * @throws Twig_Error_Runtime When an error occurred during rendering
289
     */
290
    public function render($name, array $context = array())
291
    {
292
        return $this->loadTemplate($name)->render($context);
293
    }
294

    
295
    /**
296
     * Displays a template.
297
     *
298
     * @param string $name    The template name
299
     * @param array  $context An array of parameters to pass to the template
300
     *
301
     * @throws Twig_Error_Loader  When the template cannot be found
302
     * @throws Twig_Error_Syntax  When an error occurred during compilation
303
     * @throws Twig_Error_Runtime When an error occurred during rendering
304
     */
305
    public function display($name, array $context = array())
306
    {
307
        $this->loadTemplate($name)->display($context);
308
    }
309

    
310
    /**
311
     * Loads a template by name.
312
     *
313
     * @param string  $name  The template name
314
     * @param integer $index The index if it is an embedded template
315
     *
316
     * @return Twig_TemplateInterface A template instance representing the given template name
317
     *
318
     * @throws Twig_Error_Loader When the template cannot be found
319
     * @throws Twig_Error_Syntax When an error occurred during compilation
320
     */
321
    public function loadTemplate($name, $index = null)
322
    {
323
        $cls = $this->getTemplateClass($name, $index);
324

    
325
        if (isset($this->loadedTemplates[$cls])) {
326
            return $this->loadedTemplates[$cls];
327
        }
328

    
329
        if (!class_exists($cls, false)) {
330
            if (false === $cache = $this->getCacheFilename($name)) {
331
                eval('?>'.$this->compileSource($this->getLoader()->getSource($name), $name));
332
            } else {
333
                if (!is_file($cache) || ($this->isAutoReload() && !$this->isTemplateFresh($name, filemtime($cache)))) {
334
                    $this->writeCacheFile($cache, $this->compileSource($this->getLoader()->getSource($name), $name));
335
                }
336

    
337
                require_once $cache;
338
            }
339
        }
340

    
341
        if (!$this->runtimeInitialized) {
342
            $this->initRuntime();
343
        }
344

    
345
        return $this->loadedTemplates[$cls] = new $cls($this);
346
    }
347

    
348
    /**
349
     * Returns true if the template is still fresh.
350
     *
351
     * Besides checking the loader for freshness information,
352
     * this method also checks if the enabled extensions have
353
     * not changed.
354
     *
355
     * @param string    $name The template name
356
     * @param timestamp $time The last modification time of the cached template
357
     *
358
     * @return Boolean true if the template is fresh, false otherwise
359
     */
360
    public function isTemplateFresh($name, $time)
361
    {
362
        foreach ($this->extensions as $extension) {
363
            $r = new ReflectionObject($extension);
364
            if (filemtime($r->getFileName()) > $time) {
365
                return false;
366
            }
367
        }
368

    
369
        return $this->getLoader()->isFresh($name, $time);
370
    }
371

    
372
    /**
373
     * Tries to load a template consecutively from an array.
374
     *
375
     * Similar to loadTemplate() but it also accepts Twig_TemplateInterface instances and an array
376
     * of templates where each is tried to be loaded.
377
     *
378
     * @param string|Twig_Template|array $names A template or an array of templates to try consecutively
379
     *
380
     * @return Twig_Template
381
     *
382
     * @throws Twig_Error_Loader When none of the templates can be found
383
     * @throws Twig_Error_Syntax When an error occurred during compilation
384
     */
385
    public function resolveTemplate($names)
386
    {
387
        if (!is_array($names)) {
388
            $names = array($names);
389
        }
390

    
391
        foreach ($names as $name) {
392
            if ($name instanceof Twig_Template) {
393
                return $name;
394
            }
395

    
396
            try {
397
                return $this->loadTemplate($name);
398
            } catch (Twig_Error_Loader $e) {
399
            }
400
        }
401

    
402
        if (1 === count($names)) {
403
            throw $e;
404
        }
405

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

    
409
    /**
410
     * Clears the internal template cache.
411
     */
412
    public function clearTemplateCache()
413
    {
414
        $this->loadedTemplates = array();
415
    }
416

    
417
    /**
418
     * Clears the template cache files on the filesystem.
419
     */
420
    public function clearCacheFiles()
421
    {
422
        if (false === $this->cache) {
423
            return;
424
        }
425

    
426
        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->cache), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
427
            if ($file->isFile()) {
428
                @unlink($file->getPathname());
429
            }
430
        }
431
    }
432

    
433
    /**
434
     * Gets the Lexer instance.
435
     *
436
     * @return Twig_LexerInterface A Twig_LexerInterface instance
437
     */
438
    public function getLexer()
439
    {
440
        if (null === $this->lexer) {
441
            $this->lexer = new Twig_Lexer($this);
442
        }
443

    
444
        return $this->lexer;
445
    }
446

    
447
    /**
448
     * Sets the Lexer instance.
449
     *
450
     * @param Twig_LexerInterface A Twig_LexerInterface instance
451
     */
452
    public function setLexer(Twig_LexerInterface $lexer)
453
    {
454
        $this->lexer = $lexer;
455
    }
456

    
457
    /**
458
     * Tokenizes a source code.
459
     *
460
     * @param string $source The template source code
461
     * @param string $name   The template name
462
     *
463
     * @return Twig_TokenStream A Twig_TokenStream instance
464
     *
465
     * @throws Twig_Error_Syntax When the code is syntactically wrong
466
     */
467
    public function tokenize($source, $name = null)
468
    {
469
        return $this->getLexer()->tokenize($source, $name);
470
    }
471

    
472
    /**
473
     * Gets the Parser instance.
474
     *
475
     * @return Twig_ParserInterface A Twig_ParserInterface instance
476
     */
477
    public function getParser()
478
    {
479
        if (null === $this->parser) {
480
            $this->parser = new Twig_Parser($this);
481
        }
482

    
483
        return $this->parser;
484
    }
485

    
486
    /**
487
     * Sets the Parser instance.
488
     *
489
     * @param Twig_ParserInterface A Twig_ParserInterface instance
490
     */
491
    public function setParser(Twig_ParserInterface $parser)
492
    {
493
        $this->parser = $parser;
494
    }
495

    
496
    /**
497
     * Converts a token stream to a node tree.
498
     *
499
     * @param Twig_TokenStream $stream A token stream instance
500
     *
501
     * @return Twig_Node_Module A node tree
502
     *
503
     * @throws Twig_Error_Syntax When the token stream is syntactically or semantically wrong
504
     */
505
    public function parse(Twig_TokenStream $stream)
506
    {
507
        return $this->getParser()->parse($stream);
508
    }
509

    
510
    /**
511
     * Gets the Compiler instance.
512
     *
513
     * @return Twig_CompilerInterface A Twig_CompilerInterface instance
514
     */
515
    public function getCompiler()
516
    {
517
        if (null === $this->compiler) {
518
            $this->compiler = new Twig_Compiler($this);
519
        }
520

    
521
        return $this->compiler;
522
    }
523

    
524
    /**
525
     * Sets the Compiler instance.
526
     *
527
     * @param Twig_CompilerInterface $compiler A Twig_CompilerInterface instance
528
     */
529
    public function setCompiler(Twig_CompilerInterface $compiler)
530
    {
531
        $this->compiler = $compiler;
532
    }
533

    
534
    /**
535
     * Compiles a node and returns the PHP code.
536
     *
537
     * @param Twig_NodeInterface $node A Twig_NodeInterface instance
538
     *
539
     * @return string The compiled PHP source code
540
     */
541
    public function compile(Twig_NodeInterface $node)
542
    {
543
        return $this->getCompiler()->compile($node)->getSource();
544
    }
545

    
546
    /**
547
     * Compiles a template source code.
548
     *
549
     * @param string $source The template source code
550
     * @param string $name   The template name
551
     *
552
     * @return string The compiled PHP source code
553
     *
554
     * @throws Twig_Error_Syntax When there was an error during tokenizing, parsing or compiling
555
     */
556
    public function compileSource($source, $name = null)
557
    {
558
        try {
559
            return $this->compile($this->parse($this->tokenize($source, $name)));
560
        } catch (Twig_Error $e) {
561
            $e->setTemplateFile($name);
562
            throw $e;
563
        } catch (Exception $e) {
564
            throw new Twig_Error_Syntax(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $name, $e);
565
        }
566
    }
567

    
568
    /**
569
     * Sets the Loader instance.
570
     *
571
     * @param Twig_LoaderInterface $loader A Twig_LoaderInterface instance
572
     */
573
    public function setLoader(Twig_LoaderInterface $loader)
574
    {
575
        $this->loader = $loader;
576
    }
577

    
578
    /**
579
     * Gets the Loader instance.
580
     *
581
     * @return Twig_LoaderInterface A Twig_LoaderInterface instance
582
     */
583
    public function getLoader()
584
    {
585
        if (null === $this->loader) {
586
            throw new LogicException('You must set a loader first.');
587
        }
588

    
589
        return $this->loader;
590
    }
591

    
592
    /**
593
     * Sets the default template charset.
594
     *
595
     * @param string $charset The default charset
596
     */
597
    public function setCharset($charset)
598
    {
599
        $this->charset = strtoupper($charset);
600
    }
601

    
602
    /**
603
     * Gets the default template charset.
604
     *
605
     * @return string The default charset
606
     */
607
    public function getCharset()
608
    {
609
        return $this->charset;
610
    }
611

    
612
    /**
613
     * Initializes the runtime environment.
614
     */
615
    public function initRuntime()
616
    {
617
        $this->runtimeInitialized = true;
618

    
619
        foreach ($this->getExtensions() as $extension) {
620
            $extension->initRuntime($this);
621
        }
622
    }
623

    
624
    /**
625
     * Returns true if the given extension is registered.
626
     *
627
     * @param string $name The extension name
628
     *
629
     * @return Boolean Whether the extension is registered or not
630
     */
631
    public function hasExtension($name)
632
    {
633
        return isset($this->extensions[$name]);
634
    }
635

    
636
    /**
637
     * Gets an extension by name.
638
     *
639
     * @param string $name The extension name
640
     *
641
     * @return Twig_ExtensionInterface A Twig_ExtensionInterface instance
642
     */
643
    public function getExtension($name)
644
    {
645
        if (!isset($this->extensions[$name])) {
646
            throw new Twig_Error_Runtime(sprintf('The "%s" extension is not enabled.', $name));
647
        }
648

    
649
        return $this->extensions[$name];
650
    }
651

    
652
    /**
653
     * Registers an extension.
654
     *
655
     * @param Twig_ExtensionInterface $extension A Twig_ExtensionInterface instance
656
     */
657
    public function addExtension(Twig_ExtensionInterface $extension)
658
    {
659
        if ($this->extensionInitialized) {
660
            throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName()));
661
        }
662

    
663
        $this->extensions[$extension->getName()] = $extension;
664
    }
665

    
666
    /**
667
     * Removes an extension by name.
668
     *
669
     * This method is deprecated and you should not use it.
670
     *
671
     * @param string $name The extension name
672
     *
673
     * @deprecated since 1.12 (to be removed in 2.0)
674
     */
675
    public function removeExtension($name)
676
    {
677
        if ($this->extensionInitialized) {
678
            throw new LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name));
679
        }
680

    
681
        unset($this->extensions[$name]);
682
    }
683

    
684
    /**
685
     * Registers an array of extensions.
686
     *
687
     * @param array $extensions An array of extensions
688
     */
689
    public function setExtensions(array $extensions)
690
    {
691
        foreach ($extensions as $extension) {
692
            $this->addExtension($extension);
693
        }
694
    }
695

    
696
    /**
697
     * Returns all registered extensions.
698
     *
699
     * @return array An array of extensions
700
     */
701
    public function getExtensions()
702
    {
703
        return $this->extensions;
704
    }
705

    
706
    /**
707
     * Registers a Token Parser.
708
     *
709
     * @param Twig_TokenParserInterface $parser A Twig_TokenParserInterface instance
710
     */
711
    public function addTokenParser(Twig_TokenParserInterface $parser)
712
    {
713
        if ($this->extensionInitialized) {
714
            throw new LogicException('Unable to add a token parser as extensions have already been initialized.');
715
        }
716

    
717
        $this->staging->addTokenParser($parser);
718
    }
719

    
720
    /**
721
     * Gets the registered Token Parsers.
722
     *
723
     * @return Twig_TokenParserBrokerInterface A broker containing token parsers
724
     */
725
    public function getTokenParsers()
726
    {
727
        if (!$this->extensionInitialized) {
728
            $this->initExtensions();
729
        }
730

    
731
        return $this->parsers;
732
    }
733

    
734
    /**
735
     * Gets registered tags.
736
     *
737
     * Be warned that this method cannot return tags defined by Twig_TokenParserBrokerInterface classes.
738
     *
739
     * @return Twig_TokenParserInterface[] An array of Twig_TokenParserInterface instances
740
     */
741
    public function getTags()
742
    {
743
        $tags = array();
744
        foreach ($this->getTokenParsers()->getParsers() as $parser) {
745
            if ($parser instanceof Twig_TokenParserInterface) {
746
                $tags[$parser->getTag()] = $parser;
747
            }
748
        }
749

    
750
        return $tags;
751
    }
752

    
753
    /**
754
     * Registers a Node Visitor.
755
     *
756
     * @param Twig_NodeVisitorInterface $visitor A Twig_NodeVisitorInterface instance
757
     */
758
    public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
759
    {
760
        if ($this->extensionInitialized) {
761
            throw new LogicException('Unable to add a node visitor as extensions have already been initialized.');
762
        }
763

    
764
        $this->staging->addNodeVisitor($visitor);
765
    }
766

    
767
    /**
768
     * Gets the registered Node Visitors.
769
     *
770
     * @return Twig_NodeVisitorInterface[] An array of Twig_NodeVisitorInterface instances
771
     */
772
    public function getNodeVisitors()
773
    {
774
        if (!$this->extensionInitialized) {
775
            $this->initExtensions();
776
        }
777

    
778
        return $this->visitors;
779
    }
780

    
781
    /**
782
     * Registers a Filter.
783
     *
784
     * @param string|Twig_SimpleFilter               $name   The filter name or a Twig_SimpleFilter instance
785
     * @param Twig_FilterInterface|Twig_SimpleFilter $filter A Twig_FilterInterface instance or a Twig_SimpleFilter instance
786
     */
787
    public function addFilter($name, $filter = null)
788
    {
789
        if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) {
790
            throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter');
791
        }
792

    
793
        if ($name instanceof Twig_SimpleFilter) {
794
            $filter = $name;
795
            $name = $filter->getName();
796
        }
797

    
798
        if ($this->extensionInitialized) {
799
            throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name));
800
        }
801

    
802
        $this->staging->addFilter($name, $filter);
803
    }
804

    
805
    /**
806
     * Get a filter by name.
807
     *
808
     * Subclasses may override this method and load filters differently;
809
     * so no list of filters is available.
810
     *
811
     * @param string $name The filter name
812
     *
813
     * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exist
814
     */
815
    public function getFilter($name)
816
    {
817
        if (!$this->extensionInitialized) {
818
            $this->initExtensions();
819
        }
820

    
821
        if (isset($this->filters[$name])) {
822
            return $this->filters[$name];
823
        }
824

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

    
828
            if ($count) {
829
                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
830
                    array_shift($matches);
831
                    $filter->setArguments($matches);
832

    
833
                    return $filter;
834
                }
835
            }
836
        }
837

    
838
        foreach ($this->filterCallbacks as $callback) {
839
            if (false !== $filter = call_user_func($callback, $name)) {
840
                return $filter;
841
            }
842
        }
843

    
844
        return false;
845
    }
846

    
847
    public function registerUndefinedFilterCallback($callable)
848
    {
849
        $this->filterCallbacks[] = $callable;
850
    }
851

    
852
    /**
853
     * Gets the registered Filters.
854
     *
855
     * Be warned that this method cannot return filters defined with registerUndefinedFunctionCallback.
856
     *
857
     * @return Twig_FilterInterface[] An array of Twig_FilterInterface instances
858
     *
859
     * @see registerUndefinedFilterCallback
860
     */
861
    public function getFilters()
862
    {
863
        if (!$this->extensionInitialized) {
864
            $this->initExtensions();
865
        }
866

    
867
        return $this->filters;
868
    }
869

    
870
    /**
871
     * Registers a Test.
872
     *
873
     * @param string|Twig_SimpleTest             $name The test name or a Twig_SimpleTest instance
874
     * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance
875
     */
876
    public function addTest($name, $test = null)
877
    {
878
        if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) {
879
            throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest');
880
        }
881

    
882
        if ($name instanceof Twig_SimpleTest) {
883
            $test = $name;
884
            $name = $test->getName();
885
        }
886

    
887
        if ($this->extensionInitialized) {
888
            throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name));
889
        }
890

    
891
        $this->staging->addTest($name, $test);
892
    }
893

    
894
    /**
895
     * Gets the registered Tests.
896
     *
897
     * @return Twig_TestInterface[] An array of Twig_TestInterface instances
898
     */
899
    public function getTests()
900
    {
901
        if (!$this->extensionInitialized) {
902
            $this->initExtensions();
903
        }
904

    
905
        return $this->tests;
906
    }
907

    
908
    /**
909
     * Gets a test by name.
910
     *
911
     * @param string $name The test name
912
     *
913
     * @return Twig_Test|false A Twig_Test instance or false if the test does not exist
914
     */
915
    public function getTest($name)
916
    {
917
        if (!$this->extensionInitialized) {
918
            $this->initExtensions();
919
        }
920

    
921
        if (isset($this->tests[$name])) {
922
            return $this->tests[$name];
923
        }
924

    
925
        return false;
926
    }
927

    
928
    /**
929
     * Registers a Function.
930
     *
931
     * @param string|Twig_SimpleFunction                 $name     The function name or a Twig_SimpleFunction instance
932
     * @param Twig_FunctionInterface|Twig_SimpleFunction $function A Twig_FunctionInterface instance or a Twig_SimpleFunction instance
933
     */
934
    public function addFunction($name, $function = null)
935
    {
936
        if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) {
937
            throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction');
938
        }
939

    
940
        if ($name instanceof Twig_SimpleFunction) {
941
            $function = $name;
942
            $name = $function->getName();
943
        }
944

    
945
        if ($this->extensionInitialized) {
946
            throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name));
947
        }
948

    
949
        $this->staging->addFunction($name, $function);
950
    }
951

    
952
    /**
953
     * Get a function by name.
954
     *
955
     * Subclasses may override this method and load functions differently;
956
     * so no list of functions is available.
957
     *
958
     * @param string $name function name
959
     *
960
     * @return Twig_Function|false A Twig_Function instance or false if the function does not exist
961
     */
962
    public function getFunction($name)
963
    {
964
        if (!$this->extensionInitialized) {
965
            $this->initExtensions();
966
        }
967

    
968
        if (isset($this->functions[$name])) {
969
            return $this->functions[$name];
970
        }
971

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

    
975
            if ($count) {
976
                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
977
                    array_shift($matches);
978
                    $function->setArguments($matches);
979

    
980
                    return $function;
981
                }
982
            }
983
        }
984

    
985
        foreach ($this->functionCallbacks as $callback) {
986
            if (false !== $function = call_user_func($callback, $name)) {
987
                return $function;
988
            }
989
        }
990

    
991
        return false;
992
    }
993

    
994
    public function registerUndefinedFunctionCallback($callable)
995
    {
996
        $this->functionCallbacks[] = $callable;
997
    }
998

    
999
    /**
1000
     * Gets registered functions.
1001
     *
1002
     * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback.
1003
     *
1004
     * @return Twig_FunctionInterface[] An array of Twig_FunctionInterface instances
1005
     *
1006
     * @see registerUndefinedFunctionCallback
1007
     */
1008
    public function getFunctions()
1009
    {
1010
        if (!$this->extensionInitialized) {
1011
            $this->initExtensions();
1012
        }
1013

    
1014
        return $this->functions;
1015
    }
1016

    
1017
    /**
1018
     * Registers a Global.
1019
     *
1020
     * New globals can be added before compiling or rendering a template;
1021
     * but after, you can only update existing globals.
1022
     *
1023
     * @param string $name  The global name
1024
     * @param mixed  $value The global value
1025
     */
1026
    public function addGlobal($name, $value)
1027
    {
1028
        if ($this->extensionInitialized || $this->runtimeInitialized) {
1029
            if (null === $this->globals) {
1030
                $this->globals = $this->initGlobals();
1031
            }
1032

    
1033
            /* This condition must be uncommented in Twig 2.0
1034
            if (!array_key_exists($name, $this->globals)) {
1035
                throw new LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
1036
            }
1037
            */
1038
        }
1039

    
1040
        if ($this->extensionInitialized || $this->runtimeInitialized) {
1041
            // update the value
1042
            $this->globals[$name] = $value;
1043
        } else {
1044
            $this->staging->addGlobal($name, $value);
1045
        }
1046
    }
1047

    
1048
    /**
1049
     * Gets the registered Globals.
1050
     *
1051
     * @return array An array of globals
1052
     */
1053
    public function getGlobals()
1054
    {
1055
        if (!$this->runtimeInitialized && !$this->extensionInitialized) {
1056
            return $this->initGlobals();
1057
        }
1058

    
1059
        if (null === $this->globals) {
1060
            $this->globals = $this->initGlobals();
1061
        }
1062

    
1063
        return $this->globals;
1064
    }
1065

    
1066
    /**
1067
     * Merges a context with the defined globals.
1068
     *
1069
     * @param array $context An array representing the context
1070
     *
1071
     * @return array The context merged with the globals
1072
     */
1073
    public function mergeGlobals(array $context)
1074
    {
1075
        // we don't use array_merge as the context being generally
1076
        // bigger than globals, this code is faster.
1077
        foreach ($this->getGlobals() as $key => $value) {
1078
            if (!array_key_exists($key, $context)) {
1079
                $context[$key] = $value;
1080
            }
1081
        }
1082

    
1083
        return $context;
1084
    }
1085

    
1086
    /**
1087
     * Gets the registered unary Operators.
1088
     *
1089
     * @return array An array of unary operators
1090
     */
1091
    public function getUnaryOperators()
1092
    {
1093
        if (!$this->extensionInitialized) {
1094
            $this->initExtensions();
1095
        }
1096

    
1097
        return $this->unaryOperators;
1098
    }
1099

    
1100
    /**
1101
     * Gets the registered binary Operators.
1102
     *
1103
     * @return array An array of binary operators
1104
     */
1105
    public function getBinaryOperators()
1106
    {
1107
        if (!$this->extensionInitialized) {
1108
            $this->initExtensions();
1109
        }
1110

    
1111
        return $this->binaryOperators;
1112
    }
1113

    
1114
    public function computeAlternatives($name, $items)
1115
    {
1116
        $alternatives = array();
1117
        foreach ($items as $item) {
1118
            $lev = levenshtein($name, $item);
1119
            if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
1120
                $alternatives[$item] = $lev;
1121
            }
1122
        }
1123
        asort($alternatives);
1124

    
1125
        return array_keys($alternatives);
1126
    }
1127

    
1128
    protected function initGlobals()
1129
    {
1130
        $globals = array();
1131
        foreach ($this->extensions as $extension) {
1132
            $extGlob = $extension->getGlobals();
1133
            if (!is_array($extGlob)) {
1134
                throw new UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', get_class($extension)));
1135
            }
1136

    
1137
            $globals[] = $extGlob;
1138
        }
1139

    
1140
        $globals[] = $this->staging->getGlobals();
1141

    
1142
        return call_user_func_array('array_merge', $globals);
1143
    }
1144

    
1145
    protected function initExtensions()
1146
    {
1147
        if ($this->extensionInitialized) {
1148
            return;
1149
        }
1150

    
1151
        $this->extensionInitialized = true;
1152
        $this->parsers = new Twig_TokenParserBroker();
1153
        $this->filters = array();
1154
        $this->functions = array();
1155
        $this->tests = array();
1156
        $this->visitors = array();
1157
        $this->unaryOperators = array();
1158
        $this->binaryOperators = array();
1159

    
1160
        foreach ($this->extensions as $extension) {
1161
            $this->initExtension($extension);
1162
        }
1163
        $this->initExtension($this->staging);
1164
    }
1165

    
1166
    protected function initExtension(Twig_ExtensionInterface $extension)
1167
    {
1168
        // filters
1169
        foreach ($extension->getFilters() as $name => $filter) {
1170
            if ($name instanceof Twig_SimpleFilter) {
1171
                $filter = $name;
1172
                $name = $filter->getName();
1173
            } elseif ($filter instanceof Twig_SimpleFilter) {
1174
                $name = $filter->getName();
1175
            }
1176

    
1177
            $this->filters[$name] = $filter;
1178
        }
1179

    
1180
        // functions
1181
        foreach ($extension->getFunctions() as $name => $function) {
1182
            if ($name instanceof Twig_SimpleFunction) {
1183
                $function = $name;
1184
                $name = $function->getName();
1185
            } elseif ($function instanceof Twig_SimpleFunction) {
1186
                $name = $function->getName();
1187
            }
1188

    
1189
            $this->functions[$name] = $function;
1190
        }
1191

    
1192
        // tests
1193
        foreach ($extension->getTests() as $name => $test) {
1194
            if ($name instanceof Twig_SimpleTest) {
1195
                $test = $name;
1196
                $name = $test->getName();
1197
            } elseif ($test instanceof Twig_SimpleTest) {
1198
                $name = $test->getName();
1199
            }
1200

    
1201
            $this->tests[$name] = $test;
1202
        }
1203

    
1204
        // token parsers
1205
        foreach ($extension->getTokenParsers() as $parser) {
1206
            if ($parser instanceof Twig_TokenParserInterface) {
1207
                $this->parsers->addTokenParser($parser);
1208
            } elseif ($parser instanceof Twig_TokenParserBrokerInterface) {
1209
                $this->parsers->addTokenParserBroker($parser);
1210
            } else {
1211
                throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances');
1212
            }
1213
        }
1214

    
1215
        // node visitors
1216
        foreach ($extension->getNodeVisitors() as $visitor) {
1217
            $this->visitors[] = $visitor;
1218
        }
1219

    
1220
        // operators
1221
        if ($operators = $extension->getOperators()) {
1222
            if (2 !== count($operators)) {
1223
                throw new InvalidArgumentException(sprintf('"%s::getOperators()" does not return a valid operators array.', get_class($extension)));
1224
            }
1225

    
1226
            $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);
1227
            $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]);
1228
        }
1229
    }
1230

    
1231
    protected function writeCacheFile($file, $content)
1232
    {
1233
        $dir = dirname($file);
1234
        if (!is_dir($dir)) {
1235
            if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
1236
                throw new RuntimeException(sprintf("Unable to create the cache directory (%s).", $dir));
1237
            }
1238
        } elseif (!is_writable($dir)) {
1239
            throw new RuntimeException(sprintf("Unable to write in the cache directory (%s).", $dir));
1240
        }
1241

    
1242
        $tmpFile = tempnam($dir, basename($file));
1243
        if (false !== @file_put_contents($tmpFile, $content)) {
1244
            // rename does not work on Win32 before 5.2.6
1245
            if (@rename($tmpFile, $file) || (@copy($tmpFile, $file) && unlink($tmpFile))) {
1246
                @chmod($file, 0666 & ~umask());
1247

    
1248
                return;
1249
            }
1250
        }
1251

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