Completed
Pull Request — master (#457)
by Claus
01:57
created

AbstractViewHelper::createArguments()   B

Complexity

Conditions 11
Paths 16

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 16
nop 1
dl 0
loc 24
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
        //$arguments = $this->parsedArguments;
177
        $definitions = $this->prepareArguments();
178
        $missingArguments = [];
179
        foreach ($definitions as $name => $definition) {
180
            $argument = &$arguments[$name] ?? null;
181
            if ($definition->isRequired() && !isset($argument)) {
182
                // Required but missing argument, causes failure (delayed, to report all missing arguments at once)
183
                $missingArguments[] = $name;
184
            } elseif (!isset($argument)) {
185
                // Argument is optional (required filtered out above), fit it with the default value
186
                $argument = $definition->getDefaultValue();
187
            } elseif (($type = $definition->getType()) && ($type === 'bool' || $type === 'boolean')) {
188
                // Cast the value or create a BooleanNode
189
                $argument = is_bool($argument) || is_numeric($argument) ? (bool)$argument : new BooleanNode($argument);
190
            }
191
            $arguments[$name] = $argument;
192
        }
193
        if (!empty($missingArguments)) {
194
            throw new \TYPO3Fluid\Fluid\Core\Parser\Exception('Required argument(s) not provided: ' . implode(', ', $missingArguments), 1558533510);
195
        }
196
        return $arguments;
197
    }
198
199
    /**
200
     * @param array $arguments
201
     * @return void
202
     */
203
    public function setArguments(array $arguments)
204
    {
205
        $this->arguments = $arguments;
206
    }
207
208
    /**
209
     * @param RenderingContextInterface $renderingContext
210
     * @return void
211
     */
212
    public function setRenderingContext(RenderingContextInterface $renderingContext)
213
    {
214
        $this->renderingContext = $renderingContext;
215
        $this->templateVariableContainer = $renderingContext->getVariableProvider();
216
        $this->viewHelperVariableContainer = $renderingContext->getViewHelperVariableContainer();
217
    }
218
219
    /**
220
     * Returns whether the escaping interceptors should be disabled or enabled for the result of renderChildren() calls within this ViewHelper
221
     *
222
     * Note: This method is no public API, use $this->escapeChildren instead!
223
     *
224
     * @return boolean
225
     */
226
    public function isChildrenEscapingEnabled()
227
    {
228
        if ($this->escapeChildren === null) {
229
            // Disable children escaping automatically, if output escaping is on anyway.
230
            return !$this->isOutputEscapingEnabled();
231
        }
232
        return $this->escapeChildren;
233
    }
234
235
    /**
236
     * Returns whether the escaping interceptors should be disabled or enabled for the render-result of this ViewHelper
237
     *
238
     * Note: This method is no public API, use $this->escapeChildren instead!
239
     *
240
     * @return boolean
241
     */
242
    public function isOutputEscapingEnabled()
243
    {
244
        return $this->escapeOutput !== false;
245
    }
246
247
    /**
248
     * Register a new argument. Call this method from your ViewHelper subclass
249
     * inside the initializeArguments() method.
250
     *
251
     * @param string $name Name of the argument
252
     * @param string $type Type of the argument
253
     * @param string $description Description of the argument
254
     * @param boolean $required If TRUE, argument is required. Defaults to FALSE.
255
     * @param mixed $defaultValue Default value of argument
256
     * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
257
     * @throws Exception
258
     * @api
259
     */
260 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...
261
    {
262
        if (array_key_exists($name, $this->argumentDefinitions)) {
263
            throw new Exception(
264
                'Argument "' . $name . '" has already been defined, thus it should not be defined again.',
265
                1253036401
266
            );
267
        }
268
        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue);
269
        return $this;
270
    }
