Completed
Push — master ( 27b195...cc5c3a )
by Claus
01:37
created

TemplateParser::recursiveArrayHandler()   D

Complexity

Conditions 20
Paths 8

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
nc 8
nop 3
dl 0
loc 53
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    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\Parser;
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\StopCompilingException;
10
use TYPO3Fluid\Fluid\Core\Compiler\UncompilableTemplateInterface;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionException;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
15
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ParseTimeEvaluatedExpressionNodeInterface;
16
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
17
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NumericNode;
18
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
19
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
20
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
21
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
22
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
23
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
24
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface;
25
26
/**
27
 * Template parser building up an object syntax tree
28
 */
29
class TemplateParser
30
{
31
32
    /**
33
     * The following two constants are used for tracking whether we are currently
34
     * parsing ViewHelper arguments or not. This is used to parse arrays only as
35
     * ViewHelper argument.
36
     */
37
    const CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS = 1;
38
    const CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS = 2;
39
40
    /**
41
     * Whether or not the escaping interceptors are active
42
     *
43
     * @var boolean
44
     */
45
    protected $escapingEnabled = true;
46
47
    /**
48
     * @var Configuration
49
     */
50
    protected $configuration;
51
52
    /**
53
     * @var array
54
     */
55
    protected $settings;
56
57
    /**
58
     * @var RenderingContextInterface
59
     */
60
    protected $renderingContext;
61
62
    /**
63
     * @var integer
64
     */
65
    protected $pointerLineNumber = 1;
66
67
    /**
68
     * @var integer
69
     */
70
    protected $pointerLineCharacter = 1;
71
72
    /**
73
     * @var string
74
     */
75
    protected $pointerTemplateCode = null;
76
77
    /**
78
     * @var ParsedTemplateInterface[]
79
     */
80
    protected $parsedTemplates = [];
81
82
    /**
83
     * @param RenderingContextInterface $renderingContext
84
     * @return void
85
     */
86
    public function setRenderingContext(RenderingContextInterface $renderingContext)
87
    {
88
        $this->renderingContext = $renderingContext;
89
        $this->configuration = $renderingContext->buildParserConfiguration();
90
    }
91
92
    /**
93
     * Returns an array of current line number, character in line and reference template code;
94
     * for extraction when catching parser-related Exceptions during parsing.
95
     *
96
     * @return array
97
     */
98
    public function getCurrentParsingPointers()
99
    {
100
        return [$this->pointerLineNumber, $this->pointerLineCharacter, $this->pointerTemplateCode];
101
    }
102
103
    /**
104
     * @return boolean
105
     */
106
    public function isEscapingEnabled()
107
    {
108
        return $this->escapingEnabled;
109
    }
110
111
    /**
112
     * @param boolean $escapingEnabled
113
     * @return void
114
     */
115
    public function setEscapingEnabled($escapingEnabled)
116
    {
117
        $this->escapingEnabled = (boolean) $escapingEnabled;
118
    }
119
120
    /**
121
     * Parses a given template string and returns a parsed template object.
122
     *
123
     * The resulting ParsedTemplate can then be rendered by calling evaluate() on it.
124
     *
125
     * Normally, you should use a subclass of AbstractTemplateView instead of calling the
126
     * TemplateParser directly.
127
     *
128
     * @param string $templateString The template to parse as a string
129
     * @param string|null $templateIdentifier If the template has an identifying string it can be passed here to improve error reporting.
130
     * @return ParsingState Parsed template
131
     * @throws Exception
132
     */
133
    public function parse($templateString, $templateIdentifier = null)
134
    {
135
        if (!is_string($templateString)) {
136
            throw new Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
137
        }
138
        try {
139
            $this->reset();
140
141
            $templateString = $this->preProcessTemplateSource($templateString);
142
143
            $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
144
            $parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
145
        } catch (Exception $error) {
146
            throw $this->createParsingRelatedExceptionWithContext($error, $templateIdentifier);
147
        }
148
        $this->parsedTemplates[$templateIdentifier] = $parsingState;
149
        return $parsingState;
150
    }
151
152
    /**
153
     * @param \Exception $error
154
     * @param string $templateIdentifier
155
     * @throws \Exception
156
     */
157
    public function createParsingRelatedExceptionWithContext(\Exception $error, $templateIdentifier)
158
    {
159
        list ($line, $character, $templateCode) = $this->getCurrentParsingPointers();
160
        $exceptionClass = get_class($error);
161
        return new $exceptionClass(
162
            sprintf(
163
                'Fluid parse error in template %s, line %d at character %d. Error: %s (error code %d). Template source chunk: %s',
164
                $templateIdentifier,
165
                $line,
166
                $character,
167
                $error->getMessage(),
168
                $error->getCode(),
169
                $templateCode
170
            ),
171
            $error->getCode(),
172
            $error
173
        );
174
    }
175
176
    /**
177
     * @param string $templateIdentifier
178
     * @param \Closure $templateSourceClosure Closure which returns the template source if needed
179
     * @return ParsedTemplateInterface
180
     */
181
    public function getOrParseAndStoreTemplate($templateIdentifier, $templateSourceClosure)
182
    {
183
        $compiler = $this->renderingContext->getTemplateCompiler();
184
        if (isset($this->parsedTemplates[$templateIdentifier])) {
185
            $parsedTemplate = $this->parsedTemplates[$templateIdentifier];
186
        } elseif ($compiler->has($templateIdentifier)) {
187
            $parsedTemplate = $compiler->get($templateIdentifier);
188
            if ($parsedTemplate instanceof UncompilableTemplateInterface) {
189
                $parsedTemplate = $this->parseTemplateSource($templateIdentifier, $templateSourceClosure);
190
            }
191
        } else {
192
            $parsedTemplate = $this->parseTemplateSource($templateIdentifier, $templateSourceClosure);
193
            try {
194
                $compiler->store($templateIdentifier, $parsedTemplate);
0 ignored issues
show
Compatibility introduced by
$parsedTemplate of type object<TYPO3Fluid\Fluid\...arsedTemplateInterface> is not a sub-type of object<TYPO3Fluid\Fluid\Core\Parser\ParsingState>. It seems like you assume a concrete implementation of the interface TYPO3Fluid\Fluid\Core\Pa...ParsedTemplateInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
195
            } catch (StopCompilingException $stop) {
196
                $this->renderingContext->getErrorHandler()->handleCompilerError($stop);
197
                $parsedTemplate->setCompilable(false);
198
                $compiler->store($templateIdentifier, $parsedTemplate);
0 ignored issues
show
Compatibility introduced by
$parsedTemplate of type object<TYPO3Fluid\Fluid\...arsedTemplateInterface> is not a sub-type of object<TYPO3Fluid\Fluid\Core\Parser\ParsingState>. It seems like you assume a concrete implementation of the interface TYPO3Fluid\Fluid\Core\Pa...ParsedTemplateInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
199
            }
200
        }
201
        return $parsedTemplate;
202
    }
203
204
    /**
205
     * @param string $templateIdentifier
206
     * @param \Closure $templateSourceClosure
207
     * @return ParsedTemplateInterface
208
     */
209
    protected function parseTemplateSource($templateIdentifier, $templateSourceClosure)
210
    {
211
        $parsedTemplate = $this->parse(
212
            $templateSourceClosure($this, $this->renderingContext->getTemplatePaths()),
213
            $templateIdentifier
214
        );
215
        $parsedTemplate->setIdentifier($templateIdentifier);
216
        $this->parsedTemplates[$templateIdentifier] = $parsedTemplate;
217
        return $parsedTemplate;
218
    }
219
220
    /**
221
     * Pre-process the template source, making all registered TemplateProcessors
222
     * do what they need to do with the template source before it is parsed.
223
     *
224
     * @param string $templateSource
225
     * @return string
226
     */
227
    protected function preProcessTemplateSource($templateSource)
228
    {
229
        foreach ($this->renderingContext->getTemplateProcessors() as $templateProcessor) {
230
            $templateSource = $templateProcessor->preProcessSource($templateSource);
231
        }
232
        return $templateSource;
233
    }
234
235
    /**
236
     * Resets the parser to its default values.
237
     *
238
     * @return void
239
     */
240
    protected function reset()
241
    {
242
        $this->escapingEnabled = true;
243
        $this->pointerLineNumber = 1;
244
        $this->pointerLineCharacter = 1;
245
    }
246
247
    /**
248
     * Splits the template string on all dynamic tags found.
249
     *
250
     * @param string $templateString Template string to split.
251
     * @return array Splitted template
252
     */
253
    protected function splitTemplateAtDynamicTags($templateString)
254
    {
255
        return preg_split(Patterns::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
256
    }
257
258
    /**
259
     * Build object tree from the split template
260
     *
261
     * @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a seperate array element.
262
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
263
     * @return ParsingState
264
     * @throws Exception
265
     */
266
    protected function buildObjectTree(array $splitTemplate, $context)
267
    {
268
        $state = $this->getParsingState();
269
        $previousBlock = '';
270
271
        foreach ($splitTemplate as $templateElement) {
272
            if ($context === self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS) {
273
                // Store a neat reference to the outermost chunk of Fluid template code.
274
                // Don't store the reference if parsing ViewHelper arguments object tree;
275
                // we want the reference code to contain *all* of the ViewHelper call.
276
                $this->pointerTemplateCode = $templateElement;
277
            }
278
            $this->pointerLineNumber += substr_count($templateElement, PHP_EOL);
279
            $this->pointerLineCharacter = strlen(substr($previousBlock, strrpos($previousBlock, PHP_EOL))) + 1;
280
            $previousBlock = $templateElement;
281
            $matchedVariables = [];
282
283
            if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
284
                try {
285
                    if ($this->openingViewHelperTagHandler(
286
                        $state,
287
                        $matchedVariables['NamespaceIdentifier'],
288
                        $matchedVariables['MethodIdentifier'],
289
                        $matchedVariables['Attributes'],
290
                        ($matchedVariables['Selfclosing'] === '' ? false : true),
291
                        $templateElement
292
                    )) {
293
                        continue;
294
                    }
295
                } catch (\TYPO3Fluid\Fluid\Core\ViewHelper\Exception $error) {
296
                    $this->textHandler(
297
                        $state,
298
                        $this->renderingContext->getErrorHandler()->handleViewHelperError($error)
299
                    );
300
                } catch (Exception $error) {
301
                    $this->textHandler(
302
                        $state,
303
                        $this->renderingContext->getErrorHandler()->handleParserError($error)
304
                    );
305
                }
306
            } elseif (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
307
                if ($this->closingViewHelperTagHandler(
308
                    $state,
309
                    $matchedVariables['NamespaceIdentifier'],
310
                    $matchedVariables['MethodIdentifier']
311
                )) {
312
                    continue;
313
                }
314
            }
315
            $this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
316
        }
317
318
        if ($state->countNodeStack() !== 1) {
319
            throw new Exception(
320
                'Not all tags were closed!',
321
                1238169398
322
            );
323
        }
324
        return $state;
325
    }
326
    /**
327
     * Handles an opening or self-closing view helper tag.
328
     *
329
     * @param ParsingState $state Current parsing state
330
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
331
     * @param string $methodIdentifier Method identifier
332
     * @param string $arguments Arguments string, not yet parsed
333
     * @param boolean $selfclosing true, if the tag is a self-closing tag.
334
     * @param string $templateElement The template code containing the ViewHelper call
335
     * @return NodeInterface|null
336
     */
337
    protected function openingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing, $templateElement)
