Completed
Pull Request — master (#493)
by
unknown
01:40
created

TemplateParser::buildObjectTree()   B

Complexity

Conditions 9
Paths 22

Size

Total Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 22
nop 2
dl 0
loc 48
rs 7.5789
c 0
b 0
f 0
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\Parser\SyntaxTree\ArrayNode;
11
use TYPO3Fluid\Fluid\Core\Compiler\UncompilableTemplateInterface;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionException;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ParseTimeEvaluatedExpressionNodeInterface;
15
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
16
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NumericNode;
17
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
18
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
19
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
20
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
21
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
22
23
/**
24
 * Template parser building up an object syntax tree
25
 */
26
class TemplateParser
27
{
28
29
    /**
30
     * The following two constants are used for tracking whether we are currently
31
     * parsing ViewHelper arguments or not. This is used to parse arrays only as
32
     * ViewHelper argument.
33
     */
34
    const CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS = 1;
35
    const CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS = 2;
36
37
    /**
38
     * Whether or not the escaping interceptors are active
39
     *
40
     * @var boolean
41
     */
42
    protected $escapingEnabled = true;
43
44
    /**
45
     * @var Configuration
46
     */
47
    protected $configuration;
48
49
    /**
50
     * @var array
51
     */
52
    protected $settings;
53
54
    /**
55
     * @var RenderingContextInterface
56
     */
57
    protected $renderingContext;
58
59
    /**
60
     * @var integer
61
     */
62
    protected $pointerLineNumber = 1;
63
64
    /**
65
     * @var integer
66
     */
67
    protected $pointerLineCharacter = 1;
68
69
    /**
70
     * @var string
71
     */
72
    protected $pointerTemplateCode = null;
73
74
    /**
75
     * @var ParsedTemplateInterface[]
76
     */
77
    protected $parsedTemplates = [];
78
79
    /**
80
     * @param RenderingContextInterface $renderingContext
81
     * @return void
82
     */
83
    public function setRenderingContext(RenderingContextInterface $renderingContext)
84
    {
85
        $this->renderingContext = $renderingContext;
86
        $this->configuration = $renderingContext->buildParserConfiguration();
87
    }
88
89
    /**
90
     * Returns an array of current line number, character in line and reference template code;
91
     * for extraction when catching parser-related Exceptions during parsing.
92
     *
93
     * @return array
94
     */
95
    public function getCurrentParsingPointers()
96
    {
97
        return [$this->pointerLineNumber, $this->pointerLineCharacter, $this->pointerTemplateCode];
98
    }
99
100
    /**
101
     * @return boolean
102
     */
103
    public function isEscapingEnabled()
104
    {
105
        return $this->escapingEnabled;
106
    }
107
108
    /**
109
     * @param boolean $escapingEnabled
110
     * @return void
111
     */
112
    public function setEscapingEnabled($escapingEnabled)
113
    {
114
        $this->escapingEnabled = (boolean) $escapingEnabled;
115
    }
116
117
    /**
118
     * Parses a given template string and returns a parsed template object.
119
     *
120
     * The resulting ParsedTemplate can then be rendered by calling evaluate() on it.
121
     *
122
     * Normally, you should use a subclass of AbstractTemplateView instead of calling the
123
     * TemplateParser directly.
124
     *
125
     * @param string $templateString The template to parse as a string
126
     * @param string|null $templateIdentifier If the template has an identifying string it can be passed here to improve error reporting.
127
     * @return ParsingState Parsed template
128
     * @throws Exception
129
     */
130
    public function parse($templateString, $templateIdentifier = null)
131
    {
132
        if (!is_string($templateString)) {
133
            throw new Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
134
        }
135
        try {
136
            $this->reset();
137
138
            $templateString = $this->preProcessTemplateSource($templateString);
139
140
            $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
141
            $parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
142
        } catch (Exception $error) {
143
            throw $this->createParsingRelatedExceptionWithContext($error, $templateIdentifier);
144
        }
145
        $this->parsedTemplates[$templateIdentifier] = $parsingState;
146
        return $parsingState;
147
    }
148
149
    /**
150
     * @param \Exception $error
151
     * @param string $templateIdentifier
152
     * @throws \Exception
153
     */
154
    public function createParsingRelatedExceptionWithContext(\Exception $error, $templateIdentifier)
155
    {
156
        list ($line, $character, $templateCode) = $this->getCurrentParsingPointers();
157
        $exceptionClass = get_class($error);
158
        return new $exceptionClass(
159
            sprintf(
160
                'Fluid parse error in template %s, line %d at character %d. Error: %s (error code %d). Template source chunk: %s',
161
                $templateIdentifier,
162
                $line,
163
                $character,
164
                $error->getMessage(),
165
                $error->getCode(),
166
                $templateCode
167
            ),
168
            $error->getCode(),
169
            $error
170
        );
171
    }
172
173
    /**
174
     * @param string $templateIdentifier
175
     * @param \Closure $templateSourceClosure Closure which returns the template source if needed
176
     * @return ParsedTemplateInterface
177
     */
178
    public function getOrParseAndStoreTemplate($templateIdentifier, $templateSourceClosure)
179
    {
180
        $compiler = $this->renderingContext->getTemplateCompiler();
181
        if (isset($this->parsedTemplates[$templateIdentifier])) {
182
            $parsedTemplate = $this->parsedTemplates[$templateIdentifier];
183
        } elseif ($compiler->has($templateIdentifier)) {
184
            $parsedTemplate = $compiler->get($templateIdentifier);
185
            if ($parsedTemplate instanceof UncompilableTemplateInterface) {
186
                $parsedTemplate = $this->parseTemplateSource($templateIdentifier, $templateSourceClosure);
187
            }
188
        } else {
189
            $parsedTemplate = $this->parseTemplateSource($templateIdentifier, $templateSourceClosure);
190
            try {
191
                $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...
192
            } catch (StopCompilingException $stop) {
193
                $this->renderingContext->getErrorHandler()->handleCompilerError($stop);
194
                $parsedTemplate->setCompilable(false);
195
                $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...
196
            }
197
        }
198
        return $parsedTemplate;
199
    }
200
201
    /**
202
     * @param string $templateIdentifier
203
     * @param \Closure $templateSourceClosure
204
     * @return ParsedTemplateInterface
205
     */
206
    protected function parseTemplateSource($templateIdentifier, $templateSourceClosure)
207
    {
208
        $parsedTemplate = $this->parse(
209
            $templateSourceClosure($this, $this->renderingContext->getTemplatePaths()),
210
            $templateIdentifier
211
        );
212
        $parsedTemplate->setIdentifier($templateIdentifier);
213
        $this->parsedTemplates[$templateIdentifier] = $parsedTemplate;
214
        return $parsedTemplate;
215
    }
216
217
    /**
218
     * Pre-process the template source, making all registered TemplateProcessors
219
     * do what they need to do with the template source before it is parsed.
220
     *
221
     * @param string $templateSource
222
     * @return string
223
     */
224
    protected function preProcessTemplateSource($templateSource)
225
    {
226
        foreach ($this->renderingContext->getTemplateProcessors() as $templateProcessor) {
227
            $templateSource = $templateProcessor->preProcessSource($templateSource);
228
        }
229
        return $templateSource;
230
    }
231
232
    /**
233
     * Resets the parser to its default values.
234
     *
235
     * @return void
236
     */
237
    protected function reset()
238
    {
239
        $this->escapingEnabled = true;
240
        $this->pointerLineNumber = 1;
241
        $this->pointerLineCharacter = 1;
242
    }
243
244
    /**
245
     * Splits the template string on all dynamic tags found.
246
     *
247
     * @param string $templateString Template string to split.
248
     * @return array Splitted template
249
     */
250
    protected function splitTemplateAtDynamicTags($templateString)
251
    {
252
        return preg_split(Patterns::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
253
    }
254
255
    /**
256
     * Build object tree from the split template
257
     *
258
     * @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a seperate array element.
259
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
260
     * @return ParsingState
261
     * @throws Exception
262
     */
263
    protected function buildObjectTree(array $splitTemplate, $context)
264
    {
265
        $state = $this->getParsingState();
266
        $previousBlock = '';
267
268
        foreach ($splitTemplate as $templateElement) {
269
            if ($context === self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS) {
270
                // Store a neat reference to the outermost chunk of Fluid template code.
271
                // Don't store the reference if parsing ViewHelper arguments object tree;
272
                // we want the reference code to contain *all* of the ViewHelper call.
273
                $this->pointerTemplateCode = $templateElement;
274
            }
275
            $this->pointerLineNumber += substr_count($templateElement, PHP_EOL);
276
            $this->pointerLineCharacter = strlen(substr($previousBlock, strrpos($previousBlock, PHP_EOL))) + 1;
277
            $previousBlock = $templateElement;
278
            $matchedVariables = [];
279
280
            if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
281
                if ($this->openingViewHelperTagHandler(
282
                    $state,
283
                    $matchedVariables['NamespaceIdentifier'],
284
                    $matchedVariables['MethodIdentifier'],
285
                    $matchedVariables['Attributes'],
286
                    ($matchedVariables['Selfclosing'] === '' ? false : true),
287
                    $templateElement
288
                )) {
289
                    continue;
290
                }
291
            } elseif (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
292
                if ($this->closingViewHelperTagHandler(
293
                    $state,
294
                    $matchedVariables['NamespaceIdentifier'],
295
                    $matchedVariables['MethodIdentifier']
296
                )) {
297
                    continue;
298
                }
299
            }
300
            $this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
301
        }
302
303
        if ($state->countNodeStack() !== 1) {
304
            throw new Exception(
305
                'Not all tags were closed!',
306
                1238169398
307
            );
308
        }
309
        return $state;
310
    }
311
    /**
312
     * Handles an opening or self-closing view helper tag.
313
     *
314
     * @param ParsingState $state Current parsing state
315
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
316
     * @param string $methodIdentifier Method identifier
317
     * @param string $arguments Arguments string, not yet parsed
318
     * @param boolean $selfclosing true, if the tag is a self-closing tag.
319
     * @param string $templateElement The template code containing the ViewHelper call
320
     * @return NodeInterface|null
321
     */
322
    protected function openingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing, $templateElement)
323
    {
324
        $viewHelperNode = $this->initializeViewHelperAndAddItToStack(
325
            $state,
326
            $namespaceIdentifier,
327
            $methodIdentifier,
328
            $this->parseArguments($arguments)
329
        );
330
331
        if ($viewHelperNode) {
332
            $viewHelperNode->setPointerTemplateCode($templateElement);
333
            if ($selfclosing === true) {
334
                $state->popNodeFromStack();
335
                $this->callInterceptor($viewHelperNode, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
336
                // This needs to be called here because closingViewHelperTagHandler() is not triggered for self-closing tags
337
                $state->getNodeFromStack()->addChildNode($viewHelperNode);
338
            }
339
        }
340
341
        return $viewHelperNode;
342
    }
343
344
    /**
345
     * Initialize the given ViewHelper and adds it to the current node and to
346
     * the stack.
347
     *
348
     * @param ParsingState $state Current parsing state
349
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
350
     * @param string $methodIdentifier Method identifier
351
     * @param array $argumentsObjectTree Arguments object tree
352
     * @return null|NodeInterface An instance of ViewHelperNode if identity was valid - NULL if the namespace/identity was not registered
353
     * @throws Exception
354
     */
355
    protected function initializeViewHelperAndAddItToStack(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree)
356
    {
357
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
358
        if ($viewHelperResolver->isNamespaceIgnored($namespaceIdentifier)) {
359
            return null;
360
        }
361
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
362
            throw new UnknownNamespaceException('Unknown Namespace: ' . $namespaceIdentifier);
363
        }
364
        try {
365
            $currentViewHelperNode = new ViewHelperNode(
366
                $this->renderingContext,
367
                $namespaceIdentifier,
368
                $methodIdentifier,
369
                $argumentsObjectTree,
370
                $state
371
            );
372
373
            $this->callInterceptor($currentViewHelperNode, InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
374
            $viewHelper = $currentViewHelperNode->getUninitializedViewHelper();
375
            $viewHelper::postParseEvent($currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
376
            $state->pushNodeToStack($currentViewHelperNode);
377
            return $currentViewHelperNode;
378
        } catch (\TYPO3Fluid\Fluid\Core\ViewHelper\Exception $error) {
379
            $this->textHandler(
380
                $state,
381
                $this->renderingContext->getErrorHandler()->handleViewHelperError($error)
382
            );
383
        } catch (Exception $error) {
384
            $this->textHandler(
385
                $state,
386
                $this->renderingContext->getErrorHandler()->handleParserError($error)
387
            );
388
        }
389
        return null;
390
    }
391
392
    /**
393
     * Handles a closing view helper tag
394
     *
395
     * @param ParsingState $state The current parsing state
396
     * @param string $namespaceIdentifier Namespace identifier for the closing tag.
397
     * @param string $methodIdentifier Method identifier.
398
     * @return boolean whether the viewHelper was found and added to the stack or not
399
     * @throws Exception
400
     */
401
    protected function closingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier)
402
    {
403
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
404
        if ($viewHelperResolver->isNamespaceIgnored($namespaceIdentifier)) {
405
            return false;
406
        }
407
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
408
            throw new UnknownNamespaceException('Unknown Namespace: ' . $namespaceIdentifier);
409
        }
410
        $lastStackElement = $state->popNodeFromStack();
411
        if (!($lastStackElement instanceof ViewHelperNode)) {
412
            throw new Exception('You closed a templating tag which you never opened!', 1224485838);
413
        }
414
        $actualViewHelperClassName = $viewHelperResolver->resolveViewHelperClassName($namespaceIdentifier, $methodIdentifier);
415
        $expectedViewHelperClassName = $lastStackElement->getViewHelperClassName();
416
        if ($actualViewHelperClassName !== $expectedViewHelperClassName) {
417
            throw new Exception(
418
                'Templating tags not properly nested. Expected: ' . $expectedViewHelperClassName . '; Actual: ' .
419
                $actualViewHelperClassName,
420
                1224485398
421
            );
422
        }
423
        $this->callInterceptor($lastStackElement, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
424
        $state->getNodeFromStack()->addChildNode($lastStackElement);
425
426
        return true;
427
    }
428
429
    /**
430
     * Handles the appearance of an object accessor (like {posts.author.email}).
431
     * Creates a new instance of \TYPO3Fluid\Fluid\ObjectAccessorNode.
432
     *
433
     * Handles ViewHelpers as well which are in the shorthand syntax.
434
     *
435
     * @param ParsingState $state The current parsing state
436
     * @param string $objectAccessorString String which identifies which objects to fetch
437
     * @param string $delimiter
438
     * @param string $viewHelperString
439
     * @param string $additionalViewHelpersString
440
     * @return void
441
     */
442
    protected function objectAccessorHandler(ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString)
443
    {
444
        $viewHelperString .= $additionalViewHelpersString;
445
        $numberOfViewHelpers = 0;
446
447
        // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
448
        // Resolves bug #5107.
449
        if (strlen($delimiter) === 0 && strlen($viewHelperString) > 0) {
450
            $viewHelperString = $objectAccessorString . $viewHelperString;
451
            $objectAccessorString = '';
452
        }
453
454
        // ViewHelpers
455
        $matches = [];
456
        if (strlen($viewHelperString) > 0 && preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
457
            // The last ViewHelper has to be added first for correct chaining.
458
            foreach (array_reverse($matches) as $singleMatch) {
459
                if (strlen($singleMatch['ViewHelperArguments']) > 0) {
460
                    $arguments = $this->recursiveArrayHandler($singleMatch['ViewHelperArguments']);
461
                } else {
462
                    $arguments = [];
463
                }
464
                $viewHelperNode = $this->initializeViewHelperAndAddItToStack($state, $singleMatch['NamespaceIdentifier'], $singleMatch['MethodIdentifier'], $arguments);
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...
465
                if ($viewHelperNode) {
466
                    $numberOfViewHelpers++;
467
                }
468
            }
469
        }
470
471
        // Object Accessor
472
        if (strlen($objectAccessorString) > 0) {
473
            $node = new ObjectAccessorNode($objectAccessorString);
474
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
475
            $state->getNodeFromStack()->addChildNode($node);
476
        }
477
478
        // Close ViewHelper Tags if needed.
479
        for ($i = 0; $i < $numberOfViewHelpers; $i++) {
480
            $node = $state->popNodeFromStack();
481
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
482
            $state->getNodeFromStack()->addChildNode($node);
483
        }
484
    }
485
486
    /**
487
     * Call all interceptors registered for a given interception point.
488
     *
489
     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
490
     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
491
     * @param ParsingState $state the parsing state
492
     * @return void
493
     */
494
    protected function callInterceptor(NodeInterface & $node, $interceptionPoint, ParsingState $state)
495
    {
496
        if ($this->configuration === null) {
497
            return;
498
        }
499
        if ($this->escapingEnabled) {
500
            /** @var $interceptor InterceptorInterface */
501
            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
502
                $node = $interceptor->process($node, $interceptionPoint, $state);
503
            }
504
        }
505
506
        /** @var $interceptor InterceptorInterface */
507
        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
508
            $node = $interceptor->process($node, $interceptionPoint, $state);
509
        }
510
    }