271
272
    /**
273
     * Overrides a registered argument. Call this method from your ViewHelper subclass
274
     * inside the initializeArguments() method if you want to override a previously registered argument.
275
     * @see registerArgument()
276
     *
277
     * @param string $name Name of the argument
278
     * @param string $type Type of the argument
279
     * @param string $description Description of the argument
280
     * @param boolean $required If TRUE, argument is required. Defaults to FALSE.
281
     * @param mixed $defaultValue Default value of argument
282
     * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
283
     * @throws Exception
284
     * @api
285
     */
286 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...
287
    {
288
        if (!array_key_exists($name, $this->argumentDefinitions)) {
289
            throw new Exception(
290
                'Argument "' . $name . '" has not been defined, thus it can\'t be overridden.',
291
                1279212461
292
            );
293
        }
294
        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue);
295
        return $this;
296
    }
297
298
    /**
299
     * Sets all needed attributes needed for the rendering. Called by the
300
     * framework. Populates $this->viewHelperNode.
301
     * This is PURELY INTERNAL! Never override this method!!
302
     *
303
     * @param ViewHelperNode $node View Helper node to be set.
304
     * @return void
305
     */
306
    public function setViewHelperNode(ViewHelperNode $node)
307
    {
308
        $this->viewHelperNode = $node;
309
    }
310
311
    /**
312
     * Sets all needed attributes needed for the rendering. Called by the
313
     * framework. Populates $this->viewHelperNode.
314
     * This is PURELY INTERNAL! Never override this method!!
315
     *
316
     * @param NodeInterface[] $childNodes
317
     * @return void
318
     */
319
    public function setChildNodes(array $childNodes)
320
    {
321
        $this->childNodes = $childNodes;
322
    }
323
324
    /**
325
     * Called when being inside a cached template.
326
     *
327
     * @param \Closure $renderChildrenClosure
328
     * @return void
329
     */
330
    public function setRenderChildrenClosure(\Closure $renderChildrenClosure)
331
    {
332
        $this->renderChildrenClosure = $renderChildrenClosure;
333
    }
334
335
    /**
336
     * Initialize the arguments of the ViewHelper, and call the render() method of the ViewHelper.
337
     *
338
     * @return string the rendered ViewHelper.
339
     */
340
    public function initializeArgumentsAndRender()
341
    {
342
        $this->validateArguments();
343
        $this->initialize();
344
345
        return $this->callRenderMethod();
346
    }
347
348
    /**
349
     * Call the render() method and handle errors.
350
     *
351
     * @return string the rendered ViewHelper
352
     * @throws Exception
353
     */
354
    protected function callRenderMethod()
355
    {
356
        if (method_exists($this, 'render')) {
357
            return call_user_func([$this, 'render']);
358
        }
359
        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...
360
            // Method is safe to call - will not recurse through ViewHelperInvoker via the default
361
            // implementation of renderStatic() on this class.
362
            return static::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext);
363
        }
364
        throw new Exception(
365
            sprintf(
366
                'ViewHelper class "%s" does not declare a "render()" method and inherits the default "renderStatic". ' .
367
                'Exceuting this ViewHelper would cause infinite recursion - please either implement "render()" or ' .
368
                '"renderStatic()" on your ViewHelper class',
369
                get_class($this)
370
            )
371
        );
372
    }
373
374
    /**
375
     * Initializes the view helper before invoking the render method.
376
     *
377
     * Override this method to solve tasks before the view helper content is rendered.
378
     *
379
     * @return void
380
     * @api
381
     */
382
    public function initialize()
383
    {
384
    }
385
386
    /**
387
     * Helper method which triggers the rendering of everything between the
388
     * opening and the closing tag.
389
     *
390
     * @return mixed The finally rendered child nodes.
391
     * @api
392
     */
393
    public function renderChildren()
394
    {
395
        if ($this->renderChildrenClosure !== null) {
396
            $closure = $this->renderChildrenClosure;
397
            return $closure();
398
        }
399
        return ($this->viewHelperNode ?? $this)->evaluateChildNodes($this->renderingContext);
400
    }