338
    {
339
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
340
        if ($viewHelperResolver->isNamespaceIgnored($namespaceIdentifier)) {
341
            return null;
342
        }
343
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
344
            throw new UnknownNamespaceException('Unknown Namespace: ' . $namespaceIdentifier);
345
        }
346
347
        $viewHelper = $viewHelperResolver->createViewHelperInstance($namespaceIdentifier, $methodIdentifier);
348
        $argumentDefinitions = $viewHelper->prepareArguments();
0 ignored issues
show
Unused Code introduced by
$argumentDefinitions is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
349
        $viewHelperNode = $this->initializeViewHelperAndAddItToStack(
350
            $state,
351
            $namespaceIdentifier,
352
            $methodIdentifier,
353
            $this->parseArguments($arguments, $viewHelper)
354
        );
355
356
        if ($viewHelperNode) {
357
            $viewHelperNode->setPointerTemplateCode($templateElement);
358
            if ($selfclosing === true) {
359
                $state->popNodeFromStack();
360
                $this->callInterceptor($viewHelperNode, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
361
                // This needs to be called here because closingViewHelperTagHandler() is not triggered for self-closing tags
362
                $state->getNodeFromStack()->addChildNode($viewHelperNode);
363
            }
364
        }
365
366
        return $viewHelperNode;
367
    }