511
512
    /**
513
     * Parse arguments of a given tag, and build up the Arguments Object Tree
514
     * for each argument.
515
     * Returns an associative array, where the key is the name of the argument,
516
     * and the value is a single Argument Object Tree.
517
     *
518
     * @param string $argumentsString All arguments as string
519
     * @return array An associative array of objects, where the key is the argument name.
520
     */
521
    protected function parseArguments($argumentsString)
522
    {
523
        $argumentsObjectTree = [];
524
        $matches = [];
525
        if (preg_match_all(Patterns::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
526
            $escapingEnabledBackup = $this->escapingEnabled;
527
            $this->escapingEnabled = false;
528
            foreach ($matches as $singleMatch) {
529
                $argument = $singleMatch['Argument'];
530
                $value = $this->unquoteString($singleMatch['ValueQuoted']);
531
                $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
532
            }
533
            $this->escapingEnabled = $escapingEnabledBackup;
534
        }
535
        return $argumentsObjectTree;
536
    }
537
538
    /**
539
     * Build up an argument object tree for the string in $argumentString.
540
     * This builds up the tree for a single argument value.
541
     *
542
     * This method also does some performance optimizations, so in case
543
     * no { or < is found, then we just return a TextNode.
544
     *
545
     * @param string $argumentString
546
     * @return SyntaxTree\NodeInterface the corresponding argument object tree.
547
     */
548
    protected function buildArgumentObjectTree($argumentString)
549
    {
550
        if (strpos($argumentString, '{') === false && strpos($argumentString, '<') === false) {
551
            if (is_numeric($argumentString)) {
552
                return new NumericNode($argumentString);
553
            }
554
            return new TextNode($argumentString);
555
        }
556
        $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
557
        $rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
558
        return $rootNode;
559
    }
560
561
    /**
562
     * Removes escapings from a given argument string and trims the outermost
563
     * quotes.
564
     *
565
     * This method is meant as a helper for regular expression results.
566
     *
567
     * @param string $quotedValue Value to unquote
568
     * @return string Unquoted value
569
     */
570
    public function unquoteString($quotedValue)
571
    {
572
        $value = $quotedValue;
573
        if ($value === '') {
574
            return $value;
575
        }
576
        if ($quotedValue{0} === '"') {
577
            $value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
578
        } elseif ($quotedValue{0} === '\'') {
579
            $value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
580
        }
581
        return str_replace('\\\\', '\\', $value);
582
    }
583
584
    /**
585
     * Handler for everything which is not a ViewHelperNode.
586
     *
587
     * This includes Text, array syntax, and object accessor syntax.
588
     *
589
     * @param ParsingState $state Current parsing state
590
     * @param string $text Text to process
591
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
592
     * @return void
593
     */
594
    protected function textAndShorthandSyntaxHandler(ParsingState $state, $text, $context)
595
    {
596
        $sections = preg_split(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
597
        if ($sections === false) {
598
            // String $text was not possible to split; we must return a text node with the full text instead.
599
            $this->textHandler($state, $text);
600
            return;
601
        }
602
        foreach ($sections as $section) {
603
            $matchedVariables = [];
604
            $expressionNode = null;
605
            if (preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
606
                $this->objectAccessorHandler(
607
                    $state,
608
                    $matchedVariables['Object'],
609
                    $matchedVariables['Delimiter'],
610
                    (isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : ''),
611
                    (isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : '')
612
                );
613
            } elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS
614
                && preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0
615
            ) {
616
                // We only match arrays if we are INSIDE viewhelper arguments
617
                $this->arrayHandler($state, $this->recursiveArrayHandler($matchedVariables['Array']));
618
            } else {
619
                // We ask custom ExpressionNode instances from ViewHelperResolver
620
                // if any match our expression:
621
                foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
622
                    $detectionExpression = $expressionNodeTypeClassName::$detectionExpression;
623
                    $matchedVariables = [];
624
                    preg_match_all($detectionExpression, $section, $matchedVariables, PREG_SET_ORDER);
625
                    if (is_array($matchedVariables) === true) {
626
                        foreach ($matchedVariables as $matchedVariableSet) {
627
                            $expressionStartPosition = strpos($section, $matchedVariableSet[0]);
628
                            /** @var ExpressionNodeInterface $expressionNode */
629
                            $expressionNode = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $state);
630
                            try {
631
                                // Trigger initial parse-time evaluation to allow the node to manipulate the rendering context.
632
                                if ($expressionNode instanceof ParseTimeEvaluatedExpressionNodeInterface) {
633
                                    $expressionNode->evaluate($this->renderingContext);
634
                                }
635
636
                                if ($expressionStartPosition > 0) {
637
                                    $state->getNodeFromStack()->addChildNode(new TextNode(substr($section, 0, $expressionStartPosition)));
638
                                }
639
640
                                $this->callInterceptor($expressionNode, InterceptorInterface::INTERCEPT_EXPRESSION, $state);
641
                                $state->getNodeFromStack()->addChildNode($expressionNode);
642
643
                                $expressionEndPosition = $expressionStartPosition + strlen($matchedVariableSet[0]);
644
                                if ($expressionEndPosition < strlen($section)) {
645
                                    $this->textAndShorthandSyntaxHandler($state, substr($section, $expressionEndPosition), $context);
646
                                    break;
647
                                }
648
                            } catch (ExpressionException $error) {
649
                                $this->textHandler(
650
                                    $state,
651
                                    $this->renderingContext->getErrorHandler()->handleExpressionError($error)
652
                                );
653
                            }
654
                        }
655
                    }
656
                }
657
658
                if (!$expressionNode) {
659
                    // As fallback we simply render the expression back as template content.
660
                    $this->textHandler($state, $section);
661
                }
662
            }
663
        }
