Completed
Pull Request — master (#270)
by Marc
03:17 queued 55s
created

AbstractViewHelper::createArgument()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace 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
9
use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
10
use TYPO3Fluid\Fluid\Core\Parser;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
14
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
15
use TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface;
16
17
/**
18
 * The abstract base class for all view helpers.
19
 *
20
 * @api
21
 */
22
abstract class AbstractViewHelper implements ViewHelperInterface
23
{
24
25
    /**
26
     * Stores all \TYPO3Fluid\Fluid\ArgumentDefinition instances
27
     * @var ArgumentDefinition[]
28
     */
29
    protected $argumentDefinitions = [];
30
31
    /**
32
     * Cache of argument definitions; the key is the ViewHelper class name, and the
33
     * value is the array of argument definitions.
34
     *
35
     * In our benchmarks, this cache leads to a 40% improvement when using a certain
36
     * ViewHelper class many times throughout the rendering process.
37
     * @var array
38
     */
39
    static private $argumentDefinitionCache = [];
40
41
    /**
42
     * Current view helper node
43
     * @var ViewHelperNode
44
     */
45
    protected $viewHelperNode;
46
47
    /**
48
     * Arguments array.
49
     * @var array
50
     * @api
51
     */
52
    protected $arguments = [];
53
54
    /**
55
     * Arguments array.
56
     * @var NodeInterface[] array
57
     * @api
58
     */
59
    protected $childNodes = [];
60
61
    /**
62
     * Current variable container reference.
63
     * @var VariableProviderInterface
64
     * @api
65
     */
66
    protected $templateVariableContainer;
67
68
    /**
69
     * @var RenderingContextInterface
70
     */
71
    protected $renderingContext;
72
73
    /**
74
     * @var \Closure
75
     */
76
    protected $renderChildrenClosure = null;
77
78
    /**
79
     * ViewHelper Variable Container
80
     * @var ViewHelperVariableContainer
81
     * @api
82
     */
83
    protected $viewHelperVariableContainer;
84
85
    /**
86
     * Specifies whether the escaping interceptors should be disabled or enabled for the result of renderChildren() calls within this ViewHelper
87
     * @see isChildrenEscapingEnabled()
88
     *
89
     * Note: If this is NULL the value of $this->escapingInterceptorEnabled is considered for backwards compatibility
90
     *
91
     * @var boolean
92
     * @api
93
     */
94
    protected $escapeChildren = null;
95
96
    /**
97
     * Specifies whether the escaping interceptors should be disabled or enabled for the render-result of this ViewHelper
98
     * @see isOutputEscapingEnabled()
99
     *
100
     * @var boolean
101
     * @api
102
     */
103
    protected $escapeOutput = null;
104
105
    /**
106
     * @param array $arguments
107
     * @return void
108
     */
109
    public function setArguments(array $arguments)
110
    {
111
        $this->arguments = $arguments;
112
    }
113
114
    /**
115
     * @param RenderingContextInterface $renderingContext
116
     * @return void
117
     */
118
    public function setRenderingContext(RenderingContextInterface $renderingContext)
119
    {
120
        $this->renderingContext = $renderingContext;
121
        $this->templateVariableContainer = $renderingContext->getVariableProvider();
122
        $this->viewHelperVariableContainer = $renderingContext->getViewHelperVariableContainer();
123
    }
124
125
    /**
126
     * Returns whether the escaping interceptors should be disabled or enabled for the result of renderChildren() calls within this ViewHelper
127
     *
128
     * Note: This method is no public API, use $this->escapeChildren instead!
129
     *
130
     * @return boolean
131
     */
132
    public function isChildrenEscapingEnabled()
133
    {
134
        if ($this->escapeChildren === null) {
135
            // Disable children escaping automatically, if output escaping is on anyway.
136
            return !$this->isOutputEscapingEnabled();
137
        }
138
        return $this->escapeChildren;
139
    }
140
141
    /**
142
     * Returns whether the escaping interceptors should be disabled or enabled for the render-result of this ViewHelper
143
     *
144
     * Note: This method is no public API, use $this->escapeChildren instead!
145
     *
146
     * @return boolean
147
     */
148
    public function isOutputEscapingEnabled()
149
    {
150
        return $this->escapeOutput !== false;
151
    }
152
153
    /**
154
     * Register a new argument. Call this method from your ViewHelper subclass
155
     * inside the initializeArguments() method.
156
     *
157
     * @param string $name Name of the argument
158
     * @param string $type Type of the argument
159
     * @param string $description Description of the argument
160
     * @param boolean $required If TRUE, argument is required. Defaults to FALSE.
161
     * @param mixed $defaultValue Default value of argument
162
     * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
163
     * @throws Exception
164
     * @api
165
     */
166 View Code Duplication
    protected function registerArgument($name, $type, $description, $required = false, $defaultValue = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
167
    {
168
        if (array_key_exists($name, $this->argumentDefinitions)) {
169
            throw new Exception('Argument "' . $name . '" has already been defined, thus it should not be defined again.', 1253036401);
170
        }
171
        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue);
172
        return $this;
173
    }
174
175
    /**
176
     * Register a new argument. Call this method from your ViewHelper subclass
177
     * inside the initializeArguments() method.
178
     *
179
     * @param string $name Name of the argument
180
     * @param string $type Type of the argument
181
     * @return ArgumentDefinition $this, to allow chaining.
182
     * @throws Exception
183
     * @api
184
     */
185
    protected function createArgument($name, $type)
186
    {
187
        if (array_key_exists($name, $this->argumentDefinitions)) {
188
            throw new Exception('Argument "' . $name . '" has already been defined, thus it should not be defined again.', 1253036401);
189
        }
190
        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type);
191
        return $this->argumentDefinitions[$name];
192
    }
193
194
    /**
195
     * Overrides a registered argument. Call this method from your ViewHelper subclass
196
     * inside the initializeArguments() method if you want to override a previously registered argument.
197
     * @see registerArgument()
198
     *
199
     * @param string $name Name of the argument
200
     * @param string $type Type of the argument
201
     * @param string $description Description of the argument
202
     * @param boolean $required If TRUE, argument is required. Defaults to FALSE.
203
     * @param mixed $defaultValue Default value of argument
204
     * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
205
     * @throws Exception
206
     * @api
207
     */
208 View Code Duplication
    protected function overrideArgument($name, $type, $description, $required = false, $defaultValue = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
209
    {
210
        if (!array_key_exists($name, $this->argumentDefinitions)) {
211
            throw new Exception('Argument "' . $name . '" has not been defined, thus it can\'t be overridden.', 1279212461);
212
        }
213
        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue);
214
        return $this;
215
    }
216
217
    /**
218
     * Sets all needed attributes needed for the rendering. Called by the
219
     * framework. Populates $this->viewHelperNode.
220
     * This is PURELY INTERNAL! Never override this method!!
221
     *
222
     * @param ViewHelperNode $node View Helper node to be set.
223
     * @return void
224
     */
225
    public function setViewHelperNode(ViewHelperNode $node)
226
    {
227
        $this->viewHelperNode = $node;
228
    }
229
230
    /**
231
     * Sets all needed attributes needed for the rendering. Called by the
232
     * framework. Populates $this->viewHelperNode.
233
     * This is PURELY INTERNAL! Never override this method!!
234
     *
235
     * @param NodeInterface[] $childNodes
236
     * @return void
237
     */
238
    public function setChildNodes(array $childNodes)
239
    {
240
        $this->childNodes = $childNodes;
241
    }
242
243
    /**
244
     * Called when being inside a cached template.
245
     *
246
     * @param \Closure $renderChildrenClosure
247
     * @return void
248
     */
249
    public function setRenderChildrenClosure(\Closure $renderChildrenClosure)
250
    {
251
        $this->renderChildrenClosure = $renderChildrenClosure;
252
    }
253
254
    /**
255
     * Initialize the arguments of the ViewHelper, and call the render() method of the ViewHelper.
256
     *
257
     * @return string the rendered ViewHelper.
258
     */
259
    public function initializeArgumentsAndRender()
260
    {
261
        $this->validateArguments();
262
        $this->initialize();
263
264
        return $this->callRenderMethod();
265
    }
266
267
    /**
268
     * Call the render() method and handle errors.
269
     *
270
     * @return string the rendered ViewHelper
271
     * @throws Exception
272
     */
273
    protected function callRenderMethod()
274
    {
275
        return call_user_func([$this, 'render']);
276
    }
277
278
    /**
279
     * Initializes the view helper before invoking the render method.
280
     *
281
     * Override this method to solve tasks before the view helper content is rendered.
282
     *
283
     * @return void
284
     * @api
285
     */
286
    public function initialize()
287
    {
288
    }
289
290
    /**
291
     * Helper method which triggers the rendering of everything between the
292
     * opening and the closing tag.
293
     *
294
     * @return mixed The finally rendered child nodes.
295
     * @api
296
     */
297
    public function renderChildren()
298
    {
299
        if ($this->renderChildrenClosure !== null) {
300
            $closure = $this->renderChildrenClosure;
301
            return $closure();
302
        }
303
        return $this->viewHelperNode->evaluateChildNodes($this->renderingContext);
304
    }
305
306
    /**
307
     * Helper which is mostly needed when calling renderStatic() from within
308
     * render().
309
     *
310
     * No public API yet.
311
     *
312
     * @return \Closure
313
     */
314
    protected function buildRenderChildrenClosure()
315
    {
316
        $self = clone $this;
317
        return function() use ($self) {
318
            return $self->renderChildren();
319
        };
320
    }
321
322
    /**
323
     * Initialize all arguments and return them
324
     *
325
     * @return ArgumentDefinition[]
326
     */
327
    public function prepareArguments()
328
    {
329
        $thisClassName = get_class($this);
330
        if (isset(self::$argumentDefinitionCache[$thisClassName])) {
331
            $this->argumentDefinitions = self::$argumentDefinitionCache[$thisClassName];
332
        } else {
333
            $this->initializeArguments();
334
            self::$argumentDefinitionCache[$thisClassName] = $this->argumentDefinitions;
335
        }
336
        return $this->argumentDefinitions;
337
    }
338
339
    /**
340
     * Validate arguments, and throw exception if arguments do not validate.
341
     *
342
     * @return void
343
     * @throws \InvalidArgumentException
344
     */
345
    public function validateArguments()
346
    {
347
        $argumentDefinitions = $this->prepareArguments();
348
        foreach ($argumentDefinitions as $argumentName => $registeredArgument) {
349
            if ($this->hasArgument($argumentName)) {
350
                $value = $this->arguments[$argumentName];
351
                $type = $registeredArgument->getType();
352
                if ($value !== $registeredArgument->getDefaultValue() && $type !== 'mixed') {
353
                    $givenType = is_object($value) ? get_class($value) : gettype($value);
354
                    $errorException = new \InvalidArgumentException(
355
                        'The argument "' . $argumentName . '" was registered with type "' . $type . '", but is of type "' .
356
                        $givenType . '" in view helper "' . get_class($this) . '".',
357
                        1256475113
358
                    );
359
                    if (!$this->isValidType($type, $value)) {
360
                        throw $errorException;
361
                    }
362
                }
363
            }
364
        }
365
    }
366
367
    /**
368
     * Check whether the defined type matches the value type
369
     *
370
     * @param string $type
371
     * @param mixed $value
372
     * @return boolean
373
     */
374
    protected function isValidType($type, $value)
375
    {
376
        if ($type === 'object') {
377
            if (!is_object($value)) {
378
                return false;
379
            }
380
        } elseif ($type === 'array' || substr($type, -2) === '[]') {
381
            if (!is_array($value) && !$value instanceof \ArrayAccess && !$value instanceof \Traversable && !empty($value)) {
382
                return false;
383
            } elseif (substr($type, -2) === '[]') {
384
                $firstElement = $this->getFirstElementOfNonEmpty($value);
385
                if ($firstElement === null) {
386
                    return true;
387
                }
388
                return $this->isValidType(substr($type, 0, -2), $firstElement);
389
            }
390
        } elseif ($type === 'string') {
391
            if (is_object($value) && !method_exists($value, '__toString')) {
392
                return false;
393
            }
394
        } elseif ($type === 'boolean' && !is_bool($value)) {
395
            return false;
396
        } elseif (class_exists($type) && $value !== null && !$value instanceof $type) {
397
            return false;
398
        } elseif (is_object($value) && !is_a($value, $type, true)) {
399
            return false;
400
        }
401
        return true;
402
    }
403
404
    /**
405
     * Return the first element of the given array, ArrayAccess or Traversable
406
     * that is not empty
407
     *
408
     * @param mixed $value
409
     */
410
    protected function getFirstElementOfNonEmpty($value)
411
    {
412
        if (is_array($value)) {
413
            return reset($value);
414
        } elseif ($value instanceof \Traversable) {
415
            foreach ($value as $element) {
416
                return $element;
417
            }
418
        }
419
        return null;
420
    }
421
422
    /**
423
     * Initialize all arguments. You need to override this method and call
424
     * $this->registerArgument(...) inside this method, to register all your arguments.
425
     *
426
     * @return void
427
     * @api
428
     */
429
    public function initializeArguments()
430
    {
431
    }
432
433
    /**
434
     * Tests if the given $argumentName is set, and not NULL.
435
     * The isset() test used fills both those requirements.
436
     *
437
     * @param string $argumentName
438
     * @return boolean TRUE if $argumentName is found, FALSE otherwise
439
     * @api
440
     */
441
    protected function hasArgument($argumentName)
442
    {
443
        return isset($this->arguments[$argumentName]);
444
    }
445
446
    /**
447
     * Default implementation of "handling" additional, undeclared arguments.
448
     * In this implementation the behavior is to consistently throw an error
449
     * about NOT supporting any additional arguments. This method MUST be
450
     * overridden by any ViewHelper that desires this support and this inherited
451
     * method must not be called, obviously.
452
     *
453
     * @throws Exception
454
     * @param array $arguments
455
     * @return void
456
     */
457
    public function handleAdditionalArguments(array $arguments)
458
    {
459
    }
460
461
    /**
462
     * Default implementation of validating additional, undeclared arguments.
463
     * In this implementation the behavior is to consistently throw an error
464
     * about NOT supporting any additional arguments. This method MUST be
465
     * overridden by any ViewHelper that desires this support and this inherited
466
     * method must not be called, obviously.
467
     *
468
     * @throws Exception
469
     * @param array $arguments
470
     * @return void
471
     */
472
    public function validateAdditionalArguments(array $arguments)
473
    {
474
        if (!empty($arguments)) {
475
            throw new Exception(
476
                sprintf(
477
                    'Undeclared arguments passed to ViewHelper %s: %s',
478
                    get_class($this),
479
                    implode(', ', array_keys($arguments))
480
                )
481
            );
482
        }
483
    }
484
485
    /**
486
     * You only should override this method *when you absolutely know what you
487
     * are doing*, and really want to influence the generated PHP code during
488
     * template compilation directly.
489
     *
490
     * @param string $argumentsName
491
     * @param string $closureName
492
     * @param string $initializationPhpCode
493
     * @param ViewHelperNode $node
494
     * @param TemplateCompiler $compiler
495
     * @return string
496
     */
497
    public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler)