368
369
    /**
370
     * Initialize the given ViewHelper and adds it to the current node and to
371
     * the stack.
372
     *
373
     * @param ParsingState $state Current parsing state
374
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
375
     * @param string $methodIdentifier Method identifier
376
     * @param array $argumentsObjectTree Arguments object tree
377
     * @return null|NodeInterface An instance of ViewHelperNode if identity was valid - NULL if the namespace/identity was not registered
378
     * @throws Exception
379
     */
380
    protected function initializeViewHelperAndAddItToStack(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree)
381
    {
382
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
383
        if ($viewHelperResolver->isNamespaceIgnored($namespaceIdentifier)) {
384
            return null;
385
        }
386
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
387
            throw new UnknownNamespaceException('Unknown Namespace: ' . $namespaceIdentifier);
388
        }
389
        try {
390
            $currentViewHelperNode = new ViewHelperNode(
391
                $this->renderingContext,
392
                $namespaceIdentifier,
393
                $methodIdentifier,
394
                $argumentsObjectTree,
395
                $state
396
            );
397
398
            $this->callInterceptor($currentViewHelperNode, InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
399
            $viewHelper = $currentViewHelperNode->getUninitializedViewHelper();
400
            $viewHelper::postParseEvent($currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
401
            $state->pushNodeToStack($currentViewHelperNode);
402
            return $currentViewHelperNode;
403
        } catch (\TYPO3Fluid\Fluid\Core\ViewHelper\Exception $error) {
404
            $this->textHandler(
405
                $state,
406
                $this->renderingContext->getErrorHandler()->handleViewHelperError($error)
407
            );
408
        } catch (Exception $error) {
409
            $this->textHandler(
410
                $state,
411
                $this->renderingContext->getErrorHandler()->handleParserError($error)
412
            );
413
        }
414
        return null;
415
    }
416
417
    /**
418
     * Handles a closing view helper tag
419
     *
420
     * @param ParsingState $state The current parsing state
421
     * @param string $namespaceIdentifier Namespace identifier for the closing tag.
422
     * @param string $methodIdentifier Method identifier.
423
     * @return boolean whether the viewHelper was found and added to the stack or not
424
     * @throws Exception
425
     */
426
    protected function closingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier)