401
402
    /**
403
     * Helper which is mostly needed when calling renderStatic() from within
404
     * render().
405
     *
406
     * No public API yet.
407
     *
408
     * @return \Closure
409
     */
410
    protected function buildRenderChildrenClosure()
411
    {
412
        $self = clone $this;
413
        return function() use ($self) {
414
            return $self->renderChildren();
415
        };
416
    }
417
418
    /**
419
     * Initialize all arguments and return them
420
     *
421
     * @return ArgumentDefinition[]
422
     */
423
    public function prepareArguments()
424
    {
425
        $thisClassName = get_class($this);
426
        if (isset(self::$argumentDefinitionCache[$thisClassName])) {
427
            $this->argumentDefinitions = self::$argumentDefinitionCache[$thisClassName];
428
        } else {
429
            $this->initializeArguments();
430
            self::$argumentDefinitionCache[$thisClassName] = $this->argumentDefinitions;
431
        }
432
        return $this->argumentDefinitions;
433
    }
434
435
    /**
436
     * Validate arguments, and throw exception if arguments do not validate.
437
     *
438
     * @return void
439
     * @throws \InvalidArgumentException
440
     */
441
    public function validateArguments()
442
    {
443
        $argumentDefinitions = $this->prepareArguments();
444
        foreach ($argumentDefinitions as $argumentName => $registeredArgument) {
445
            if ($this->hasArgument($argumentName)) {
446
                $value = $this->arguments[$argumentName];
447
                $type = $registeredArgument->getType();
448
                if ($value !== $registeredArgument->getDefaultValue() && $type !== 'mixed') {
449
                    $givenType = is_object($value) ? get_class($value) : gettype($value);
450
                    if (!$this->isValidType($type, $value)) {
451
                        throw new \InvalidArgumentException(
452
                            'The argument "' . $argumentName . '" was registered with type "' . $type . '", but is of type "' .
453
                            $givenType . '" in view helper "' . get_class($this) . '". Value: ' . var_export($value, true),
454
                            1256475113
455
                        );
456
                    }
457
                }
458
            }
459
        }
460
    }
461
462
    /**
463
     * Check whether the defined type matches the value type
464
     *
465
     * @param string $type
466
     * @param mixed $value
467
     * @return boolean
468
     */
469
    protected function isValidType($type, $value)
470
    {
471
        if ($type === 'object') {
472
            if (!is_object($value)) {
473
                return false;
474
            }
475
        } elseif ($type === 'array' || substr($type, -2) === '[]') {
476
            if (!is_array($value) && !$value instanceof \ArrayAccess && !$value instanceof \Traversable && !empty($value)) {
477
                return false;
478
            } elseif (substr($type, -2) === '[]') {
479
                $firstElement = $this->getFirstElementOfNonEmpty($value);
480
                if ($firstElement === null) {
481
                    return true;
482
                }
483
                return $this->isValidType(substr($type, 0, -2), $firstElement);
484
            }
485
        } elseif ($type === 'string') {
486
            if (is_object($value) && !method_exists($value, '__toString')) {
487
                return false;
488
            }
489
        } elseif ($type === 'boolean' && !is_bool($value)) {
490
            return false;
491
        } elseif (class_exists($type) && $value !== null && !$value instanceof $type) {
492
            return false;
493
        } elseif (is_object($value) && !is_a($value, $type, true)) {
494
            return false;
495
        }
496
        return true;
497
    }
498
499
    /**
500
     * Return the first element of the given array, ArrayAccess or Traversable
501
     * that is not empty
502
     *
503
     * @param mixed $value
504
     * @return mixed
505
     */
506
    protected function getFirstElementOfNonEmpty($value)
507
    {
508
        if (is_array($value)) {
509
            return reset($value);
510
        } elseif ($value instanceof \Traversable) {
511
            foreach ($value as $element) {
512
                return $element;
513
            }
514
        }
515
        return null;
516
    }
