Completed
Push — master ( 920e94...27b195 )
by Mathias
01:43
created

TemplateParser::objectAccessorHandler()   C

Complexity

Conditions 11
Paths 18

Size

Total Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

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