[BUGFIX] ViewHelper Exceptions are logged in Production
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / Core / ViewHelper / AbstractViewHelper.php
1 <?php
2 namespace TYPO3\CMS\Fluid\Core\ViewHelper;
3
4 /* *
5 * This script is backported from the TYPO3 Flow package "TYPO3.Fluid". *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License, either version 3 *
9 * of the License, or (at your option) any later version. *
10 * *
11 * The TYPO3 project - inspiring people to share! *
12 * */
13 use Psr\Log\LoggerInterface;
14 use TYPO3\CMS\Core\Utility\GeneralUtility;
15 use TYPO3\CMS\Core\Log\LogManager;
16
17 /**
18 * The abstract base class for all view helpers.
19 *
20 * @api
21 */
22 abstract class AbstractViewHelper {
23
24 /**
25 * TRUE if arguments have already been initialized
26 *
27 * @var bool
28 */
29 private $argumentsInitialized = FALSE;
30
31 /**
32 * Stores all \TYPO3\CMS\Fluid\ArgumentDefinition instances
33 *
34 * @var array
35 */
36 private $argumentDefinitions = array();
37
38 /**
39 * Cache of argument definitions; the key is the ViewHelper class name, and the
40 * value is the array of argument definitions.
41 *
42 * In our benchmarks, this cache leads to a 40% improvement when using a certain
43 * ViewHelper class many times throughout the rendering process.
44 *
45 * @var array
46 */
47 static private $argumentDefinitionCache = array();
48
49 /**
50 * Current view helper node
51 *
52 * @var \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode
53 */
54 private $viewHelperNode;
55
56 /**
57 * Arguments array.
58 *
59 * @var array
60 * @api
61 */
62 protected $arguments;
63
64 /**
65 * Current variable container reference.
66 *
67 * @var \TYPO3\CMS\Fluid\Core\ViewHelper\TemplateVariableContainer
68 * @api
69 */
70 protected $templateVariableContainer;
71
72 /**
73 * Controller Context to use
74 *
75 * @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
76 * @api
77 */
78 protected $controllerContext;
79
80 /**
81 * @var \TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface
82 */
83 protected $renderingContext;
84
85 /**
86 * @var \Closure
87 */
88 protected $renderChildrenClosure = NULL;
89
90 /**
91 * ViewHelper Variable Container
92 *
93 * @var \TYPO3\CMS\Fluid\Core\ViewHelper\ViewHelperVariableContainer
94 * @api
95 */
96 protected $viewHelperVariableContainer;
97
98 /**
99 * Reflection service
100 *
101 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
102 */
103 private $reflectionService;
104
105 /**
106 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
107 * @inject
108 */
109 protected $objectManager;
110
111 /**
112 * With this flag, you can disable the escaping interceptor inside this ViewHelper.
113 * THIS MIGHT CHANGE WITHOUT NOTICE, NO PUBLIC API!
114 * @var bool
115 */
116 protected $escapingInterceptorEnabled = TRUE;
117
118 /**
119 * @param array $arguments
120 * @return void
121 */
122 public function setArguments(array $arguments) {
123 $this->arguments = $arguments;
124 }
125
126 /**
127 * @param \TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface $renderingContext
128 * @return void
129 */
130 public function setRenderingContext(\TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface $renderingContext) {
131 $this->renderingContext = $renderingContext;
132 $this->templateVariableContainer = $renderingContext->getTemplateVariableContainer();
133 if ($renderingContext->getControllerContext() !== NULL) {
134 $this->controllerContext = $renderingContext->getControllerContext();
135 }
136 $this->viewHelperVariableContainer = $renderingContext->getViewHelperVariableContainer();
137 }
138
139 /**
140 * Inject a Reflection service
141 *
142 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService Reflection service
143 */
144 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService) {
145 $this->reflectionService = $reflectionService;
146 }
147
148 /**
149 * Returns whether the escaping interceptor should be disabled or enabled inside the tags contents.
150 *
151 * THIS METHOD MIGHT CHANGE WITHOUT NOTICE; NO PUBLIC API!
152 *
153 * @return bool
154 */
155 public function isEscapingInterceptorEnabled() {
156 return $this->escapingInterceptorEnabled;
157 }
158
159 /**
160 * Register a new argument. Call this method from your ViewHelper subclass
161 * inside the initializeArguments() method.
162 *
163 * @param string $name Name of the argument
164 * @param string $type Type of the argument
165 * @param string $description Description of the argument
166 * @param bool $required If TRUE, argument is required. Defaults to FALSE.
167 * @param mixed $defaultValue Default value of argument
168 * @return \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
169 * @throws \TYPO3\CMS\Fluid\Core\ViewHelper\Exception
170 * @api
171 */
172 protected function registerArgument($name, $type, $description, $required = FALSE, $defaultValue = NULL) {
173 if (array_key_exists($name, $this->argumentDefinitions)) {
174 throw new \TYPO3\CMS\Fluid\Core\ViewHelper\Exception('Argument "' . $name . '" has already been defined, thus it should not be defined again.', 1253036401);
175 }
176 $this->argumentDefinitions[$name] = new \TYPO3\CMS\Fluid\Core\ViewHelper\ArgumentDefinition($name, $type, $description, $required, $defaultValue);
177 return $this;
178 }
179
180 /**
181 * Overrides a registered argument. Call this method from your ViewHelper subclass
182 * inside the initializeArguments() method if you want to override a previously registered argument.
183 *
184 * @see registerArgument()
185 * @param string $name Name of the argument
186 * @param string $type Type of the argument
187 * @param string $description Description of the argument
188 * @param bool $required If TRUE, argument is required. Defaults to FALSE.
189 * @param mixed $defaultValue Default value of argument
190 * @return \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
191 * @throws \TYPO3\CMS\Fluid\Core\ViewHelper\Exception
192 * @api
193 */
194 protected function overrideArgument($name, $type, $description, $required = FALSE, $defaultValue = NULL) {
195 if (!array_key_exists($name, $this->argumentDefinitions)) {
196 throw new \TYPO3\CMS\Fluid\Core\ViewHelper\Exception('Argument "' . $name . '" has not been defined, thus it can\'t be overridden.', 1279212461);
197 }
198 $this->argumentDefinitions[$name] = new \TYPO3\CMS\Fluid\Core\ViewHelper\ArgumentDefinition($name, $type, $description, $required, $defaultValue);
199 return $this;
200 }
201
202 /**
203 * Sets all needed attributes needed for the rendering. Called by the
204 * framework. Populates $this->viewHelperNode.
205 * This is PURELY INTERNAL! Never override this method!!
206 *
207 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode $node View Helper node to be set.
208 * @return void
209 */
210 public function setViewHelperNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode $node) {
211 $this->viewHelperNode = $node;
212 }
213
214 /**
215 * Called when being inside a cached template.
216 *
217 * @param \Closure $renderChildrenClosure
218 * @return void
219 */
220 public function setRenderChildrenClosure(\Closure $renderChildrenClosure) {
221 $this->renderChildrenClosure = $renderChildrenClosure;
222 }
223
224 /**
225 * Initialize the arguments of the ViewHelper, and call the render() method of the ViewHelper.
226 *
227 * @return string the rendered ViewHelper.
228 */
229 public function initializeArgumentsAndRender() {
230 $this->validateArguments();
231 $this->initialize();
232
233 return $this->callRenderMethod();
234 }
235
236 /**
237 * Call the render() method and handle errors.
238 *
239 * @return string the rendered ViewHelper
240 * @throws Exception
241 */
242 protected function callRenderMethod() {
243 $renderMethodParameters = array();
244 foreach ($this->argumentDefinitions as $argumentName => $argumentDefinition) {
245 if ($argumentDefinition->isMethodParameter()) {
246 $renderMethodParameters[$argumentName] = $this->arguments[$argumentName];
247 }
248 }
249
250 try {
251 return call_user_func_array(array($this, 'render'), $renderMethodParameters);
252 } catch (Exception $exception) {
253 if (GeneralUtility::getApplicationContext()->isProduction()) {
254 $this->getLogger()->error('A Fluid ViewHelper Exception was captured: ' . $exception->getMessage() . ' (' . $exception->getCode() . ')', array('exception' => $exception));
255 return '';
256 } else {
257 throw $exception;
258 }
259 }
260 }
261
262 /**
263 * @return LoggerInterface
264 */
265 protected function getLogger() {
266 return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
267 }
268
269 /**
270 * Initializes the view helper before invoking the render method.
271 *
272 * Override this method to solve tasks before the view helper content is rendered.
273 *
274 * @return void
275 * @api
276 */
277 public function initialize() {
278 }
279
280 /**
281 * Helper method which triggers the rendering of everything between the
282 * opening and the closing tag.
283 *
284 * @return mixed The finally rendered child nodes.
285 * @api
286 */
287 public function renderChildren() {
288 if ($this->renderChildrenClosure !== NULL) {
289 $closure = $this->renderChildrenClosure;
290 return $closure();
291 }
292 return $this->viewHelperNode->evaluateChildNodes($this->renderingContext);
293 }
294
295 /**
296 * Helper which is mostly needed when calling renderStatic() from within
297 * render().
298 *
299 * No public API yet.
300 *
301 * @return \Closure
302 */
303 protected function buildRenderChildrenClosure() {
304 $self = $this;
305 return function () use ($self) {
306 return $self->renderChildren();
307 };
308 }
309
310 /**
311 * Initialize all arguments and return them
312 *
313 * @return array Array of \TYPO3\CMS\Fluid\Core\ViewHelper\ArgumentDefinition instances.
314 */
315 public function prepareArguments() {
316 if (!$this->argumentsInitialized) {
317 $thisClassName = get_class($this);
318 if (isset(self::$argumentDefinitionCache[$thisClassName])) {
319 $this->argumentDefinitions = self::$argumentDefinitionCache[$thisClassName];
320 } else {
321 $this->registerRenderMethodArguments();
322 $this->initializeArguments();
323 self::$argumentDefinitionCache[$thisClassName] = $this->argumentDefinitions;
324 }
325 $this->argumentsInitialized = TRUE;
326 }
327 return $this->argumentDefinitions;
328 }
329
330 /**
331 * Register method arguments for "render" by analysing the doc comment above.
332 *
333 * @return void
334 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
335 */
336 private function registerRenderMethodArguments() {
337 $methodParameters = $this->reflectionService->getMethodParameters(get_class($this), 'render');
338 if (count($methodParameters) === 0) {
339 return;
340 }
341
342 if (\TYPO3\CMS\Fluid\Fluid::$debugMode) {
343 $methodTags = $this->reflectionService->getMethodTagsValues(get_class($this), 'render');
344
345 $paramAnnotations = array();
346 if (isset($methodTags['param'])) {
347 $paramAnnotations = $methodTags['param'];
348 }
349 }
350
351 $i = 0;
352 foreach ($methodParameters as $parameterName => $parameterInfo) {
353 $dataType = NULL;
354 if (isset($parameterInfo['type'])) {
355 $dataType = $parameterInfo['type'];
356 } elseif ($parameterInfo['array']) {
357 $dataType = 'array';
358 }
359 if ($dataType === NULL) {
360 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('could not determine type of argument "' . $parameterName . '" of the render-method in ViewHelper "' . get_class($this) . '". Either the methods docComment is invalid or some PHP optimizer strips off comments.', 1242292003);
361 }
362
363 $description = '';
364 if (\TYPO3\CMS\Fluid\Fluid::$debugMode && isset($paramAnnotations[$i])) {
365 $explodedAnnotation = explode(' ', $paramAnnotations[$i]);
366 array_shift($explodedAnnotation);
367 array_shift($explodedAnnotation);
368 $description = implode(' ', $explodedAnnotation);
369 }
370 $defaultValue = NULL;
371 if (isset($parameterInfo['defaultValue'])) {
372 $defaultValue = $parameterInfo['defaultValue'];
373 }
374 $this->argumentDefinitions[$parameterName] = new \TYPO3\CMS\Fluid\Core\ViewHelper\ArgumentDefinition($parameterName, $dataType, $description, ($parameterInfo['optional'] === FALSE), $defaultValue, TRUE);
375 $i++;
376 }
377 }
378
379 /**
380 * Validate arguments, and throw exception if arguments do not validate.
381 *
382 * @return void
383 * @throws \InvalidArgumentException
384 */
385 public function validateArguments() {
386 $argumentDefinitions = $this->prepareArguments();
387 if (!count($argumentDefinitions)) {
388 return;
389 }
390 foreach ($argumentDefinitions as $argumentName => $registeredArgument) {
391 if ($this->hasArgument($argumentName)) {
392 if ($this->arguments[$argumentName] === $registeredArgument->getDefaultValue()) {
393 continue;
394 }
395
396 $type = $registeredArgument->getType();
397 if ($type === 'array') {
398 if (!is_array($this->arguments[$argumentName]) && !$this->arguments[$argumentName] instanceof \ArrayAccess && !$this->arguments[$argumentName] instanceof \Traversable) {
399 throw new \InvalidArgumentException('The argument "' . $argumentName . '" was registered with type "array", but is of type "' . gettype($this->arguments[$argumentName]) . '" in view helper "' . get_class($this) . '"', 1237900529);
400 }
401 } elseif ($type === 'boolean') {
402 if (!is_bool($this->arguments[$argumentName])) {
403 throw new \InvalidArgumentException('The argument "' . $argumentName . '" was registered with type "boolean", but is of type "' . gettype($this->arguments[$argumentName]) . '" in view helper "' . get_class($this) . '".', 1240227732);
404 }
405 } elseif (class_exists($type, FALSE)) {
406 if (!($this->arguments[$argumentName] instanceof $type)) {
407 if (is_object($this->arguments[$argumentName])) {
408 throw new \InvalidArgumentException('The argument "' . $argumentName . '" was registered with type "' . $type . '", but is of type "' . get_class($this->arguments[$argumentName]) . '" in view helper "' . get_class($this) . '".', 1256475114);
409 } else {
410 throw new \InvalidArgumentException('The argument "' . $argumentName . '" was registered with type "' . $type . '", but is of type "' . gettype($this->arguments[$argumentName]) . '" in view helper "' . get_class($this) . '".', 1256475113);
411 }
412 }
413 }
414 }
415 }
416 }
417
418 /**
419 * Initialize all arguments. You need to override this method and call
420 * $this->registerArgument(...) inside this method, to register all your arguments.
421 *
422 * @return void
423 * @api
424 */
425 public function initializeArguments() {
426 }
427
428 /**
429 * Render method you need to implement for your custom view helper.
430 * Available objects at this point are $this->arguments, and $this->templateVariableContainer.
431 *
432 * Besides, you often need $this->renderChildren().
433 *
434 * @return string rendered string, view helper specific
435 * @api
436 */
437 //abstract public function render();
438
439 /**
440 * Tests if the given $argumentName is set, and not NULL.
441 *
442 * @param string $argumentName
443 * @return bool TRUE if $argumentName is found, FALSE otherwise
444 * @api
445 */
446 protected function hasArgument($argumentName) {
447 return isset($this->arguments[$argumentName]) && $this->arguments[$argumentName] !== NULL;
448 }
449
450 /**
451 * Default implementation for CompilableInterface. By default,
452 * inserts a renderStatic() call to itself.
453 *
454 * You only should override this method *when you absolutely know what you
455 * are doing*, and really want to influence the generated PHP code during
456 * template compilation directly.
457 *
458 * @param string $argumentsVariableName
459 * @param string $renderChildrenClosureVariableName
460 * @param string $initializationPhpCode
461 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $syntaxTreeNode
462 * @param \TYPO3\CMS\Fluid\Core\Compiler\TemplateCompiler $templateCompiler
463 * @return string
464 * @see \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\CompilableInterface
465 */
466 public function compile($argumentsVariableName, $renderChildrenClosureVariableName, &$initializationPhpCode, \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $syntaxTreeNode, \TYPO3\CMS\Fluid\Core\Compiler\TemplateCompiler $templateCompiler) {
467 return sprintf('%s::renderStatic(%s, %s, $renderingContext)',
468 get_class($this), $argumentsVariableName, $renderChildrenClosureVariableName);
469 }
470
471 /**
472 * Default implementation for CompilableInterface. See CompilableInterface
473 * for a detailed description of this method.
474 *
475 * @param array $arguments
476 * @param \Closure $renderChildrenClosure
477 * @param \TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface $renderingContext
478 * @return mixed
479 * @see \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\CompilableInterface
480 */
481 static public function renderStatic(array $arguments, \Closure $renderChildrenClosure, \TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface $renderingContext) {
482 return NULL;
483 }
484
485 /**
486 * Resets the ViewHelper state.
487 *
488 * Overwrite this method if you need to get a clean state of your ViewHelper.
489 *
490 * @return void
491 */
492 public function resetState() {
493 }
494
495 }