Completed
Pull Request — master (#404)
by Claus
02:56 queued 48s
created

AbstractViewHelper::resolveContentArgumentName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
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\Compiler\ViewHelperCompiler;
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
     * Content argument identification. Override this property and set value to the name of an argument, then calling
107
     * $renderChildrenClosure() or $this->renderChildren() will return the value of that argument if the argument was
108
     * not passed as explicit argument. Allows one argument to become a "content argument" that facilitates passing
109
     * with inline syntax pipe/pass or as traditional ViewHelper argument, without needing to do any special checks
110
     * in your ViewHelper class.
111
     *
112
     * @var string
113
     */
114
    protected $contentArgumentName = null;
115
116
    /**
117
     * @param array $arguments
118
     * @return void
119
     */
120
    public function setArguments(array $arguments)
121
    {
122
        $this->arguments = $arguments;
123
    }
124
125
    /**
126
     * @param RenderingContextInterface $renderingContext
127
     * @return void
128
     */
129
    public function setRenderingContext(RenderingContextInterface $renderingContext)
130
    {
131
        $this->renderingContext = $renderingContext;
132
        $this->templateVariableContainer = $renderingContext->getVariableProvider();
133
        $this->viewHelperVariableContainer = $renderingContext->getViewHelperVariableContainer();
134
    }
135
136
    /**
137
     * Returns whether the escaping interceptors should be disabled or enabled for the result of renderChildren() calls within this ViewHelper
138
     *
139
     * Note: This method is no public API, use $this->escapeChildren instead!
140
     *
141
     * @return boolean
142
     */
143
    public function isChildrenEscapingEnabled()
144
    {
145
        if ($this->escapeChildren === null) {
146
            // Disable children escaping automatically, if output escaping is on anyway.
147
            return !$this->isOutputEscapingEnabled();
148
        }
149
        return $this->escapeChildren;
150
    }
151
152
    /**
153
     * Returns whether the escaping interceptors should be disabled or enabled for the render-result of this ViewHelper
154
     *
155
     * Note: This method is no public API, use $this->escapeChildren instead!
156
     *
157
     * @return boolean
158
     */
159
    public function isOutputEscapingEnabled()
160
    {
161
        return $this->escapeOutput !== false;
162
    }
163
164
    /**
165
     * Register a new argument. Call this method from your ViewHelper subclass
166
     * inside the initializeArguments() method.
167
     *
168
     * @param string $name Name of the argument
169
     * @param string $type Type of the argument
170
     * @param string $description Description of the argument
171
     * @param boolean $required If TRUE, argument is required. Defaults to FALSE.
172
     * @param mixed $defaultValue Default value of argument
173
     * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
174
     * @throws Exception
175
     * @api
176
     */
177 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...
178
    {
179
        if (array_key_exists($name, $this->argumentDefinitions)) {
180
            throw new Exception(
181
                'Argument "' . $name . '" has already been defined, thus it should not be defined again.',
182
                1253036401
183
            );
184
        }
185
        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue);
186
        return $this;
187
    }
188
189
    /**
190
     * Overrides a registered argument. Call this method from your ViewHelper subclass
191
     * inside the initializeArguments() method if you want to override a previously registered argument.
192
     * @see registerArgument()
193
     *
194
     * @param string $name Name of the argument
195
     * @param string $type Type of the argument
196
     * @param string $description Description of the argument
197
     * @param boolean $required If TRUE, argument is required. Defaults to FALSE.
198
     * @param mixed $defaultValue Default value of argument
199
     * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
200
     * @throws Exception
201
     * @api
202
     */
203 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...
204
    {
205
        if (!array_key_exists($name, $this->argumentDefinitions)) {
206
            throw new Exception(
207
                'Argument "' . $name . '" has not been defined, thus it can\'t be overridden.',
208
                1279212461
209
            );
210
        }
211
        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue);
212
        return $this;
213
    }
214
215
    /**
216
     * Sets all needed attributes needed for the rendering. Called by the
217
     * framework. Populates $this->viewHelperNode.
218
     * This is PURELY INTERNAL! Never override this method!!
219
     *
220
     * @param ViewHelperNode $node View Helper node to be set.
221
     * @return void
222
     */