427
    {
428
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
429
        if ($viewHelperResolver->isNamespaceIgnored($namespaceIdentifier)) {
430
            return false;
431
        }
432
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
433
            throw new UnknownNamespaceException('Unknown Namespace: ' . $namespaceIdentifier);
434
        }
435
        $lastStackElement = $state->popNodeFromStack();
436
        if (!($lastStackElement instanceof ViewHelperNode)) {
437
            throw new Exception('You closed a templating tag which you never opened!', 1224485838);
438
        }
439
        $actualViewHelperClassName = $viewHelperResolver->resolveViewHelperClassName($namespaceIdentifier, $methodIdentifier);
440
        $expectedViewHelperClassName = $lastStackElement->getViewHelperClassName();
441
        if ($actualViewHelperClassName !== $expectedViewHelperClassName) {
442
            throw new Exception(
443
                'Templating tags not properly nested. Expected: ' . $expectedViewHelperClassName . '; Actual: ' .
444
                $actualViewHelperClassName,
445
                1224485398
446
            );
447
        }
448
        $this->callInterceptor($lastStackElement, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
449
        $state->getNodeFromStack()->addChildNode($lastStackElement);
450
451
        return true;
452
    }
453
454
    /**
455
     * Handles the appearance of an object accessor (like {posts.author.email}).
456
     * Creates a new instance of \TYPO3Fluid\Fluid\ObjectAccessorNode.
457
     *
458
     * Handles ViewHelpers as well which are in the shorthand syntax.
459
     *
460
     * @param ParsingState $state The current parsing state
461
     * @param string $objectAccessorString String which identifies which objects to fetch
462
     * @param string $delimiter
463
     * @param string $viewHelperString
464
     * @param string $additionalViewHelpersString
465
     * @return void
466
     */
467
    protected function objectAccessorHandler(ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString)
