1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Templating; 13 14use Symfony\Component\Templating\Helper\HelperInterface; 15use Symfony\Component\Templating\Loader\LoaderInterface; 16use Symfony\Component\Templating\Storage\FileStorage; 17use Symfony\Component\Templating\Storage\Storage; 18use Symfony\Component\Templating\Storage\StringStorage; 19 20/** 21 * PhpEngine is an engine able to render PHP templates. 22 * 23 * @author Fabien Potencier <fabien@symfony.com> 24 */ 25class PhpEngine implements EngineInterface, \ArrayAccess 26{ 27 protected $loader; 28 protected $current; 29 /** 30 * @var HelperInterface[] 31 */ 32 protected $helpers = []; 33 protected $parents = []; 34 protected $stack = []; 35 protected $charset = 'UTF-8'; 36 protected $cache = []; 37 protected $escapers = []; 38 protected static $escaperCache = []; 39 protected $globals = []; 40 protected $parser; 41 42 private $evalTemplate; 43 private $evalParameters; 44 45 /** 46 * @param TemplateNameParserInterface $parser A TemplateNameParserInterface instance 47 * @param LoaderInterface $loader A loader instance 48 * @param HelperInterface[] $helpers An array of helper instances 49 */ 50 public function __construct(TemplateNameParserInterface $parser, LoaderInterface $loader, array $helpers = []) 51 { 52 $this->parser = $parser; 53 $this->loader = $loader; 54 55 $this->addHelpers($helpers); 56 57 $this->initializeEscapers(); 58 foreach ($this->escapers as $context => $escaper) { 59 $this->setEscaper($context, $escaper); 60 } 61 } 62 63 /** 64 * {@inheritdoc} 65 * 66 * @throws \InvalidArgumentException if the template does not exist 67 */ 68 public function render($name, array $parameters = []) 69 { 70 $storage = $this->load($name); 71 $key = hash('sha256', serialize($storage)); 72 $this->current = $key; 73 $this->parents[$key] = null; 74 75 // attach the global variables 76 $parameters = array_replace($this->getGlobals(), $parameters); 77 // render 78 if (false === $content = $this->evaluate($storage, $parameters)) { 79 throw new \RuntimeException(sprintf('The template "%s" cannot be rendered.', $this->parser->parse($name))); 80 } 81 82 // decorator 83 if ($this->parents[$key]) { 84 $slots = $this->get('slots'); 85 $this->stack[] = $slots->get('_content'); 86 $slots->set('_content', $content); 87 88 $content = $this->render($this->parents[$key], $parameters); 89 90 $slots->set('_content', array_pop($this->stack)); 91 } 92 93 return $content; 94 } 95 96 /** 97 * {@inheritdoc} 98 */ 99 public function exists($name) 100 { 101 try { 102 $this->load($name); 103 } catch (\InvalidArgumentException $e) { 104 return false; 105 } 106 107 return true; 108 } 109 110 /** 111 * {@inheritdoc} 112 */ 113 public function supports($name) 114 { 115 $template = $this->parser->parse($name); 116 117 return 'php' === $template->get('engine'); 118 } 119 120 /** 121 * Evaluates a template. 122 * 123 * @param Storage $template The template to render 124 * @param array $parameters An array of parameters to pass to the template 125 * 126 * @return string|false The evaluated template, or false if the engine is unable to render the template 127 * 128 * @throws \InvalidArgumentException 129 */ 130 protected function evaluate(Storage $template, array $parameters = []) 131 { 132 $this->evalTemplate = $template; 133 $this->evalParameters = $parameters; 134 unset($template, $parameters); 135 136 if (isset($this->evalParameters['this'])) { 137 throw new \InvalidArgumentException('Invalid parameter (this).'); 138 } 139 if (isset($this->evalParameters['view'])) { 140 throw new \InvalidArgumentException('Invalid parameter (view).'); 141 } 142 143 // the view variable is exposed to the require file below 144 $view = $this; 145 if ($this->evalTemplate instanceof FileStorage) { 146 extract($this->evalParameters, \EXTR_SKIP); 147 $this->evalParameters = null; 148 149 ob_start(); 150 require $this->evalTemplate; 151 152 $this->evalTemplate = null; 153 154 return ob_get_clean(); 155 } elseif ($this->evalTemplate instanceof StringStorage) { 156 extract($this->evalParameters, \EXTR_SKIP); 157 $this->evalParameters = null; 158 159 ob_start(); 160 eval('; ?>'.$this->evalTemplate.'<?php ;'); 161 162 $this->evalTemplate = null; 163 164 return ob_get_clean(); 165 } 166 167 return false; 168 } 169 170 /** 171 * Gets a helper value. 172 * 173 * @param string $name The helper name 174 * 175 * @return HelperInterface The helper value 176 * 177 * @throws \InvalidArgumentException if the helper is not defined 178 */ 179 public function offsetGet($name) 180 { 181 return $this->get($name); 182 } 183 184 /** 185 * Returns true if the helper is defined. 186 * 187 * @param string $name The helper name 188 * 189 * @return bool true if the helper is defined, false otherwise 190 */ 191 public function offsetExists($name) 192 { 193 return isset($this->helpers[$name]); 194 } 195 196 /** 197 * Sets a helper. 198 * 199 * @param HelperInterface $name The helper instance 200 * @param string $value An alias 201 */ 202 public function offsetSet($name, $value) 203 { 204 $this->set($name, $value); 205 } 206 207 /** 208 * Removes a helper. 209 * 210 * @param string $name The helper name 211 * 212 * @throws \LogicException 213 */ 214 public function offsetUnset($name) 215 { 216 throw new \LogicException(sprintf('You can\'t unset a helper (%s).', $name)); 217 } 218 219 /** 220 * Adds some helpers. 221 * 222 * @param HelperInterface[] $helpers An array of helper 223 */ 224 public function addHelpers(array $helpers) 225 { 226 foreach ($helpers as $alias => $helper) { 227 $this->set($helper, \is_int($alias) ? null : $alias); 228 } 229 } 230 231 /** 232 * Sets the helpers. 233 * 234 * @param HelperInterface[] $helpers An array of helper 235 */ 236 public function setHelpers(array $helpers) 237 { 238 $this->helpers = []; 239 $this->addHelpers($helpers); 240 } 241 242 /** 243 * Sets a helper. 244 * 245 * @param HelperInterface $helper The helper instance 246 * @param string $alias An alias 247 */ 248 public function set(HelperInterface $helper, $alias = null) 249 { 250 $this->helpers[$helper->getName()] = $helper; 251 if (null !== $alias) { 252 $this->helpers[$alias] = $helper; 253 } 254 255 $helper->setCharset($this->charset); 256 } 257 258 /** 259 * Returns true if the helper if defined. 260 * 261 * @param string $name The helper name 262 * 263 * @return bool true if the helper is defined, false otherwise 264 */ 265 public function has($name) 266 { 267 return isset($this->helpers[$name]); 268 } 269 270 /** 271 * Gets a helper value. 272 * 273 * @param string $name The helper name 274 * 275 * @return HelperInterface The helper instance 276 * 277 * @throws \InvalidArgumentException if the helper is not defined 278 */ 279 public function get($name) 280 { 281 if (!isset($this->helpers[$name])) { 282 throw new \InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); 283 } 284 285 return $this->helpers[$name]; 286 } 287 288 /** 289 * Decorates the current template with another one. 290 * 291 * @param string $template The decorator logical name 292 */ 293 public function extend($template) 294 { 295 $this->parents[$this->current] = $template; 296 } 297 298 /** 299 * Escapes a string by using the current charset. 300 * 301 * @param mixed $value A variable to escape 302 * @param string $context The context name 303 * 304 * @return mixed The escaped value 305 */ 306 public function escape($value, $context = 'html') 307 { 308 if (is_numeric($value)) { 309 return $value; 310 } 311 312 // If we deal with a scalar value, we can cache the result to increase 313 // the performance when the same value is escaped multiple times (e.g. loops) 314 if (is_scalar($value)) { 315 if (!isset(self::$escaperCache[$context][$value])) { 316 self::$escaperCache[$context][$value] = \call_user_func($this->getEscaper($context), $value); 317 } 318 319 return self::$escaperCache[$context][$value]; 320 } 321 322 return \call_user_func($this->getEscaper($context), $value); 323 } 324 325 /** 326 * Sets the charset to use. 327 * 328 * @param string $charset The charset 329 */ 330 public function setCharset($charset) 331 { 332 if ('UTF8' === $charset = strtoupper($charset)) { 333 $charset = 'UTF-8'; // iconv on Windows requires "UTF-8" instead of "UTF8" 334 } 335 $this->charset = $charset; 336 337 foreach ($this->helpers as $helper) { 338 $helper->setCharset($this->charset); 339 } 340 } 341 342 /** 343 * Gets the current charset. 344 * 345 * @return string The current charset 346 */ 347 public function getCharset() 348 { 349 return $this->charset; 350 } 351 352 /** 353 * Adds an escaper for the given context. 354 * 355 * @param string $context The escaper context (html, js, ...) 356 * @param callable $escaper A PHP callable 357 */ 358 public function setEscaper($context, callable $escaper) 359 { 360 $this->escapers[$context] = $escaper; 361 self::$escaperCache[$context] = []; 362 } 363 364 /** 365 * Gets an escaper for a given context. 366 * 367 * @param string $context The context name 368 * 369 * @return callable A PHP callable 370 * 371 * @throws \InvalidArgumentException 372 */ 373 public function getEscaper($context) 374 { 375 if (!isset($this->escapers[$context])) { 376 throw new \InvalidArgumentException(sprintf('No registered escaper for context "%s".', $context)); 377 } 378 379 return $this->escapers[$context]; 380 } 381 382 /** 383 * @param string $name 384 * @param mixed $value 385 */ 386 public function addGlobal($name, $value) 387 { 388 $this->globals[$name] = $value; 389 } 390 391 /** 392 * Returns the assigned globals. 393 * 394 * @return array 395 */ 396 public function getGlobals() 397 { 398 return $this->globals; 399 } 400 401 /** 402 * Initializes the built-in escapers. 403 * 404 * Each function specifies a way for applying a transformation to a string 405 * passed to it. The purpose is for the string to be "escaped" so it is 406 * suitable for the format it is being displayed in. 407 * 408 * For example, the string: "It's required that you enter a username & password.\n" 409 * If this were to be displayed as HTML it would be sensible to turn the 410 * ampersand into '&' and the apostrophe into '&aps;'. However if it were 411 * going to be used as a string in JavaScript to be displayed in an alert box 412 * it would be right to leave the string as-is, but c-escape the apostrophe and 413 * the new line. 414 * 415 * For each function there is a define to avoid problems with strings being 416 * incorrectly specified. 417 */ 418 protected function initializeEscapers() 419 { 420 $flags = \ENT_QUOTES | \ENT_SUBSTITUTE; 421 422 $this->escapers = [ 423 'html' => 424 /** 425 * Runs the PHP function htmlspecialchars on the value passed. 426 * 427 * @param string $value The value to escape 428 * 429 * @return string the escaped value 430 */ 431 function ($value) use ($flags) { 432 // Numbers and Boolean values get turned into strings which can cause problems 433 // with type comparisons (e.g. === or is_int() etc). 434 return \is_string($value) ? htmlspecialchars($value, $flags, $this->getCharset(), false) : $value; 435 }, 436 437 'js' => 438 /** 439 * A function that escape all non-alphanumeric characters 440 * into their \xHH or \uHHHH representations. 441 * 442 * @param string $value The value to escape 443 * 444 * @return string the escaped value 445 */ 446 function ($value) { 447 if ('UTF-8' != $this->getCharset()) { 448 $value = iconv($this->getCharset(), 'UTF-8', $value); 449 } 450 451 $callback = function ($matches) { 452 $char = $matches[0]; 453 454 // \xHH 455 if (!isset($char[1])) { 456 return '\\x'.substr('00'.bin2hex($char), -2); 457 } 458 459 // \uHHHH 460 $char = iconv('UTF-8', 'UTF-16BE', $char); 461 462 return '\\u'.substr('0000'.bin2hex($char), -4); 463 }; 464 465 if (null === $value = preg_replace_callback('#[^\p{L}\p{N} ]#u', $callback, $value)) { 466 throw new \InvalidArgumentException('The string to escape is not a valid UTF-8 string.'); 467 } 468 469 if ('UTF-8' != $this->getCharset()) { 470 $value = iconv('UTF-8', $this->getCharset(), $value); 471 } 472 473 return $value; 474 }, 475 ]; 476 477 self::$escaperCache = []; 478 } 479 480 /** 481 * Gets the loader associated with this engine. 482 * 483 * @return LoaderInterface A LoaderInterface instance 484 */ 485 public function getLoader() 486 { 487 return $this->loader; 488 } 489 490 /** 491 * Loads the given template. 492 * 493 * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance 494 * 495 * @return Storage A Storage instance 496 * 497 * @throws \InvalidArgumentException if the template cannot be found 498 */ 499 protected function load($name) 500 { 501 $template = $this->parser->parse($name); 502 503 $key = $template->getLogicalName(); 504 if (isset($this->cache[$key])) { 505 return $this->cache[$key]; 506 } 507 508 $storage = $this->loader->load($template); 509 510 if (false === $storage) { 511 throw new \InvalidArgumentException(sprintf('The template "%s" does not exist.', $template)); 512 } 513 514 return $this->cache[$key] = $storage; 515 } 516} 517