223
    public function setViewHelperNode(ViewHelperNode $node)
224
    {
225
        $this->viewHelperNode = $node;
226
    }
227
228
    /**
229
     * Sets all needed attributes needed for the rendering. Called by the
230
     * framework. Populates $this->viewHelperNode.
231
     * This is PURELY INTERNAL! Never override this method!!
232
     *
233
     * @param NodeInterface[] $childNodes
234
     * @return void
235
     */
236
    public function setChildNodes(array $childNodes)
237
    {
238
        $this->childNodes = $childNodes;
239
    }
240
241
    /**
242
     * Called when being inside a cached template.
243
     *
244
     * @param \Closure $renderChildrenClosure
245
     * @return void
246
     */
247
    public function setRenderChildrenClosure(\Closure $renderChildrenClosure)
248
    {
249
        $this->renderChildrenClosure = $renderChildrenClosure;
250
    }
251
252
    /**
253
     * Initialize the arguments of the ViewHelper, and call the render() method of the ViewHelper.
254
     *
255
     * @return string the rendered ViewHelper.
256
     */
257
    public function initializeArgumentsAndRender()
258
    {
259
        $this->validateArguments();
260
        $this->initialize();
261
262
        return $this->callRenderMethod();
263
    }
264
265
    /**
266
     * Call the render() method and handle errors.
267
     *
268
     * @return string the rendered ViewHelper
269
     * @throws Exception
270
     */
271
    protected function callRenderMethod()
272
    {
273
        if (method_exists($this, 'render')) {
274
            return call_user_func([$this, 'render']);
275
        }
276
        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...
277
            // Method is safe to call - will not recurse through ViewHelperInvoker via the default
278
            // implementation of renderStatic() on this class.
279
            return static::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext);
280
        }
281
        throw new Exception(
282
            sprintf(
283
                'ViewHelper class "%s" does not declare a "render()" method and inherits the default "renderStatic". ' .
284
                'Exceuting this ViewHelper would cause infinite recursion - please either implement "render()" or ' .
285
                '"renderStatic()" on your ViewHelper class',
286
                get_class($this)
287
            )
288
        );
289
    }
290
291
    /**
292
     * Initializes the view helper before invoking the render method.
293
     *
294
     * Override this method to solve tasks before the view helper content is rendered.
295
     *
296
     * @return void
297
     * @api
298
     */
299
    public function initialize()
300
    {
301
    }
302
303
    /**
304
     * Helper method which triggers the rendering of everything between the
305
     * opening and the closing tag.
306
     *
307
     * @return mixed The finally rendered child nodes.
308
     * @api
309
     */
310
    public function renderChildren()
311
    {
312
        if ($this->renderChildrenClosure !== null) {
313
            $closure = $this->renderChildrenClosure;
314
            return $closure();
315
        }
316
        return $this->viewHelperNode->evaluateChildNodes($this->renderingContext);
317
    }
318
319
    /**
320
     * Creates a closure that will render either the tag content or inline-passed value
321
     * received by the ViewHelper - or returns the value of the "content argument" if
322
     * this is implemented in the specific ViewHelper class.
323
     *
324
     * @return \Closure
325
     */
326
    protected function buildRenderChildrenClosure()
327
    {
328
        $argumentName = $this->resolveContentArgumentName();
329
        $arguments = $this->arguments;
330
        if (!empty($argumentName) && isset($arguments[$argumentName])) {
331
            $renderChildrenClosure = function () use ($arguments, $argumentName) {
332
                return $arguments[$argumentName];
333
            };
334
        } else {
335
            $self = clone $this;
336
            $renderChildrenClosure = function () use ($self) {
337
                return $self->renderChildren();
338
            };
339
        }
340
        return $renderChildrenClosure;
341
    }
342
343
    /**
344
     * Initialize all arguments and return them
345
     *
346
     * @return ArgumentDefinition[]
347
     */
348
    public function prepareArguments()