468
    {
469
        $viewHelperString .= $additionalViewHelpersString;
470
        $numberOfViewHelpers = 0;
471
472
        // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
473
        // Resolves bug #5107.
474
        if (strlen($delimiter) === 0 && strlen($viewHelperString) > 0) {
475
            $viewHelperString = $objectAccessorString . $viewHelperString;
476
            $objectAccessorString = '';
477
        }
478
479
        // ViewHelpers
480
        $matches = [];
481
        if (strlen($viewHelperString) > 0 && preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
482
            // The last ViewHelper has to be added first for correct chaining.
483
            // Note that ignoring namespaces is NOT possible in inline syntax; any inline syntax that contains a namespace
484
            // which is invalid will be reported as an error regardless of whether the namespace is marked as ignored.
485
            $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
486
            foreach (array_reverse($matches) as $singleMatch) {
487
                if (!$viewHelperResolver->isNamespaceValid($singleMatch['NamespaceIdentifier'])) {
488
                    throw new UnknownNamespaceException('Unknown Namespace: ' . $singleMatch['NamespaceIdentifier']);
489
                }
490
                $viewHelper = $viewHelperResolver->createViewHelperInstance($singleMatch['NamespaceIdentifier'], $singleMatch['MethodIdentifier']);
491
                if (strlen($singleMatch['ViewHelperArguments']) > 0) {
492
                    $arguments = $this->recursiveArrayHandler($state, $singleMatch['ViewHelperArguments'], $viewHelper);
493
                } else {
494
                    $arguments = [];
495
                }
496
                $viewHelperNode = $this->initializeViewHelperAndAddItToStack(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $viewHelperNode is correct as $this->initializeViewHel...entifier'], $arguments) (which targets TYPO3Fluid\Fluid\Core\Pa...HelperAndAddItToStack()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
497
                    $state,
498
                    $singleMatch['NamespaceIdentifier'],
499
                    $singleMatch['MethodIdentifier'],
500
                    $arguments
501
                );
502
                if ($viewHelperNode) {
503
                    $numberOfViewHelpers++;
504
                }
505
            }
506
        }
507
508
        // Object Accessor
509
        if (strlen($objectAccessorString) > 0) {
510
            $node = new ObjectAccessorNode($objectAccessorString);
511
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
512
            $state->getNodeFromStack()->addChildNode($node);
513
        }
514
515
        // Close ViewHelper Tags if needed.
516
        for ($i = 0; $i < $numberOfViewHelpers; $i++) {
517
            $node = $state->popNodeFromStack();
518
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
519
            $state->getNodeFromStack()->addChildNode($node);
520
        }
521
    }
522
523
    /**
524
     * Call all interceptors registered for a given interception point.
525
     *
526
     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
527
     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
528
     * @param ParsingState $state the parsing state
529
     * @return void
530
     */
531
    protected function callInterceptor(NodeInterface & $node, $interceptionPoint, ParsingState $state)
532
    {
533
        if ($this->configuration === null) {
534
            return;
535
        }
536
        if ($this->escapingEnabled) {
537
            /** @var $interceptor InterceptorInterface */
538
            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
539
                $node = $interceptor->process($node, $interceptionPoint, $state);
540
            }
541
        }
542
543
        /** @var $interceptor InterceptorInterface */
544
        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
545
            $node = $interceptor->process($node, $interceptionPoint, $state);
546
        }
547
    }
548
549
    /**
550
     * Parse arguments of a given tag, and build up the Arguments Object Tree
551
     * for each argument.
552
     * Returns an associative array, where the key is the name of the argument,
553
     * and the value is a single Argument Object Tree.
554
     *
555
     * @param string $argumentsString All arguments as string
556
     * @param ViewHelperInterface $viewHelper
557
     * @return array An associative array of objects, where the key is the argument name.
558
     */
559
    protected function parseArguments($argumentsString, ViewHelperInterface $viewHelper)
