1<?php 2namespace TYPO3Fluid\Fluid\Core\ViewHelper; 3 4/* 5 * This file belongs to the package "TYPO3 Fluid". 6 * See LICENSE.txt that was shipped with this package. 7 */ 8 9use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler; 10use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface; 11use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode; 12use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode; 13use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; 14use TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface; 15 16/** 17 * The abstract base class for all view helpers. 18 * 19 * @api 20 */ 21abstract class AbstractViewHelper implements ViewHelperInterface 22{ 23 24 /** 25 * Stores all \TYPO3Fluid\Fluid\ArgumentDefinition instances 26 * @var ArgumentDefinition[] 27 */ 28 protected $argumentDefinitions = []; 29 30 /** 31 * Cache of argument definitions; the key is the ViewHelper class name, and the 32 * value is the array of argument definitions. 33 * 34 * In our benchmarks, this cache leads to a 40% improvement when using a certain 35 * ViewHelper class many times throughout the rendering process. 36 * @var array 37 */ 38 static private $argumentDefinitionCache = []; 39 40 /** 41 * Current view helper node 42 * @var ViewHelperNode 43 */ 44 protected $viewHelperNode; 45 46 /** 47 * Arguments array. 48 * @var array 49 * @api 50 */ 51 protected $arguments = []; 52 53 /** 54 * Arguments array. 55 * @var NodeInterface[] array 56 * @api 57 */ 58 protected $childNodes = []; 59 60 /** 61 * Current variable container reference. 62 * @var VariableProviderInterface 63 * @api 64 */ 65 protected $templateVariableContainer; 66 67 /** 68 * @var RenderingContextInterface 69 */ 70 protected $renderingContext; 71 72 /** 73 * @var \Closure 74 */ 75 protected $renderChildrenClosure = null; 76 77 /** 78 * ViewHelper Variable Container 79 * @var ViewHelperVariableContainer 80 * @api 81 */ 82 protected $viewHelperVariableContainer; 83 84 /** 85 * Specifies whether the escaping interceptors should be disabled or enabled for the result of renderChildren() calls within this ViewHelper 86 * @see isChildrenEscapingEnabled() 87 * 88 * Note: If this is NULL the value of $this->escapingInterceptorEnabled is considered for backwards compatibility 89 * 90 * @var boolean 91 * @api 92 */ 93 protected $escapeChildren = null; 94 95 /** 96 * Specifies whether the escaping interceptors should be disabled or enabled for the render-result of this ViewHelper 97 * @see isOutputEscapingEnabled() 98 * 99 * @var boolean 100 * @api 101 */ 102 protected $escapeOutput = null; 103 104 /** 105 * @param array $arguments 106 * @return void 107 */ 108 public function setArguments(array $arguments) 109 { 110 $this->arguments = $arguments; 111 } 112 113 /** 114 * @param RenderingContextInterface $renderingContext 115 * @return void 116 */ 117 public function setRenderingContext(RenderingContextInterface $renderingContext) 118 { 119 $this->renderingContext = $renderingContext; 120 $this->templateVariableContainer = $renderingContext->getVariableProvider(); 121 $this->viewHelperVariableContainer = $renderingContext->getViewHelperVariableContainer(); 122 } 123 124 /** 125 * Returns whether the escaping interceptors should be disabled or enabled for the result of renderChildren() calls within this ViewHelper 126 * 127 * Note: This method is no public API, use $this->escapeChildren instead! 128 * 129 * @return boolean 130 */ 131 public function isChildrenEscapingEnabled() 132 { 133 if ($this->escapeChildren === null) { 134 // Disable children escaping automatically, if output escaping is on anyway. 135 return !$this->isOutputEscapingEnabled(); 136 } 137 return $this->escapeChildren; 138 } 139 140 /** 141 * Returns whether the escaping interceptors should be disabled or enabled for the render-result of this ViewHelper 142 * 143 * Note: This method is no public API, use $this->escapeChildren instead! 144 * 145 * @return boolean 146 */ 147 public function isOutputEscapingEnabled() 148 { 149 return $this->escapeOutput !== false; 150 } 151 152 /** 153 * Register a new argument. Call this method from your ViewHelper subclass 154 * inside the initializeArguments() method. 155 * 156 * @param string $name Name of the argument 157 * @param string $type Type of the argument 158 * @param string $description Description of the argument 159 * @param boolean $required If TRUE, argument is required. Defaults to FALSE. 160 * @param mixed $defaultValue Default value of argument 161 * @param bool|null $escape Can be toggled to TRUE to force escaping of variables and inline syntax passed as argument value. 162 * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining. 163 * @throws Exception 164 * @api 165 */ 166 protected function registerArgument($name, $type, $description, $required = false, $defaultValue = null, $escape = null) 167 { 168 if (array_key_exists($name, $this->argumentDefinitions)) { 169 throw new Exception( 170 'Argument "' . $name . '" has already been defined, thus it should not be defined again.', 171 1253036401 172 ); 173 } 174 $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue, $escape); 175 return $this; 176 } 177 178 /** 179 * Overrides a registered argument. Call this method from your ViewHelper subclass 180 * inside the initializeArguments() method if you want to override a previously registered argument. 181 * @see registerArgument() 182 * 183 * @param string $name Name of the argument 184 * @param string $type Type of the argument 185 * @param string $description Description of the argument 186 * @param boolean $required If TRUE, argument is required. Defaults to FALSE. 187 * @param mixed $defaultValue Default value of argument 188 * @param bool|null $escape Can be toggled to TRUE to force escaping of variables and inline syntax passed as argument value. 189 * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining. 190 * @throws Exception 191 * @api 192 */ 193 protected function overrideArgument($name, $type, $description, $required = false, $defaultValue = null, $escape = null) 194 { 195 if (!array_key_exists($name, $this->argumentDefinitions)) { 196 throw new Exception( 197 'Argument "' . $name . '" has not been defined, thus it can\'t be overridden.', 198 1279212461 199 ); 200 } 201 $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue, $escape); 202 return $this; 203 } 204 205 /** 206 * Sets all needed attributes needed for the rendering. Called by the 207 * framework. Populates $this->viewHelperNode. 208 * This is PURELY INTERNAL! Never override this method!! 209 * 210 * @param ViewHelperNode $node View Helper node to be set. 211 * @return void 212 */ 213 public function setViewHelperNode(ViewHelperNode $node) 214 { 215 $this->viewHelperNode = $node; 216 } 217 218 /** 219 * Sets all needed attributes needed for the rendering. Called by the 220 * framework. Populates $this->viewHelperNode. 221 * This is PURELY INTERNAL! Never override this method!! 222 * 223 * @param NodeInterface[] $childNodes 224 * @return void 225 */ 226 public function setChildNodes(array $childNodes) 227 { 228 $this->childNodes = $childNodes; 229 } 230 231 /** 232 * Called when being inside a cached template. 233 * 234 * @param \Closure $renderChildrenClosure 235 * @return void 236 */ 237 public function setRenderChildrenClosure(\Closure $renderChildrenClosure) 238 { 239 $this->renderChildrenClosure = $renderChildrenClosure; 240 } 241 242 /** 243 * Initialize the arguments of the ViewHelper, and call the render() method of the ViewHelper. 244 * 245 * @return string the rendered ViewHelper. 246 */ 247 public function initializeArgumentsAndRender() 248 { 249 $this->validateArguments(); 250 $this->initialize(); 251 252 return $this->callRenderMethod(); 253 } 254 255 /** 256 * Call the render() method and handle errors. 257 * 258 * @return string the rendered ViewHelper 259 * @throws Exception 260 */ 261 protected function callRenderMethod() 262 { 263 if (method_exists($this, 'render')) { 264 return call_user_func([$this, 'render']); 265 } 266 if ((new \ReflectionMethod($this, 'renderStatic'))->getDeclaringClass()->getName() !== AbstractViewHelper::class) { 267 // Method is safe to call - will not recurse through ViewHelperInvoker via the default 268 // implementation of renderStatic() on this class. 269 return static::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext); 270 } 271 throw new Exception( 272 sprintf( 273 'ViewHelper class "%s" does not declare a "render()" method and inherits the default "renderStatic". ' . 274 'Executing this ViewHelper would cause infinite recursion - please either implement "render()" or ' . 275 '"renderStatic()" on your ViewHelper class', 276 get_class($this) 277 ) 278 ); 279 } 280 281 /** 282 * Initializes the view helper before invoking the render method. 283 * 284 * Override this method to solve tasks before the view helper content is rendered. 285 * 286 * @return void 287 * @api 288 */ 289 public function initialize() 290 { 291 } 292 293 /** 294 * Helper method which triggers the rendering of everything between the 295 * opening and the closing tag. 296 * 297 * @return mixed The finally rendered child nodes. 298 * @api 299 */ 300 public function renderChildren() 301 { 302 if ($this->renderChildrenClosure !== null) { 303 $closure = $this->renderChildrenClosure; 304 return $closure(); 305 } 306 return $this->viewHelperNode->evaluateChildNodes($this->renderingContext); 307 } 308 309 /** 310 * Helper which is mostly needed when calling renderStatic() from within 311 * render(). 312 * 313 * No public API yet. 314 * 315 * @return \Closure 316 */ 317 protected function buildRenderChildrenClosure() 318 { 319 $self = clone $this; 320 return function() use ($self) { 321 return $self->renderChildren(); 322 }; 323 } 324 325 /** 326 * Initialize all arguments and return them 327 * 328 * @return ArgumentDefinition[] 329 */ 330 public function prepareArguments() 331 { 332 $thisClassName = get_class($this); 333 if (isset(self::$argumentDefinitionCache[$thisClassName])) { 334 $this->argumentDefinitions = self::$argumentDefinitionCache[$thisClassName]; 335 } else { 336 $this->initializeArguments(); 337 self::$argumentDefinitionCache[$thisClassName] = $this->argumentDefinitions; 338 } 339 return $this->argumentDefinitions; 340 } 341 342 /** 343 * Validate arguments, and throw exception if arguments do not validate. 344 * 345 * @return void 346 * @throws \InvalidArgumentException 347 */ 348 public function validateArguments() 349 { 350 $argumentDefinitions = $this->prepareArguments(); 351 foreach ($argumentDefinitions as $argumentName => $registeredArgument) { 352 if ($this->hasArgument($argumentName)) { 353 $value = $this->arguments[$argumentName]; 354 $type = $registeredArgument->getType(); 355 if ($value !== $registeredArgument->getDefaultValue() && $type !== 'mixed') { 356 $givenType = is_object($value) ? get_class($value) : gettype($value); 357 if (!$this->isValidType($type, $value)) { 358 throw new \InvalidArgumentException( 359 'The argument "' . $argumentName . '" was registered with type "' . $type . '", but is of type "' . 360 $givenType . '" in view helper "' . get_class($this) . '".', 361 1256475113 362 ); 363 } 364 } 365 } 366 } 367 } 368 369 /** 370 * Check whether the defined type matches the value type 371 * 372 * @param string $type 373 * @param mixed $value 374 * @return boolean 375 */ 376 protected function isValidType($type, $value) 377 { 378 if ($type === 'object') { 379 if (!is_object($value)) { 380 return false; 381 } 382 } elseif ($type === 'array' || substr($type, -2) === '[]') { 383 if (!is_array($value) && !$value instanceof \ArrayAccess && !$value instanceof \Traversable && !empty($value)) { 384 return false; 385 } elseif (substr($type, -2) === '[]') { 386 $firstElement = $this->getFirstElementOfNonEmpty($value); 387 if ($firstElement === null) { 388 return true; 389 } 390 return $this->isValidType(substr($type, 0, -2), $firstElement); 391 } 392 } elseif ($type === 'string') { 393 if (is_object($value) && !method_exists($value, '__toString')) { 394 return false; 395 } 396 } elseif ($type === 'boolean' && !is_bool($value)) { 397 return false; 398 } elseif (class_exists($type) && $value !== null && !$value instanceof $type) { 399 return false; 400 } elseif (is_object($value) && !is_a($value, $type, true)) { 401 return false; 402 } 403 return true; 404 } 405 406 /** 407 * Return the first element of the given array, ArrayAccess or Traversable 408 * that is not empty 409 * 410 * @param mixed $value 411 * @return mixed 412 */ 413 protected function getFirstElementOfNonEmpty($value) 414 { 415 if (is_array($value)) { 416 return reset($value); 417 } elseif ($value instanceof \Traversable) { 418 foreach ($value as $element) { 419 return $element; 420 } 421 } 422 return null; 423 } 424 425 /** 426 * Initialize all arguments. You need to override this method and call 427 * $this->registerArgument(...) inside this method, to register all your arguments. 428 * 429 * @return void 430 * @api 431 */ 432 public function initializeArguments() 433 { 434 } 435 436 /** 437 * Tests if the given $argumentName is set, and not NULL. 438 * The isset() test used fills both those requirements. 439 * 440 * @param string $argumentName 441 * @return boolean TRUE if $argumentName is found, FALSE otherwise 442 * @api 443 */ 444 protected function hasArgument($argumentName) 445 { 446 return isset($this->arguments[$argumentName]); 447 } 448 449 /** 450 * Default implementation of "handling" additional, undeclared arguments. 451 * In this implementation the behavior is to consistently throw an error 452 * about NOT supporting any additional arguments. This method MUST be 453 * overridden by any ViewHelper that desires this support and this inherited 454 * method must not be called, obviously. 455 * 456 * @throws Exception 457 * @param array $arguments 458 * @return void 459 */ 460 public function handleAdditionalArguments(array $arguments) 461 { 462 } 463 464 /** 465 * Default implementation of validating additional, undeclared arguments. 466 * In this implementation the behavior is to consistently throw an error 467 * about NOT supporting any additional arguments. This method MUST be 468 * overridden by any ViewHelper that desires this support and this inherited 469 * method must not be called, obviously. 470 * 471 * @throws Exception 472 * @param array $arguments 473 * @return void 474 */ 475 public function validateAdditionalArguments(array $arguments) 476 { 477 if (!empty($arguments)) { 478 throw new Exception( 479 sprintf( 480 'Undeclared arguments passed to ViewHelper %s: %s. Valid arguments are: %s', 481 get_class($this), 482 implode(', ', array_keys($arguments)), 483 implode(', ', array_keys($this->argumentDefinitions)) 484 ) 485 ); 486 } 487 } 488 489 /** 490 * You only should override this method *when you absolutely know what you 491 * are doing*, and really want to influence the generated PHP code during 492 * template compilation directly. 493 * 494 * @param string $argumentsName 495 * @param string $closureName 496 * @param string $initializationPhpCode 497 * @param ViewHelperNode $node 498 * @param TemplateCompiler $compiler 499 * @return string 500 */ 501 public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler) 502 { 503 return sprintf( 504 '%s::renderStatic(%s, %s, $renderingContext)', 505 get_class($this), 506 $argumentsName, 507 $closureName 508 ); 509 } 510 511 /** 512 * Default implementation of static rendering; useful API method if your ViewHelper 513 * when compiled is able to render itself statically to increase performance. This 514 * default implementation will simply delegate to the ViewHelperInvoker. 515 * 516 * @param array $arguments 517 * @param \Closure $renderChildrenClosure 518 * @param RenderingContextInterface $renderingContext 519 * @return mixed 520 */ 521 public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) 522 { 523 $viewHelperClassName = get_called_class(); 524 return $renderingContext->getViewHelperInvoker()->invoke($viewHelperClassName, $arguments, $renderingContext, $renderChildrenClosure); 525 } 526 527 /** 528 * Save the associated ViewHelper node in a static public class variable. 529 * called directly after the ViewHelper was built. 530 * 531 * @param ViewHelperNode $node 532 * @param TextNode[] $arguments 533 * @param VariableProviderInterface $variableContainer 534 * @return void 535 */ 536 public static function postParseEvent(ViewHelperNode $node, array $arguments, VariableProviderInterface $variableContainer) 537 { 538 } 539 540 /** 541 * Resets the ViewHelper state. 542 * 543 * Overwrite this method if you need to get a clean state of your ViewHelper. 544 * 545 * @return void 546 */ 547 public function resetState() 548 { 549 } 550} 551