349
    {
350
        $thisClassName = get_class($this);
351
        if (isset(self::$argumentDefinitionCache[$thisClassName])) {
352
            $this->argumentDefinitions = self::$argumentDefinitionCache[$thisClassName];
353
        } else {
354
            $this->initializeArguments();
355
            self::$argumentDefinitionCache[$thisClassName] = $this->argumentDefinitions;
356
        }
357
        return $this->argumentDefinitions;
358
    }
359
360
    /**
361
     * Validate arguments, and throw exception if arguments do not validate.
362
     *
363
     * @return void
364
     * @throws \InvalidArgumentException
365
     */
366
    public function validateArguments()
367
    {
368
        $argumentDefinitions = $this->prepareArguments();
369
        foreach ($argumentDefinitions as $argumentName => $registeredArgument) {
370
            if ($this->hasArgument($argumentName)) {
371
                $value = $this->arguments[$argumentName];
372
                $type = $registeredArgument->getType();
373
                if ($value !== $registeredArgument->getDefaultValue() && $type !== 'mixed') {
374
                    $givenType = is_object($value) ? get_class($value) : gettype($value);
375
                    if (!$this->isValidType($type, $value)) {
376
                        throw new \InvalidArgumentException(
377
                            'The argument "' . $argumentName . '" was registered with type "' . $type . '", but is of type "' .
378
                            $givenType . '" in view helper "' . get_class($this) . '".',
379
                            1256475113
380
                        );
381
                    }
382
                }
383
            }
384
        }
385
    }
386
387
    /**
388
     * Implemented for plug-and-play compatibility with deprecated trait that used to provide
389
     * content argument support for ViewHelpers.
390
     *
391
     * @return string
392
     */
393
    protected function resolveContentArgumentName()
394
    {
395
        return $this->contentArgumentName;
396
    }
397
398
    /**
399
     * Check whether the defined type matches the value type
400
     *
401
     * @param string $type
402
     * @param mixed $value
403
     * @return boolean
404
     */
405
    protected function isValidType($type, $value)
406
    {
407
        if ($type === 'object') {
408
            if (!is_object($value)) {
409
                return false;
410
            }
411
        } elseif ($type === 'array' || substr($type, -2) === '[]') {
412
            if (!is_array($value) && !$value instanceof \ArrayAccess && !$value instanceof \Traversable && !empty($value)) {
413
                return false;
414
            } elseif (substr($type, -2) === '[]') {
415
                $firstElement = $this->getFirstElementOfNonEmpty($value);
416
                if ($firstElement === null) {
417
                    return true;
418
                }
419
                return $this->isValidType(substr($type, 0, -2), $firstElement);
420
            }
421
        } elseif ($type === 'string') {
422
            if (is_object($value) && !method_exists($value, '__toString')) {
423
                return false;
424
            }
425
        } elseif ($type === 'boolean' && !is_bool($value)) {
426
            return false;
427
        } elseif (class_exists($type) && $value !== null && !$value instanceof $type) {
428
            return false;
429
        } elseif (is_object($value) && !is_a($value, $type, true)) {
430
            return false;
431
        }
432
        return true;
433
    }
434
435
    /**
436
     * Return the first element of the given array, ArrayAccess or Traversable
437
     * that is not empty
438
     *
439
     * @param mixed $value
440
     * @return mixed
441
     */
442
    protected function getFirstElementOfNonEmpty($value)
443
    {
444
        if (is_array($value)) {
445
            return reset($value);
446
        } elseif ($value instanceof \Traversable) {
447
            foreach ($value as $element) {
448
                return $element;
449
            }
450
        }
451
        return null;
452
    }
453
454
    /**
455
     * Initialize all arguments. You need to override this method and call
456
     * $this->registerArgument(...) inside this method, to register all your arguments.
457
     *
458
     * @return void
459
     * @api
460
     */
461
    public function initializeArguments()
462
    {
463
    }
464
465
    /**
466
     * Tests if the given $argumentName is set, and not NULL.
467
     * The isset() test used fills both those requirements.
468
     *
469
     * @param string $argumentName
470
     * @return boolean TRUE if $argumentName is found, FALSE otherwise
471
     * @api
472
     */