517
518
    /**
519
     * Initialize all arguments. You need to override this method and call
520
     * $this->registerArgument(...) inside this method, to register all your arguments.
521
     *
522
     * @return void
523
     * @api
524
     */
525
    public function initializeArguments()
526
    {
527
    }
528
529
    /**
530
     * Tests if the given $argumentName is set, and not NULL.
531
     * The isset() test used fills both those requirements.
532
     *
533
     * @param string $argumentName
534
     * @return boolean TRUE if $argumentName is found, FALSE otherwise
535
     * @api
536
     */
537
    protected function hasArgument($argumentName)
538
    {
539
        return isset($this->arguments[$argumentName]);
540
    }
541
542
    /**
543
     * Default implementation of "handling" additional, undeclared arguments.
544
     * In this implementation the behavior is to consistently throw an error
545
     * about NOT supporting any additional arguments. This method MUST be
546
     * overridden by any ViewHelper that desires this support and this inherited
547
     * method must not be called, obviously.
548
     *
549
     * @throws Exception
550
     * @param array $arguments
551
     * @return void
552
     */
553
    public function handleAdditionalArguments(array $arguments)
554
    {
555
    }
556
557
    /**
558
     * Default implementation of validating additional, undeclared arguments.
559
     * In this implementation the behavior is to consistently throw an error
560
     * about NOT supporting any additional arguments. This method MUST be
561
     * overridden by any ViewHelper that desires this support and this inherited
562
     * method must not be called, obviously.
563
     *
564
     * @throws Exception
565
     * @param array $arguments
566
     * @return void
567
     */
568
    public function validateAdditionalArguments(array $arguments)
569
    {
570
        if (!empty($arguments)) {
571
            throw new Exception(
572
                sprintf(
573
                    'Undeclared arguments passed to ViewHelper %s: %s. Valid arguments are: %s',
574
                    get_class($this),
575
                    implode(', ', array_keys($arguments)),
576
                    implode(', ', array_keys($this->argumentDefinitions))
577
                )
578
            );
579
        }
580
    }
581
582
    /**
583
     * You only should override this method *when you absolutely know what you
584
     * are doing*, and really want to influence the generated PHP code during
585
     * template compilation directly.
586
     *
587
     * @param string $argumentsName
588
     * @param string $closureName
589
     * @param string $initializationPhpCode
590
     * @param ViewHelperNode $node
591
     * @param TemplateCompiler $compiler
592
     * @return string
593
     */
594
    public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler)
595
    {
596
        return sprintf(
597
            '%s::renderStatic(%s, %s, $renderingContext)',
598
            get_class($this),
599
            $argumentsName,
600
            $closureName
601
        );
602
    }
603
604
    /**
605
     * Default implementation of static rendering; useful API method if your ViewHelper
606
     * when compiled is able to render itself statically to increase performance. This
607
     * default implementation will simply delegate to the ViewHelperInvoker.
608
     *
609
     * @param array $arguments
610
     * @param \Closure $renderChildrenClosure
611
     * @param RenderingContextInterface $renderingContext
612
     * @return mixed
613
     */
614
    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
615
    {
616
        $viewHelperClassName = get_called_class();
617
        return $renderingContext->getViewHelperInvoker()->invoke($viewHelperClassName, $arguments, $renderingContext, $renderChildrenClosure);
618
    }
619
620
    /**
621
     * Save the associated ViewHelper node in a static public class variable.
622
     * called directly after the ViewHelper was built.
623
     *
624
     * @param ViewHelperNode $node
625
     * @param TextNode[] $arguments
626
     * @param VariableProviderInterface $variableContainer
627
     * @return void
628
     */
629
    public static function postParseEvent(ViewHelperNode $node, array $arguments, VariableProviderInterface $variableContainer)
630
    {
631
    }
632
633
    /**
634
     * Resets the ViewHelper state.
635
     *
636
     * Overwrite this method if you need to get a clean state of your ViewHelper.
637
     *
638
     * @return void
639
     */
640
    public function resetState()
641
    {
642
    }
643
}
644