498
    {
499
        return sprintf(
500
            '%s::renderStatic(%s, %s, $renderingContext)',
501
            get_class($this),
502
            $argumentsName,
503
            $closureName
504
        );
505
    }
506
507
    /**
508
     * Default implementation of static rendering; useful API method if your ViewHelper
509
     * when compiled is able to render itself statically to increase performance. This
510
     * default implementation will simply delegate to the ViewHelperInvoker.
511
     *
512
     * @param array $arguments
513
     * @param \Closure $renderChildrenClosure
514
     * @param RenderingContextInterface $renderingContext
515
     * @return mixed
516
     */
517
    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
518
    {
519
        $viewHelperClassName = get_called_class();
520
        return $renderingContext->getViewHelperInvoker()->invoke($viewHelperClassName, $arguments, $renderingContext, $renderChildrenClosure);
521
    }
522
523
    /**
524
     * Save the associated ViewHelper node in a static public class variable.
525
     * called directly after the ViewHelper was built.
526
     *
527
     * @param ViewHelperNode $node
528
     * @param TextNode[] $arguments
529
     * @param VariableProviderInterface $variableContainer
530
     * @return void
531
     */
532
    public static function postParseEvent(ViewHelperNode $node, array $arguments, VariableProviderInterface $variableContainer)
533
    {
534
    }
535
536
    /**
537
     * Resets the ViewHelper state.
538
     *
539
     * Overwrite this method if you need to get a clean state of your ViewHelper.
540
     *
541
     * @return void
542
     */
543
    public function resetState()
544
    {
545
    }
546
}
547