560
    {
561
        $argumentDefinitions = $this->renderingContext->getViewHelperResolver()->getArgumentDefinitionsForViewHelper($viewHelper);
562
        $argumentsObjectTree = [];
563
        $undeclaredArguments = [];
564
        $matches = [];
565
        if (preg_match_all(Patterns::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
566
            foreach ($matches as $singleMatch) {
567
                $argument = $singleMatch['Argument'];
568
                $value = $this->unquoteString($singleMatch['ValueQuoted']);
569
                $escapingEnabledBackup = $this->escapingEnabled;
570
                if (isset($argumentDefinitions[$argument])) {
571
                    $argumentDefinition = $argumentDefinitions[$argument];
572
                    $this->escapingEnabled = $this->escapingEnabled && $this->isArgumentEscaped($viewHelper, $argumentDefinition);
573
                    $isBoolean = $argumentDefinition->getType() === 'boolean' || $argumentDefinition->getType() === 'bool';
574
                    $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
575
                    if ($isBoolean) {
576
                        $argumentsObjectTree[$argument] = new BooleanNode($argumentsObjectTree[$argument]);
577
                    }
578
                } else {
579
                    $this->escapingEnabled = false;
580
                    $undeclaredArguments[$argument] = $this->buildArgumentObjectTree($value);
581
                }
582
                $this->escapingEnabled = $escapingEnabledBackup;
583
            }
584
        }
585
        $this->abortIfRequiredArgumentsAreMissing($argumentDefinitions, $argumentsObjectTree);
586
        $viewHelper->validateAdditionalArguments($undeclaredArguments);
587
        return $argumentsObjectTree + $undeclaredArguments;
588
    }
589
590
    protected function isArgumentEscaped(ViewHelperInterface $viewHelper, ArgumentDefinition $argumentDefinition = null)
591
    {
592
        $hasDefinition = $argumentDefinition instanceof ArgumentDefinition;
593
        $isBoolean = $hasDefinition && ($argumentDefinition->getType() === 'boolean' || $argumentDefinition->getType() === 'bool');
594
        $escapingEnabled = $this->configuration->isViewHelperArgumentEscapingEnabled();
595
        $isArgumentEscaped = $hasDefinition && $argumentDefinition->getEscape() === true;
596
        $isContentArgument = $hasDefinition && method_exists($viewHelper, 'resolveContentArgumentName') && $argumentDefinition->getName() === $viewHelper->resolveContentArgumentName();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TYPO3Fluid\Fluid\Core\Vi...per\ViewHelperInterface as the method resolveContentArgumentName() does only exist in the following implementations of said interface: TYPO3Fluid\Fluid\ViewHelpers\CountViewHelper, TYPO3Fluid\Fluid\ViewHel...\Format\CdataViewHelper, TYPO3Fluid\Fluid\ViewHel...Format\PrintfViewHelper, TYPO3Fluid\Fluid\ViewHelpers\Format\RawViewHelper, TYPO3Fluid\Fluid\ViewHelpers\InlineViewHelper, TYPO3Fluid\Fluid\ViewHelpers\OrViewHelper, TYPO3Fluid\Fluid\ViewHelpers\VariableViewHelper.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
597
        if ($isContentArgument) {
598
            return !$isBoolean && ($viewHelper->isChildrenEscapingEnabled() || $isArgumentEscaped);
599
        }
600
        return !$isBoolean && $escapingEnabled && $isArgumentEscaped;
601
    }
602
603
    /**
604
     * Build up an argument object tree for the string in $argumentString.
605
     * This builds up the tree for a single argument value.
606
     *
607
     * This method also does some performance optimizations, so in case
608
     * no { or < is found, then we just return a TextNode.
609
     *
610
     * @param string $argumentString
611
     * @return SyntaxTree\NodeInterface the corresponding argument object tree.
612
     */
613
    protected function buildArgumentObjectTree($argumentString)
614
    {
615
        if (strpos($argumentString, '{') === false && strpos($argumentString, '<') === false) {
616
            if (is_numeric($argumentString)) {
617
                return new NumericNode($argumentString);
618
            }
619
            return new TextNode($argumentString);
620
        }
621
        $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
622
        $rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
623
        return $rootNode;
624
    }
625
626
    /**
627
     * Removes escapings from a given argument string and trims the outermost
628
     * quotes.
629
     *
630
     * This method is meant as a helper for regular expression results.
631
     *
632
     * @param string $quotedValue Value to unquote
633
     * @return string Unquoted value
634
     */
635
    public function unquoteString($quotedValue)
636
    {
637
        $value = $quotedValue;
638
        if ($value === '') {
639
            return $value;
640
        }
641
        if ($quotedValue[0] === '"') {
642
            $value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
643
        } elseif ($quotedValue[0] === '\'') {
644
            $value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
645
        }
646
        return str_replace('\\\\', '\\', $value);
647
    }
648
649
    /**
650
     * Handler for everything which is not a ViewHelperNode.
651
     *
652
     * This includes Text, array syntax, and object accessor syntax.
653
     *
654
     * @param ParsingState $state Current parsing state
655
     * @param string $text Text to process
656
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
657
     * @return void
658
     */
659
    protected function textAndShorthandSyntaxHandler(ParsingState $state, $text, $context)
660
    {
661
        $sections = preg_split(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
662
        if ($sections === false) {
663
            // String $text was not possible to split; we must return a text node with the full text instead.
664
            $this->textHandler($state, $text);
665
            return;
666
        }
667
        foreach ($sections as $section) {
668
            $matchedVariables = [];
669
            $expressionNode = null;
670
            if (preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
671
                $this->objectAccessorHandler(
672
                    $state,
673
                    $matchedVariables['Object'],
674
                    $matchedVariables['Delimiter'],
675
                    (isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : ''),
676
                    (isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : '')
677
                );
678
            } elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS
679
                && preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0
680
            ) {
681
                // We only match arrays if we are INSIDE viewhelper arguments
682
                $this->arrayHandler($state, $this->recursiveArrayHandler($state, $matchedVariables['Array']));
683
            } else {
684
                // We ask custom ExpressionNode instances from ViewHelperResolver
685
                // if any match our expression:
686
                foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
687
                    $detectionExpression = $expressionNodeTypeClassName::$detectionExpression;
688
                    $matchedVariables = [];
689
                    preg_match_all($detectionExpression, $section, $matchedVariables, PREG_SET_ORDER);
690
                    if (is_array($matchedVariables) === true) {
691
                        foreach ($matchedVariables as $matchedVariableSet) {
692
                            $expressionStartPosition = strpos($section, $matchedVariableSet[0]);
693
                            /** @var ExpressionNodeInterface $expressionNode */
694
                            $expressionNode = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $state);
695
                            try {
696
                                // Trigger initial parse-time evaluation to allow the node to manipulate the rendering context.
697
                                if ($expressionNode instanceof ParseTimeEvaluatedExpressionNodeInterface) {
698
                                    $expressionNode->evaluate($this->renderingContext);
699
                                }
700
701
                                if ($expressionStartPosition > 0) {
702
                                    $state->getNodeFromStack()->addChildNode(new TextNode(substr($section, 0, $expressionStartPosition)));
703
                                }
704
705
                                $this->callInterceptor($expressionNode, InterceptorInterface::INTERCEPT_EXPRESSION, $state);
706
                                $state->getNodeFromStack()->addChildNode($expressionNode);
707
708
                                $expressionEndPosition = $expressionStartPosition + strlen($matchedVariableSet[0]);
709
                                if ($expressionEndPosition < strlen($section)) {
710
                                    $this->textAndShorthandSyntaxHandler($state, substr($section, $expressionEndPosition), $context);
711
                                    break;
712
                                }
713
                            } catch (ExpressionException $error) {
714
                                $this->textHandler(
715
                                    $state,
716
                                    $this->renderingContext->getErrorHandler()->handleExpressionError($error)
717
                                );
718
                            }
719
                        }
720
                    }
721
                }
722
723
                if (!$expressionNode) {
724
                    // As fallback we simply render the expression back as template content.
725
                    $this->textHandler($state, $section);
726
                }
727
            }
728
        }