664
    }
665
666
    /**
667
     * Handler for array syntax. This creates the array object recursively and
668
     * adds it to the current node.
669
     *
670
     * @param ParsingState $state The current parsing state
671
     * @param NodeInterface[] $arrayText The array as string.
672
     * @return void
673
     */
674
    protected function arrayHandler(ParsingState $state, $arrayText)
675
    {
676
        $arrayNode = new ArrayNode($arrayText);
677
        $state->getNodeFromStack()->addChildNode($arrayNode);
678
    }
679
680
    /**
681
     * Recursive function which takes the string representation of an array and
682
     * builds an object tree from it.
683
     *
684
     * Deals with the following value types:
685
     * - Numbers (Integers and Floats)
686
     * - Strings
687
     * - Variables
688
     * - sub-arrays
689
     *
690
     * @param string $arrayText Array text
691
     * @return NodeInterface[] the array node built up
692
     * @throws Exception
693
     */
694
    protected function recursiveArrayHandler($arrayText)
695
    {
696
        $matches = [];
697
        $arrayToBuild = [];
698
        if (preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER)) {
699
            foreach ($matches as $singleMatch) {
700
                $arrayKey = $this->unquoteString($singleMatch['Key']);
701
                if (array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray']) && is_array($singleMatch['Subarray'])){
702
                    $arrayToBuild[$arrayKey] = new ArrayNode($this->recursiveArrayHandler($singleMatch['Subarray']));
0 ignored issues
show
Documentation introduced by
$singleMatch['Subarray'] is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
703
                } elseif (!empty($singleMatch['VariableIdentifier'])) {
704
                    $arrayToBuild[$arrayKey] = new ObjectAccessorNode($singleMatch['VariableIdentifier']);
705
                } elseif (array_key_exists('Number', $singleMatch) && (!empty($singleMatch['Number']) || $singleMatch['Number'] === '0')) {
706
                    // Note: this method of casting picks "int" when value is a natural number and "float" if any decimals are found. See also NumericNode.
707
                    $arrayToBuild[$arrayKey] = $singleMatch['Number'] + 0;
708
                } elseif ((array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']))) {
709
                    $argumentString = $this->unquoteString($singleMatch['QuotedString']);
710
                    $arrayToBuild[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
711
                }
712
            }
713
        }
714
        return $arrayToBuild;
715
    }
716
717
    /**
718
     * Text node handler
719
     *
720
     * @param ParsingState $state
721
     * @param string $text
722
     * @return void
723
     */
724
    protected function textHandler(ParsingState $state, $text)
725
    {
726
        $node = new TextNode($text);
727
        $this->callInterceptor($node, InterceptorInterface::INTERCEPT_TEXT, $state);
728
        $state->getNodeFromStack()->addChildNode($node);
729
    }
730
731
    /**
732
     * @return ParsingState
733
     */
734
    protected function getParsingState()
735
    {
736
        $rootNode = new RootNode();
737
        $variableProvider = $this->renderingContext->getVariableProvider();
738
        $state = new ParsingState();
739
        $state->setRootNode($rootNode);
740
        $state->pushNodeToStack($rootNode);
741
        $state->setVariableProvider($variableProvider->getScopeCopy($variableProvider->getAll()));
742
        return $state;
743
    }
744
}
745