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