473
    protected function hasArgument($argumentName)
474
    {
475
        return isset($this->arguments[$argumentName]);
476
    }
477
478
    /**
479
     * Default implementation of "handling" additional, undeclared arguments.
480
     * In this implementation the behavior is to consistently throw an error
481
     * about NOT supporting any additional arguments. This method MUST be
482
     * overridden by any ViewHelper that desires this support and this inherited
483
     * method must not be called, obviously.
484
     *
485
     * @throws Exception
486
     * @param array $arguments
487
     * @return void
488
     */
489
    public function handleAdditionalArguments(array $arguments)
490
    {
491
    }
492
493
    /**
494
     * Default implementation of validating additional, undeclared arguments.
495
     * In this implementation the behavior is to consistently throw an error
496
     * about NOT supporting any additional arguments. This method MUST be
497
     * overridden by any ViewHelper that desires this support and this inherited
498
     * method must not be called, obviously.
499
     *
500
     * @throws Exception
501
     * @param array $arguments
502
     * @return void
503
     */
504
    public function validateAdditionalArguments(array $arguments)
505
    {
506
        if (!empty($arguments)) {
507
            throw new Exception(
508
                sprintf(
509
                    'Undeclared arguments passed to ViewHelper %s: %s. Valid arguments are: %s',
510
                    get_class($this),
511
                    implode(', ', array_keys($arguments)),
512
                    implode(', ', array_keys($this->argumentDefinitions))
513
                )
514
            );
515
        }
516
    }
517
518
    /**
519
     * @param string $argumentsName
520
     * @param string $closureName
521
     * @param string $initializationPhpCode
522
     * @param ViewHelperNode $node
523
     * @param TemplateCompiler $compiler
524
     * @return string
525
     */
526
    public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler)
527
    {
528
        list ($initialization, $execution) = ViewHelperCompiler::getInstance()->compileWithCallToStaticMethod(
529
            $this,
530
            $argumentsName,
531
            $closureName,
532
            ViewHelperCompiler::RENDER_STATIC,
533
            get_class($this)
534
        );
535
536
        $contentArgumentName = $this->resolveContentArgumentName();
537
        if (empty($contentArgumentName)) {
538
            $initializationPhpCode .= $initialization;
539
            return $execution;
540
        }
541
542
        $initializationPhpCode .= sprintf(
543
            '%s = (%s[\'%s\'] !== null) ? function() use (%s) { return %s[\'%s\']; } : %s;',
544
            $closureName,
545
            $argumentsName,
546
            $contentArgumentName,
547
            $argumentsName,
548
            $argumentsName,
549
            $contentArgumentName,
550
            $closureName
551
        );
552
        $initializationPhpCode .= $initialization;
553
        return $execution;
554
    }
555
556
    /**
557
     * Default implementation of static rendering; useful API method if your ViewHelper
558
     * when compiled is able to render itself statically to increase performance. This
559
     * default implementation will simply delegate to the ViewHelperInvoker.
560
     *
561
     * @param array $arguments
562
     * @param \Closure $renderChildrenClosure
563
     * @param RenderingContextInterface $renderingContext
564
     * @return mixed
565
     */
566
    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
567
    {
568
        $viewHelperClassName = get_called_class();
569
        return $renderingContext->getViewHelperInvoker()->invoke($viewHelperClassName, $arguments, $renderingContext, $renderChildrenClosure);
570
    }
571
572
    /**
573
     * Save the associated ViewHelper node in a static public class variable.
574
     * called directly after the ViewHelper was built.
575
     *
576
     * @param ViewHelperNode $node
577
     * @param TextNode[] $arguments
578
     * @param VariableProviderInterface $variableContainer
579
     * @return void
580
     */
581
    public static function postParseEvent(ViewHelperNode $node, array $arguments, VariableProviderInterface $variableContainer)
582
    {
583
    }
584
585
    /**
586
     * Resets the ViewHelper state.
587
     *
588
     * Overwrite this method if you need to get a clean state of your ViewHelper.
589
     *
590
     * @return void
591
     */
592
    public function resetState()
593
    {
594
    }
595
}
596