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 */ 17class Twig_Environment 18{ 19 const VERSION = '1.29.0'; 20 const VERSION_ID = 12900; 21 const MAJOR_VERSION = 1; 22 const MINOR_VERSION = 29; 23 const RELEASE_VERSION = 0; 24 const EXTRA_VERSION = ''; 25 26 protected $charset; 27 protected $loader; 28 protected $debug; 29 protected $autoReload; 30 protected $cache; 31 protected $lexer; 32 protected $parser; 33 protected $compiler; 34 protected $baseTemplateClass; 35 protected $extensions; 36 protected $parsers; 37 protected $visitors; 38 protected $filters; 39 protected $tests; 40 protected $functions; 41 protected $globals; 42 protected $runtimeInitialized = false; 43 protected $extensionInitialized = false; 44 protected $loadedTemplates; 45 protected $strictVariables; 46 protected $unaryOperators; 47 protected $binaryOperators; 48 protected $templateClassPrefix = '__TwigTemplate_'; 49 protected $functionCallbacks = array(); 50 protected $filterCallbacks = array(); 51 protected $staging; 52 53 private $originalCache; 54 private $bcWriteCacheFile = false; 55 private $bcGetCacheFilename = false; 56 private $lastModifiedExtension = 0; 57 private $extensionsByClass = array(); 58 private $runtimeLoaders = array(); 59 private $runtimes = array(); 60 private $optionsHash; 61 62 /** 63 * Constructor. 64 * 65 * Available options: 66 * 67 * * debug: When set to true, it automatically set "auto_reload" to true as 68 * well (default to false). 69 * 70 * * charset: The charset used by the templates (default to UTF-8). 71 * 72 * * base_template_class: The base template class to use for generated 73 * templates (default to Twig_Template). 74 * 75 * * cache: An absolute path where to store the compiled templates, 76 * a Twig_Cache_Interface implementation, 77 * or false to disable compilation cache (default). 78 * 79 * * auto_reload: Whether to reload the template if the original source changed. 80 * If you don't provide the auto_reload option, it will be 81 * determined automatically based on the debug value. 82 * 83 * * strict_variables: Whether to ignore invalid variables in templates 84 * (default to false). 85 * 86 * * autoescape: Whether to enable auto-escaping (default to html): 87 * * false: disable auto-escaping 88 * * true: equivalent to html 89 * * html, js: set the autoescaping to one of the supported strategies 90 * * name: set the autoescaping strategy based on the template name extension 91 * * PHP callback: a PHP callback that returns an escaping strategy based on the template "name" 92 * 93 * * optimizations: A flag that indicates which optimizations to apply 94 * (default to -1 which means that all optimizations are enabled; 95 * set it to 0 to disable). 96 * 97 * @param Twig_LoaderInterface $loader 98 * @param array $options An array of options 99 */ 100 public function __construct(Twig_LoaderInterface $loader = null, $options = array()) 101 { 102 if (null !== $loader) { 103 $this->setLoader($loader); 104 } else { 105 @trigger_error('Not passing a Twig_LoaderInterface as the first constructor argument of Twig_Environment is deprecated since version 1.21.', E_USER_DEPRECATED); 106 } 107 108 $options = array_merge(array( 109 'debug' => false, 110 'charset' => 'UTF-8', 111 'base_template_class' => 'Twig_Template', 112 'strict_variables' => false, 113 'autoescape' => 'html', 114 'cache' => false, 115 'auto_reload' => null, 116 'optimizations' => -1, 117 ), $options); 118 119 $this->debug = (bool) $options['debug']; 120 $this->charset = strtoupper($options['charset']); 121 $this->baseTemplateClass = $options['base_template_class']; 122 $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; 123 $this->strictVariables = (bool) $options['strict_variables']; 124 $this->setCache($options['cache']); 125 126 $this->addExtension(new Twig_Extension_Core()); 127 $this->addExtension(new Twig_Extension_Escaper($options['autoescape'])); 128 $this->addExtension(new Twig_Extension_Optimizer($options['optimizations'])); 129 $this->staging = new Twig_Extension_Staging(); 130 131 // For BC 132 if (is_string($this->originalCache)) { 133 $r = new ReflectionMethod($this, 'writeCacheFile'); 134 if ($r->getDeclaringClass()->getName() !== __CLASS__) { 135 @trigger_error('The Twig_Environment::writeCacheFile method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED); 136 137 $this->bcWriteCacheFile = true; 138 } 139 140 $r = new ReflectionMethod($this, 'getCacheFilename'); 141 if ($r->getDeclaringClass()->getName() !== __CLASS__) { 142 @trigger_error('The Twig_Environment::getCacheFilename method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED); 143 144 $this->bcGetCacheFilename = true; 145 } 146 } 147 } 148 149 /** 150 * Gets the base template class for compiled templates. 151 * 152 * @return string The base template class name 153 */ 154 public function getBaseTemplateClass() 155 { 156 return $this->baseTemplateClass; 157 } 158 159 /** 160 * Sets the base template class for compiled templates. 161 * 162 * @param string $class The base template class name 163 */ 164 public function setBaseTemplateClass($class) 165 { 166 $this->baseTemplateClass = $class; 167 $this->updateOptionsHash(); 168 } 169 170 /** 171 * Enables debugging mode. 172 */ 173 public function enableDebug() 174 { 175 $this->debug = true; 176 $this->updateOptionsHash(); 177 } 178 179 /** 180 * Disables debugging mode. 181 */ 182 public function disableDebug() 183 { 184 $this->debug = false; 185 $this->updateOptionsHash(); 186 } 187 188 /** 189 * Checks if debug mode is enabled. 190 * 191 * @return bool true if debug mode is enabled, false otherwise 192 */ 193 public function isDebug() 194 { 195 return $this->debug; 196 } 197 198 /** 199 * Enables the auto_reload option. 200 */ 201 public function enableAutoReload() 202 { 203 $this->autoReload = true; 204 } 205 206 /** 207 * Disables the auto_reload option. 208 */ 209 public function disableAutoReload() 210 { 211 $this->autoReload = false; 212 } 213 214 /** 215 * Checks if the auto_reload option is enabled. 216 * 217 * @return bool true if auto_reload is enabled, false otherwise 218 */ 219 public function isAutoReload() 220 { 221 return $this->autoReload; 222 } 223 224 /** 225 * Enables the strict_variables option. 226 */ 227 public function enableStrictVariables() 228 { 229 $this->strictVariables = true; 230 $this->updateOptionsHash(); 231 } 232 233 /** 234 * Disables the strict_variables option. 235 */ 236 public function disableStrictVariables() 237 { 238 $this->strictVariables = false; 239 $this->updateOptionsHash(); 240 } 241 242 /** 243 * Checks if the strict_variables option is enabled. 244 * 245 * @return bool true if strict_variables is enabled, false otherwise 246 */ 247 public function isStrictVariables() 248 { 249 return $this->strictVariables; 250 } 251 252 /** 253 * Gets the current cache implementation. 254 * 255 * @param bool $original Whether to return the original cache option or the real cache instance 256 * 257 * @return Twig_CacheInterface|string|false A Twig_CacheInterface implementation, 258 * an absolute path to the compiled templates, 259 * or false to disable cache 260 */ 261 public function getCache($original = true) 262 { 263 return $original ? $this->originalCache : $this->cache; 264 } 265 266 /** 267 * Sets the current cache implementation. 268 * 269 * @param Twig_CacheInterface|string|false $cache A Twig_CacheInterface implementation, 270 * an absolute path to the compiled templates, 271 * or false to disable cache 272 */ 273 public function setCache($cache) 274 { 275 if (is_string($cache)) { 276 $this->originalCache = $cache; 277 $this->cache = new Twig_Cache_Filesystem($cache); 278 } elseif (false === $cache) { 279 $this->originalCache = $cache; 280 $this->cache = new Twig_Cache_Null(); 281 } elseif (null === $cache) { 282 @trigger_error('Using "null" as the cache strategy is deprecated since version 1.23 and will be removed in Twig 2.0.', E_USER_DEPRECATED); 283 $this->originalCache = false; 284 $this->cache = new Twig_Cache_Null(); 285 } elseif ($cache instanceof Twig_CacheInterface) { 286 $this->originalCache = $this->cache = $cache; 287 } else { 288 throw new LogicException(sprintf('Cache can only be a string, false, or a Twig_CacheInterface implementation.')); 289 } 290 } 291 292 /** 293 * Gets the cache filename for a given template. 294 * 295 * @param string $name The template name 296 * 297 * @return string|false The cache file name or false when caching is disabled 298 * 299 * @deprecated since 1.22 (to be removed in 2.0) 300 */ 301 public function getCacheFilename($name) 302 { 303 @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); 304 305 $key = $this->cache->generateKey($name, $this->getTemplateClass($name)); 306 307 return !$key ? false : $key; 308 } 309 310 /** 311 * Gets the template class associated with the given string. 312 * 313 * The generated template class is based on the following parameters: 314 * 315 * * The cache key for the given template; 316 * * The currently enabled extensions; 317 * * Whether the Twig C extension is available or not; 318 * * PHP version; 319 * * Twig version; 320 * * Options with what environment was created. 321 * 322 * @param string $name The name for which to calculate the template class name 323 * @param int|null $index The index if it is an embedded template 324 * 325 * @return string The template class name 326 */ 327 public function getTemplateClass($name, $index = null) 328 { 329 $key = $this->getLoader()->getCacheKey($name).$this->optionsHash; 330 331 return $this->templateClassPrefix.hash('sha256', $key).(null === $index ? '' : '_'.$index); 332 } 333 334 /** 335 * Gets the template class prefix. 336 * 337 * @return string The template class prefix 338 * 339 * @deprecated since 1.22 (to be removed in 2.0) 340 */ 341 public function getTemplateClassPrefix() 342 { 343 @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); 344 345 return $this->templateClassPrefix; 346 } 347 348 /** 349 * Renders a template. 350 * 351 * @param string $name The template name 352 * @param array $context An array of parameters to pass to the template 353 * 354 * @return string The rendered template 355 * 356 * @throws Twig_Error_Loader When the template cannot be found 357 * @throws Twig_Error_Syntax When an error occurred during compilation 358 * @throws Twig_Error_Runtime When an error occurred during rendering 359 */ 360 public function render($name, array $context = array()) 361 { 362 return $this->loadTemplate($name)->render($context); 363 } 364 365 /** 366 * Displays a template. 367 * 368 * @param string $name The template name 369 * @param array $context An array of parameters to pass to the template 370 * 371 * @throws Twig_Error_Loader When the template cannot be found 372 * @throws Twig_Error_Syntax When an error occurred during compilation 373 * @throws Twig_Error_Runtime When an error occurred during rendering 374 */ 375 public function display($name, array $context = array()) 376 { 377 $this->loadTemplate($name)->display($context); 378 } 379 380 /** 381 * Loads a template. 382 * 383 * @param string|Twig_TemplateWrapper|Twig_Template $name The template name 384 * 385 * @return Twig_TemplateWrapper 386 */ 387 public function load($name) 388 { 389 if ($name instanceof Twig_TemplateWrapper) { 390 return $name; 391 } 392 393 if ($name instanceof Twig_Template) { 394 return new Twig_TemplateWrapper($this, $name); 395 } 396 397 return new Twig_TemplateWrapper($this, $this->loadTemplate($name)); 398 } 399 400 /** 401 * Loads a template internal representation. 402 * 403 * This method is for internal use only and should never be called 404 * directly. 405 * 406 * @param string $name The template name 407 * @param int $index The index if it is an embedded template 408 * 409 * @return Twig_TemplateInterface A template instance representing the given template name 410 * 411 * @throws Twig_Error_Loader When the template cannot be found 412 * @throws Twig_Error_Syntax When an error occurred during compilation 413 * 414 * @internal 415 */ 416 public function loadTemplate($name, $index = null) 417 { 418 $cls = $this->getTemplateClass($name, $index); 419 420 if (isset($this->loadedTemplates[$cls])) { 421 return $this->loadedTemplates[$cls]; 422 } 423 424 if (!class_exists($cls, false)) { 425 if ($this->bcGetCacheFilename) { 426 $key = $this->getCacheFilename($name); 427 } else { 428 $key = $this->cache->generateKey($name, $cls); 429 } 430 431 if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) { 432 $this->cache->load($key); 433 } 434 435 if (!class_exists($cls, false)) { 436 $loader = $this->getLoader(); 437 if (!$loader instanceof Twig_SourceContextLoaderInterface) { 438 $source = new Twig_Source($loader->getSource($name), $name); 439 } else { 440 $source = $loader->getSourceContext($name); 441 } 442 443 $content = $this->compileSource($source); 444 445 if ($this->bcWriteCacheFile) { 446 $this->writeCacheFile($key, $content); 447 } else { 448 $this->cache->write($key, $content); 449 $this->cache->load($key); 450 } 451 452 if (!class_exists($cls, false)) { 453 /* Last line of defense if either $this->bcWriteCacheFile was used, 454 * $this->cache is implemented as a no-op or we have a race condition 455 * where the cache was cleared between the above calls to write to and load from 456 * the cache. 457 */ 458 eval('?>'.$content); 459 } 460 } 461 } 462 463 if (!$this->runtimeInitialized) { 464 $this->initRuntime(); 465 } 466 467 return $this->loadedTemplates[$cls] = new $cls($this); 468 } 469 470 /** 471 * Creates a template from source. 472 * 473 * This method should not be used as a generic way to load templates. 474 * 475 * @param string $template The template name 476 * 477 * @return Twig_Template A template instance representing the given template name 478 * 479 * @throws Twig_Error_Loader When the template cannot be found 480 * @throws Twig_Error_Syntax When an error occurred during compilation 481 */ 482 public function createTemplate($template) 483 { 484 $name = sprintf('__string_template__%s', hash('sha256', uniqid(mt_rand(), true), false)); 485 486 $loader = new Twig_Loader_Chain(array( 487 new Twig_Loader_Array(array($name => $template)), 488 $current = $this->getLoader(), 489 )); 490 491 $this->setLoader($loader); 492 try { 493 $template = $this->loadTemplate($name); 494 } catch (Exception $e) { 495 $this->setLoader($current); 496 497 throw $e; 498 } catch (Throwable $e) { 499 $this->setLoader($current); 500 501 throw $e; 502 } 503 $this->setLoader($current); 504 505 return $template; 506 } 507 508 /** 509 * Returns true if the template is still fresh. 510 * 511 * Besides checking the loader for freshness information, 512 * this method also checks if the enabled extensions have 513 * not changed. 514 * 515 * @param string $name The template name 516 * @param int $time The last modification time of the cached template 517 * 518 * @return bool true if the template is fresh, false otherwise 519 */ 520 public function isTemplateFresh($name, $time) 521 { 522 if (0 === $this->lastModifiedExtension) { 523 foreach ($this->extensions as $extension) { 524 $r = new ReflectionObject($extension); 525 if (file_exists($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModifiedExtension) { 526 $this->lastModifiedExtension = $extensionTime; 527 } 528 } 529 } 530 531 return $this->lastModifiedExtension <= $time && $this->getLoader()->isFresh($name, $time); 532 } 533 534 /** 535 * Tries to load a template consecutively from an array. 536 * 537 * Similar to loadTemplate() but it also accepts Twig_TemplateInterface instances and an array 538 * of templates where each is tried to be loaded. 539 * 540 * @param string|Twig_Template|array $names A template or an array of templates to try consecutively 541 * 542 * @return Twig_Template 543 * 544 * @throws Twig_Error_Loader When none of the templates can be found 545 * @throws Twig_Error_Syntax When an error occurred during compilation 546 */ 547 public function resolveTemplate($names) 548 { 549 if (!is_array($names)) { 550 $names = array($names); 551 } 552 553 foreach ($names as $name) { 554 if ($name instanceof Twig_Template) { 555 return $name; 556 } 557 558 try { 559 return $this->loadTemplate($name); 560 } catch (Twig_Error_Loader $e) { 561 } 562 } 563 564 if (1 === count($names)) { 565 throw $e; 566 } 567 568 throw new Twig_Error_Loader(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); 569 } 570 571 /** 572 * Clears the internal template cache. 573 * 574 * @deprecated since 1.18.3 (to be removed in 2.0) 575 */ 576 public function clearTemplateCache() 577 { 578 @trigger_error(sprintf('The %s method is deprecated since version 1.18.3 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); 579 580 $this->loadedTemplates = array(); 581 } 582 583 /** 584 * Clears the template cache files on the filesystem. 585 * 586 * @deprecated since 1.22 (to be removed in 2.0) 587 */ 588 public function clearCacheFiles() 589 { 590 @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); 591 592 if (is_string($this->originalCache)) { 593 foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->originalCache), RecursiveIteratorIterator::LEAVES_ONLY) as $file) { 594 if ($file->isFile()) { 595 @unlink($file->getPathname()); 596 } 597 } 598 } 599 } 600 601 /** 602 * Gets the Lexer instance. 603 * 604 * @return Twig_LexerInterface 605 * 606 * @deprecated since 1.25 (to be removed in 2.0) 607 */ 608 public function getLexer() 609 { 610 @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED); 611 612 if (null === $this->lexer) { 613 $this->lexer = new Twig_Lexer($this); 614 } 615 616 return $this->lexer; 617 } 618 619 public function setLexer(Twig_LexerInterface $lexer) 620 { 621 $this->lexer = $lexer; 622 } 623 624 /** 625 * Tokenizes a source code. 626 * 627 * @param string|Twig_Source $source The template source code 628 * @param string $name The template name (deprecated) 629 * 630 * @return Twig_TokenStream 631 * 632 * @throws Twig_Error_Syntax When the code is syntactically wrong 633 */ 634 public function tokenize($source, $name = null) 635 { 636 if (!$source instanceof Twig_Source) { 637 @trigger_error(sprintf('Passing a string as the $source argument of %s() is deprecated since version 1.27. Pass a Twig_Source instance instead.', __METHOD__), E_USER_DEPRECATED); 638 $source = new Twig_Source($source, $name); 639 } 640 641 if (null === $this->lexer) { 642 $this->lexer = new Twig_Lexer($this); 643 } 644 645 return $this->lexer->tokenize($source); 646 } 647 648 /** 649 * Gets the Parser instance. 650 * 651 * @return Twig_ParserInterface 652 * 653 * @deprecated since 1.25 (to be removed in 2.0) 654 */ 655 public function getParser() 656 { 657 @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED); 658 659 if (null === $this->parser) { 660 $this->parser = new Twig_Parser($this); 661 } 662 663 return $this->parser; 664 } 665 666 public function setParser(Twig_ParserInterface $parser) 667 { 668 $this->parser = $parser; 669 } 670 671 /** 672 * Converts a token stream to a node tree. 673 * 674 * @return Twig_Node_Module 675 * 676 * @throws Twig_Error_Syntax When the token stream is syntactically or semantically wrong 677 */ 678 public function parse(Twig_TokenStream $stream) 679 { 680 if (null === $this->parser) { 681 $this->parser = new Twig_Parser($this); 682 } 683 684 return $this->parser->parse($stream); 685 } 686 687 /** 688 * Gets the Compiler instance. 689 * 690 * @return Twig_CompilerInterface 691 * 692 * @deprecated since 1.25 (to be removed in 2.0) 693 */ 694 public function getCompiler() 695 { 696 @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED); 697 698 if (null === $this->compiler) { 699 $this->compiler = new Twig_Compiler($this); 700 } 701 702 return $this->compiler; 703 } 704 705 public function setCompiler(Twig_CompilerInterface $compiler) 706 { 707 $this->compiler = $compiler; 708 } 709 710 /** 711 * Compiles a node and returns the PHP code. 712 * 713 * @return string The compiled PHP source code 714 */ 715 public function compile(Twig_NodeInterface $node) 716 { 717 if (null === $this->compiler) { 718 $this->compiler = new Twig_Compiler($this); 719 } 720 721 return $this->compiler->compile($node)->getSource(); 722 } 723 724 /** 725 * Compiles a template source code. 726 * 727 * @param string|Twig_Source $source The template source code 728 * @param string $name The template name (deprecated) 729 * 730 * @return string The compiled PHP source code 731 * 732 * @throws Twig_Error_Syntax When there was an error during tokenizing, parsing or compiling 733 */ 734 public function compileSource($source, $name = null) 735 { 736 if (!$source instanceof Twig_Source) { 737 @trigger_error(sprintf('Passing a string as the $source argument of %s() is deprecated since version 1.27. Pass a Twig_Source instance instead.', __METHOD__), E_USER_DEPRECATED); 738 $source = new Twig_Source($source, $name); 739 } 740 741 try { 742 return $this->compile($this->parse($this->tokenize($source))); 743 } catch (Twig_Error $e) { 744 $e->setSourceContext($source); 745 throw $e; 746 } catch (Exception $e) { 747 throw new Twig_Error_Syntax(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e); 748 } 749 } 750 751 public function setLoader(Twig_LoaderInterface $loader) 752 { 753 if (!$loader instanceof Twig_SourceContextLoaderInterface && 0 !== strpos(get_class($loader), 'Mock_Twig_LoaderInterface')) { 754 @trigger_error(sprintf('Twig loader "%s" should implement Twig_SourceContextLoaderInterface since version 1.27.', get_class($loader)), E_USER_DEPRECATED); 755 } 756 757 $this->loader = $loader; 758 } 759 760 /** 761 * Gets the Loader instance. 762 * 763 * @return Twig_LoaderInterface 764 */ 765 public function getLoader() 766 { 767 if (null === $this->loader) { 768 throw new LogicException('You must set a loader first.'); 769 } 770 771 return $this->loader; 772 } 773 774 /** 775 * Sets the default template charset. 776 * 777 * @param string $charset The default charset 778 */ 779 public function setCharset($charset) 780 { 781 $this->charset = strtoupper($charset); 782 } 783 784 /** 785 * Gets the default template charset. 786 * 787 * @return string The default charset 788 */ 789 public function getCharset() 790 { 791 return $this->charset; 792 } 793 794 /** 795 * Initializes the runtime environment. 796 * 797 * @deprecated since 1.23 (to be removed in 2.0) 798 */ 799 public function initRuntime() 800 { 801 $this->runtimeInitialized = true; 802 803 foreach ($this->getExtensions() as $name => $extension) { 804 if (!$extension instanceof Twig_Extension_InitRuntimeInterface) { 805 $m = new ReflectionMethod($extension, 'initRuntime'); 806 807 if ('Twig_Extension' !== $m->getDeclaringClass()->getName()) { 808 @trigger_error(sprintf('Defining the initRuntime() method in the "%s" extension is deprecated since version 1.23. Use the `needs_environment` option to get the Twig_Environment instance in filters, functions, or tests; or explicitly implement Twig_Extension_InitRuntimeInterface if needed (not recommended).', $name), E_USER_DEPRECATED); 809 } 810 } 811 812 $extension->initRuntime($this); 813 } 814 } 815 816 /** 817 * Returns true if the given extension is registered. 818 * 819 * @param string $class The extension class name 820 * 821 * @return bool Whether the extension is registered or not 822 */ 823 public function hasExtension($class) 824 { 825 $class = ltrim($class, '\\'); 826 if (isset($this->extensions[$class])) { 827 if ($class !== get_class($this->extensions[$class])) { 828 @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED); 829 } 830 831 return true; 832 } 833 834 return isset($this->extensionsByClass[ltrim($class, '\\')]); 835 } 836 837 /** 838 * Adds a runtime loader. 839 */ 840 public function addRuntimeLoader(Twig_RuntimeLoaderInterface $loader) 841 { 842 $this->runtimeLoaders[] = $loader; 843 } 844 845 /** 846 * Gets an extension by class name. 847 * 848 * @param string $class The extension class name 849 * 850 * @return Twig_ExtensionInterface 851 */ 852 public function getExtension($class) 853 { 854 $class = ltrim($class, '\\'); 855 856 if (isset($this->extensions[$class])) { 857 if ($class !== get_class($this->extensions[$class])) { 858 @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED); 859 } 860 861 return $this->extensions[$class]; 862 } 863 864 if (!isset($this->extensionsByClass[$class])) { 865 throw new Twig_Error_Runtime(sprintf('The "%s" extension is not enabled.', $class)); 866 } 867 868 return $this->extensionsByClass[$class]; 869 } 870 871 /** 872 * Returns the runtime implementation of a Twig element (filter/function/test). 873 * 874 * @param string $class A runtime class name 875 * 876 * @return object The runtime implementation 877 * 878 * @throws Twig_Error_Runtime When the template cannot be found 879 */ 880 public function getRuntime($class) 881 { 882 if (isset($this->runtimes[$class])) { 883 return $this->runtimes[$class]; 884 } 885 886 foreach ($this->runtimeLoaders as $loader) { 887 if (null !== $runtime = $loader->load($class)) { 888 return $this->runtimes[$class] = $runtime; 889 } 890 } 891 892 throw new Twig_Error_Runtime(sprintf('Unable to load the "%s" runtime.', $class)); 893 } 894 895 public function addExtension(Twig_ExtensionInterface $extension) 896 { 897 if ($this->extensionInitialized) { 898 throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName())); 899 } 900 901 $class = get_class($extension); 902 if ($class !== $extension->getName()) { 903 if (isset($this->extensions[$extension->getName()])) { 904 unset($this->extensions[$extension->getName()], $this->extensionsByClass[$class]); 905 @trigger_error(sprintf('The possibility to register the same extension twice ("%s") is deprecated since version 1.23 and will be removed in Twig 2.0. Use proper PHP inheritance instead.', $extension->getName()), E_USER_DEPRECATED); 906 } 907 } 908 909 $this->lastModifiedExtension = 0; 910 $this->extensionsByClass[$class] = $extension; 911 $this->extensions[$extension->getName()] = $extension; 912 $this->updateOptionsHash(); 913 } 914 915 /** 916 * Removes an extension by name. 917 * 918 * This method is deprecated and you should not use it. 919 * 920 * @param string $name The extension name 921 * 922 * @deprecated since 1.12 (to be removed in 2.0) 923 */ 924 public function removeExtension($name) 925 { 926 @trigger_error(sprintf('The %s method is deprecated since version 1.12 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); 927 928 if ($this->extensionInitialized) { 929 throw new LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name)); 930 } 931 932 $class = ltrim($name, '\\'); 933 if (isset($this->extensions[$class])) { 934 if ($class !== get_class($this->extensions[$class])) { 935 @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED); 936 } 937 938 unset($this->extensions[$class]); 939 } 940 941 unset($this->extensions[$class]); 942 $this->updateOptionsHash(); 943 } 944 945 /** 946 * Registers an array of extensions. 947 * 948 * @param array $extensions An array of extensions 949 */ 950 public function setExtensions(array $extensions) 951 { 952 foreach ($extensions as $extension) { 953 $this->addExtension($extension); 954 } 955 } 956 957 /** 958 * Returns all registered extensions. 959 * 960 * @return Twig_ExtensionInterface[] An array of extensions (keys are for internal usage only and should not be relied on) 961 */ 962 public function getExtensions() 963 { 964 return $this->extensions; 965 } 966 967 public function addTokenParser(Twig_TokenParserInterface $parser) 968 { 969 if ($this->extensionInitialized) { 970 throw new LogicException('Unable to add a token parser as extensions have already been initialized.'); 971 } 972 973 $this->staging->addTokenParser($parser); 974 } 975 976 /** 977 * Gets the registered Token Parsers. 978 * 979 * @return Twig_TokenParserBrokerInterface 980 * 981 * @internal 982 */ 983 public function getTokenParsers() 984 { 985 if (!$this->extensionInitialized) { 986 $this->initExtensions(); 987 } 988 989 return $this->parsers; 990 } 991 992 /** 993 * Gets registered tags. 994 * 995 * Be warned that this method cannot return tags defined by Twig_TokenParserBrokerInterface classes. 996 * 997 * @return Twig_TokenParserInterface[] 998 * 999 * @internal 1000 */ 1001 public function getTags() 1002 { 1003 $tags = array(); 1004 foreach ($this->getTokenParsers()->getParsers() as $parser) { 1005 if ($parser instanceof Twig_TokenParserInterface) { 1006 $tags[$parser->getTag()] = $parser; 1007 } 1008 } 1009 1010 return $tags; 1011 } 1012 1013 public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) 1014 { 1015 if ($this->extensionInitialized) { 1016 throw new LogicException('Unable to add a node visitor as extensions have already been initialized.'); 1017 } 1018 1019 $this->staging->addNodeVisitor($visitor); 1020 } 1021 1022 /** 1023 * Gets the registered Node Visitors. 1024 * 1025 * @return Twig_NodeVisitorInterface[] 1026 * 1027 * @internal 1028 */ 1029 public function getNodeVisitors() 1030 { 1031 if (!$this->extensionInitialized) { 1032 $this->initExtensions(); 1033 } 1034 1035 return $this->visitors; 1036 } 1037 1038 /** 1039 * Registers a Filter. 1040 * 1041 * @param string|Twig_SimpleFilter $name The filter name or a Twig_SimpleFilter instance 1042 * @param Twig_FilterInterface|Twig_SimpleFilter $filter 1043 */ 1044 public function addFilter($name, $filter = null) 1045 { 1046 if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) { 1047 throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter.'); 1048 } 1049 1050 if ($name instanceof Twig_SimpleFilter) { 1051 $filter = $name; 1052 $name = $filter->getName(); 1053 } else { 1054 @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleFilter" instead when defining filter "%s".', __METHOD__, $name), E_USER_DEPRECATED); 1055 } 1056 1057 if ($this->extensionInitialized) { 1058 throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name)); 1059 } 1060 1061 $this->staging->addFilter($name, $filter); 1062 } 1063 1064 /** 1065 * Get a filter by name. 1066 * 1067 * Subclasses may override this method and load filters differently; 1068 * so no list of filters is available. 1069 * 1070 * @param string $name The filter name 1071 * 1072 * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exist 1073 * 1074 * @internal 1075 */ 1076 public function getFilter($name) 1077 { 1078 if (!$this->extensionInitialized) { 1079 $this->initExtensions(); 1080 } 1081 1082 if (isset($this->filters[$name])) { 1083 return $this->filters[$name]; 1084 } 1085 1086 foreach ($this->filters as $pattern => $filter) { 1087 $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); 1088 1089 if ($count) { 1090 if (preg_match('#^'.$pattern.'$#', $name, $matches)) { 1091 array_shift($matches); 1092 $filter->setArguments($matches); 1093 1094 return $filter; 1095 } 1096 } 1097 } 1098 1099 foreach ($this->filterCallbacks as $callback) { 1100 if (false !== $filter = call_user_func($callback, $name)) { 1101 return $filter; 1102 } 1103 } 1104 1105 return false; 1106 } 1107 1108 public function registerUndefinedFilterCallback($callable) 1109 { 1110 $this->filterCallbacks[] = $callable; 1111 } 1112 1113 /** 1114 * Gets the registered Filters. 1115 * 1116 * Be warned that this method cannot return filters defined with registerUndefinedFilterCallback. 1117 * 1118 * @return Twig_FilterInterface[] 1119 * 1120 * @see registerUndefinedFilterCallback 1121 * 1122 * @internal 1123 */ 1124 public function getFilters() 1125 { 1126 if (!$this->extensionInitialized) { 1127 $this->initExtensions(); 1128 } 1129 1130 return $this->filters; 1131 } 1132 1133 /** 1134 * Registers a Test. 1135 * 1136 * @param string|Twig_SimpleTest $name The test name or a Twig_SimpleTest instance 1137 * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance 1138 */ 1139 public function addTest($name, $test = null) 1140 { 1141 if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) { 1142 throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest.'); 1143 } 1144 1145 if ($name instanceof Twig_SimpleTest) { 1146 $test = $name; 1147 $name = $test->getName(); 1148 } else { 1149 @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleTest" instead when defining test "%s".', __METHOD__, $name), E_USER_DEPRECATED); 1150 } 1151 1152 if ($this->extensionInitialized) { 1153 throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name)); 1154 } 1155 1156 $this->staging->addTest($name, $test); 1157 } 1158 1159 /** 1160 * Gets the registered Tests. 1161 * 1162 * @return Twig_TestInterface[] 1163 * 1164 * @internal 1165 */ 1166 public function getTests() 1167 { 1168 if (!$this->extensionInitialized) { 1169 $this->initExtensions(); 1170 } 1171 1172 return $this->tests; 1173 } 1174 1175 /** 1176 * Gets a test by name. 1177 * 1178 * @param string $name The test name 1179 * 1180 * @return Twig_Test|false A Twig_Test instance or false if the test does not exist 1181 * 1182 * @internal 1183 */ 1184 public function getTest($name) 1185 { 1186 if (!$this->extensionInitialized) { 1187 $this->initExtensions(); 1188 } 1189 1190 if (isset($this->tests[$name])) { 1191 return $this->tests[$name]; 1192 } 1193 1194 return false; 1195 } 1196 1197 /** 1198 * Registers a Function. 1199 * 1200 * @param string|Twig_SimpleFunction $name The function name or a Twig_SimpleFunction instance 1201 * @param Twig_FunctionInterface|Twig_SimpleFunction $function 1202 */ 1203 public function addFunction($name, $function = null) 1204 { 1205 if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) { 1206 throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction.'); 1207 } 1208 1209 if ($name instanceof Twig_SimpleFunction) { 1210 $function = $name; 1211 $name = $function->getName(); 1212 } else { 1213 @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleFunction" instead when defining function "%s".', __METHOD__, $name), E_USER_DEPRECATED); 1214 } 1215 1216 if ($this->extensionInitialized) { 1217 throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name)); 1218 } 1219 1220 $this->staging->addFunction($name, $function); 1221 } 1222 1223 /** 1224 * Get a function by name. 1225 * 1226 * Subclasses may override this method and load functions differently; 1227 * so no list of functions is available. 1228 * 1229 * @param string $name function name 1230 * 1231 * @return Twig_Function|false A Twig_Function instance or false if the function does not exist 1232 * 1233 * @internal 1234 */ 1235 public function getFunction($name) 1236 { 1237 if (!$this->extensionInitialized) { 1238 $this->initExtensions(); 1239 } 1240 1241 if (isset($this->functions[$name])) { 1242 return $this->functions[$name]; 1243 } 1244 1245 foreach ($this->functions as $pattern => $function) { 1246 $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); 1247 1248 if ($count) { 1249 if (preg_match('#^'.$pattern.'$#', $name, $matches)) { 1250 array_shift($matches); 1251 $function->setArguments($matches); 1252 1253 return $function; 1254 } 1255 } 1256 } 1257 1258 foreach ($this->functionCallbacks as $callback) { 1259 if (false !== $function = call_user_func($callback, $name)) { 1260 return $function; 1261 } 1262 } 1263 1264 return false; 1265 } 1266 1267 public function registerUndefinedFunctionCallback($callable) 1268 { 1269 $this->functionCallbacks[] = $callable; 1270 } 1271 1272 /** 1273 * Gets registered functions. 1274 * 1275 * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback. 1276 * 1277 * @return Twig_FunctionInterface[] 1278 * 1279 * @see registerUndefinedFunctionCallback 1280 * 1281 * @internal 1282 */ 1283 public function getFunctions() 1284 { 1285 if (!$this->extensionInitialized) { 1286 $this->initExtensions(); 1287 } 1288 1289 return $this->functions; 1290 } 1291 1292 /** 1293 * Registers a Global. 1294 * 1295 * New globals can be added before compiling or rendering a template; 1296 * but after, you can only update existing globals. 1297 * 1298 * @param string $name The global name 1299 * @param mixed $value The global value 1300 */ 1301 public function addGlobal($name, $value) 1302 { 1303 if ($this->extensionInitialized || $this->runtimeInitialized) { 1304 if (null === $this->globals) { 1305 $this->globals = $this->initGlobals(); 1306 } 1307 1308 if (!array_key_exists($name, $this->globals)) { 1309 // The deprecation notice must be turned into the following exception in Twig 2.0 1310 @trigger_error(sprintf('Registering global variable "%s" at runtime or when the extensions have already been initialized is deprecated since version 1.21.', $name), E_USER_DEPRECATED); 1311 //throw new LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); 1312 } 1313 } 1314 1315 if ($this->extensionInitialized || $this->runtimeInitialized) { 1316 // update the value 1317 $this->globals[$name] = $value; 1318 } else { 1319 $this->staging->addGlobal($name, $value); 1320 } 1321 } 1322 1323 /** 1324 * Gets the registered Globals. 1325 * 1326 * @return array An array of globals 1327 * 1328 * @internal 1329 */ 1330 public function getGlobals() 1331 { 1332 if (!$this->runtimeInitialized && !$this->extensionInitialized) { 1333 return $this->initGlobals(); 1334 } 1335 1336 if (null === $this->globals) { 1337 $this->globals = $this->initGlobals(); 1338 } 1339 1340 return $this->globals; 1341 } 1342 1343 /** 1344 * Merges a context with the defined globals. 1345 * 1346 * @param array $context An array representing the context 1347 * 1348 * @return array The context merged with the globals 1349 */ 1350 public function mergeGlobals(array $context) 1351 { 1352 // we don't use array_merge as the context being generally 1353 // bigger than globals, this code is faster. 1354 foreach ($this->getGlobals() as $key => $value) { 1355 if (!array_key_exists($key, $context)) { 1356 $context[$key] = $value; 1357 } 1358 } 1359 1360 return $context; 1361 } 1362 1363 /** 1364 * Gets the registered unary Operators. 1365 * 1366 * @return array An array of unary operators 1367 * 1368 * @internal 1369 */ 1370 public function getUnaryOperators() 1371 { 1372 if (!$this->extensionInitialized) { 1373 $this->initExtensions(); 1374 } 1375 1376 return $this->unaryOperators; 1377 } 1378 1379 /** 1380 * Gets the registered binary Operators. 1381 * 1382 * @return array An array of binary operators 1383 * 1384 * @internal 1385 */ 1386 public function getBinaryOperators() 1387 { 1388 if (!$this->extensionInitialized) { 1389 $this->initExtensions(); 1390 } 1391 1392 return $this->binaryOperators; 1393 } 1394 1395 /** 1396 * @deprecated since 1.23 (to be removed in 2.0) 1397 */ 1398 public function computeAlternatives($name, $items) 1399 { 1400 @trigger_error(sprintf('The %s method is deprecated since version 1.23 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); 1401 1402 return Twig_Error_Syntax::computeAlternatives($name, $items); 1403 } 1404 1405 /** 1406 * @internal 1407 */ 1408 protected function initGlobals() 1409 { 1410 $globals = array(); 1411 foreach ($this->extensions as $name => $extension) { 1412 if (!$extension instanceof Twig_Extension_GlobalsInterface) { 1413 $m = new ReflectionMethod($extension, 'getGlobals'); 1414 1415 if ('Twig_Extension' !== $m->getDeclaringClass()->getName()) { 1416 @trigger_error(sprintf('Defining the getGlobals() method in the "%s" extension without explicitly implementing Twig_Extension_GlobalsInterface is deprecated since version 1.23.', $name), E_USER_DEPRECATED); 1417 } 1418 } 1419 1420 $extGlob = $extension->getGlobals(); 1421 if (!is_array($extGlob)) { 1422 throw new UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', get_class($extension))); 1423 } 1424 1425 $globals[] = $extGlob; 1426 } 1427 1428 $globals[] = $this->staging->getGlobals(); 1429 1430 return call_user_func_array('array_merge', $globals); 1431 } 1432 1433 /** 1434 * @internal 1435 */ 1436 protected function initExtensions() 1437 { 1438 if ($this->extensionInitialized) { 1439 return; 1440 } 1441 1442 $this->extensionInitialized = true; 1443 $this->parsers = new Twig_TokenParserBroker(array(), array(), false); 1444 $this->filters = array(); 1445 $this->functions = array(); 1446 $this->tests = array(); 1447 $this->visitors = array(); 1448 $this->unaryOperators = array(); 1449 $this->binaryOperators = array(); 1450 1451 foreach ($this->extensions as $extension) { 1452 $this->initExtension($extension); 1453 } 1454 $this->initExtension($this->staging); 1455 } 1456 1457 /** 1458 * @internal 1459 */ 1460 protected function initExtension(Twig_ExtensionInterface $extension) 1461 { 1462 // filters 1463 foreach ($extension->getFilters() as $name => $filter) { 1464 if ($filter instanceof Twig_SimpleFilter) { 1465 $name = $filter->getName(); 1466 } else { 1467 @trigger_error(sprintf('Using an instance of "%s" for filter "%s" is deprecated since version 1.21. Use Twig_SimpleFilter instead.', get_class($filter), $name), E_USER_DEPRECATED); 1468 } 1469 1470 $this->filters[$name] = $filter; 1471 } 1472 1473 // functions 1474 foreach ($extension->getFunctions() as $name => $function) { 1475 if ($function instanceof Twig_SimpleFunction) { 1476 $name = $function->getName(); 1477 } else { 1478 @trigger_error(sprintf('Using an instance of "%s" for function "%s" is deprecated since version 1.21. Use Twig_SimpleFunction instead.', get_class($function), $name), E_USER_DEPRECATED); 1479 } 1480 1481 $this->functions[$name] = $function; 1482 } 1483 1484 // tests 1485 foreach ($extension->getTests() as $name => $test) { 1486 if ($test instanceof Twig_SimpleTest) { 1487 $name = $test->getName(); 1488 } else { 1489 @trigger_error(sprintf('Using an instance of "%s" for test "%s" is deprecated since version 1.21. Use Twig_SimpleTest instead.', get_class($test), $name), E_USER_DEPRECATED); 1490 } 1491 1492 $this->tests[$name] = $test; 1493 } 1494 1495 // token parsers 1496 foreach ($extension->getTokenParsers() as $parser) { 1497 if ($parser instanceof Twig_TokenParserInterface) { 1498 $this->parsers->addTokenParser($parser); 1499 } elseif ($parser instanceof Twig_TokenParserBrokerInterface) { 1500 @trigger_error('Registering a Twig_TokenParserBrokerInterface instance is deprecated since version 1.21.', E_USER_DEPRECATED); 1501 1502 $this->parsers->addTokenParserBroker($parser); 1503 } else { 1504 throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances.'); 1505 } 1506 } 1507 1508 // node visitors 1509 foreach ($extension->getNodeVisitors() as $visitor) { 1510 $this->visitors[] = $visitor; 1511 } 1512 1513 // operators 1514 if ($operators = $extension->getOperators()) { 1515 if (!is_array($operators)) { 1516 throw new InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', get_class($extension), is_object($operators) ? get_class($operators) : gettype($operators).(is_resource($operators) ? '' : '#'.$operators))); 1517 } 1518 1519 if (2 !== count($operators)) { 1520 throw new InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', get_class($extension), count($operators))); 1521 } 1522 1523 $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]); 1524 $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]); 1525 } 1526 } 1527 1528 /** 1529 * @deprecated since 1.22 (to be removed in 2.0) 1530 */ 1531 protected function writeCacheFile($file, $content) 1532 { 1533 $this->cache->write($file, $content); 1534 } 1535 1536 private function updateOptionsHash() 1537 { 1538 $hashParts = array_merge( 1539 array_keys($this->extensions), 1540 array( 1541 (int) function_exists('twig_template_get_attributes'), 1542 PHP_MAJOR_VERSION, 1543 PHP_MINOR_VERSION, 1544 self::VERSION, 1545 (int) $this->debug, 1546 $this->baseTemplateClass, 1547 (int) $this->strictVariables, 1548 ) 1549 ); 1550 $this->optionsHash = implode(':', $hashParts); 1551 } 1552} 1553