729
    }
730
731
    /**
732
     * Handler for array syntax. This creates the array object recursively and
733
     * adds it to the current node.
734
     *
735
     * @param ParsingState $state The current parsing state
736
     * @param NodeInterface[] $arrayText The array as string.
737
     * @return void
738
     */
739
    protected function arrayHandler(ParsingState $state, $arrayText)
740
    {
741
        $arrayNode = new ArrayNode($arrayText);
742
        $state->getNodeFromStack()->addChildNode($arrayNode);
743
    }
744
745
    /**
746
     * Recursive function which takes the string representation of an array and
747
     * builds an object tree from it.
748
     *
749
     * Deals with the following value types:
750
     * - Numbers (Integers and Floats)
751
     * - Strings
752
     * - Variables
753
     * - sub-arrays
754
     *
755
     * @param ParsingState $state
756
     * @param string $arrayText Array text
757
     * @param ViewHelperInterface|null $viewHelper ViewHelper instance - passed only if the array is a collection of arguments for an inline ViewHelper
758
     * @return NodeInterface[] the array node built up
759
     * @throws Exception
760
     */
761
    protected function recursiveArrayHandler(ParsingState $state, $arrayText, ViewHelperInterface $viewHelper = null)
762
    {
763
        $undeclaredArguments = [];
764
        $argumentDefinitions = [];
765
        if ($viewHelper instanceof ViewHelperInterface) {
766
            $argumentDefinitions = $this->renderingContext->getViewHelperResolver()->getArgumentDefinitionsForViewHelper($viewHelper);
767
        }
768
        $matches = [];
769
        $arrayToBuild = [];
770
        if (preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER)) {
771
            foreach ($matches as $singleMatch) {
772
                $arrayKey = $this->unquoteString($singleMatch['Key']);
773
                $assignInto = &$arrayToBuild;
774
                $isBoolean = false;
775
                $argumentDefinition = null;
776
                if (isset($argumentDefinitions[$arrayKey])) {
777
                    $argumentDefinition = $argumentDefinitions[$arrayKey];
778
                    $isBoolean = $argumentDefinitions[$arrayKey]->getType() === 'boolean' || $argumentDefinitions[$arrayKey]->getType() === 'bool';
779
                } else {
780
                    $assignInto = &$undeclaredArguments;
781
                }
782
783
                $escapingEnabledBackup = $this->escapingEnabled;
784
                $this->escapingEnabled = $this->escapingEnabled && $viewHelper instanceof ViewHelperInterface && $this->isArgumentEscaped($viewHelper, $argumentDefinition);
785
786
                if (array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
787
                    $assignInto[$arrayKey] = new ArrayNode($this->recursiveArrayHandler($state, $singleMatch['Subarray']));
788
                } elseif (!empty($singleMatch['VariableIdentifier'])) {
789
                    $assignInto[$arrayKey] = new ObjectAccessorNode($singleMatch['VariableIdentifier']);
790
                    if ($viewHelper instanceof ViewHelperInterface && !$isBoolean) {
791
                        $this->callInterceptor($assignInto[$arrayKey], InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
792
                    }
793
                } elseif (array_key_exists('Number', $singleMatch) && (!empty($singleMatch['Number']) || $singleMatch['Number'] === '0')) {
794
                    // Note: this method of casting picks "int" when value is a natural number and "float" if any decimals are found. See also NumericNode.
795
                    $assignInto[$arrayKey] = $singleMatch['Number'] + 0;
796
                } elseif ((array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']))) {
797
                    $argumentString = $this->unquoteString($singleMatch['QuotedString']);
798
                    $assignInto[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
799
                }
800
801
                if ($isBoolean) {
802
                    $assignInto[$arrayKey] = new BooleanNode($assignInto[$arrayKey]);
803
                }
804
805
                $this->escapingEnabled = $escapingEnabledBackup;
806
            }
807
        }
808
        if ($viewHelper instanceof ViewHelperInterface) {
809
            $this->abortIfRequiredArgumentsAreMissing($argumentDefinitions, $arrayToBuild);
810
            $viewHelper->validateAdditionalArguments($undeclaredArguments);
811
        }
812
        return $arrayToBuild + $undeclaredArguments;
813
    }
814
815
    /**
816
     * Text node handler
817
     *
818
     * @param ParsingState $state
819
     * @param string $text
820
     * @return void
821
     */
822
    protected function textHandler(ParsingState $state, $text)
823
    {
824
        $node = new TextNode($text);
825
        $this->callInterceptor($node, InterceptorInterface::INTERCEPT_TEXT, $state);
826
        $state->getNodeFromStack()->addChildNode($node);
827
    }
828
829
    /**
830
     * @return ParsingState
831
     */
832
    protected function getParsingState()
833
    {
834
        $rootNode = new RootNode();
835
        $variableProvider = $this->renderingContext->getVariableProvider();
836
        $state = new ParsingState();
837
        $state->setRootNode($rootNode);
838
        $state->pushNodeToStack($rootNode);
839
        $state->setVariableProvider($variableProvider->getScopeCopy($variableProvider->getAll()));
840
        return $state;
841
    }
842
843
    /**
844
     * Throw an exception if required arguments are missing
845
     *
846
     * @param ArgumentDefinition[] $expectedArguments Array of all expected arguments
847
     * @param NodeInterface[] $actualArguments Actual arguments
848
     * @throws Exception
849
     */
850
    protected function abortIfRequiredArgumentsAreMissing($expectedArguments, $actualArguments)
851
    {
852
        $actualArgumentNames = array_keys($actualArguments);
853
        foreach ($expectedArguments as $name => $expectedArgument) {
854
            if ($expectedArgument->isRequired() && !in_array($name, $actualArgumentNames)) {
855
                throw new Exception('Required argument "' . $name . '" was not supplied.', 1237823699);
856
            }
857
        }
858
    }
859
}
860