Completed
Pull Request — master (#457)
by Claus
02:40
created

AbstractViewHelper::createArguments()   B

Complexity

Conditions 11
Paths 16

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 16
nop 1
dl 0
loc 23
rs 7.3166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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