Completed
Pull Request — master (#440)
by Claus
01:41
created

NodeConverter::convertArrayNode()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 1
dl 0
loc 42
rs 8.9368
c 0
b 0
f 0
1
<?php
2
namespace TYPO3Fluid\Fluid\Core\Compiler;
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\Parser\BooleanParser;
10
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\EscapingNode;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
15
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NumericNode;
16
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
17
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
18
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
19
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
20
use TYPO3Fluid\Fluid\Core\Variables\VariableExtractor;
21
22
/**
23
 * Class NodeConverter
24
 */
25
class NodeConverter
26
{
27
28
    /**
29
     * @var integer
30
     */
31
    protected $variableCounter = 0;
32
33
    /**
34
     * @var TemplateCompiler
35
     */
36
    protected $templateCompiler;
37
38
    /**
39
     * @param TemplateCompiler $templateCompiler
40
     */
41
    public function __construct(TemplateCompiler $templateCompiler)
42
    {
43
        $this->templateCompiler = $templateCompiler;
44
    }
45
46
    /**
47
     * @param integer $variableCounter
48
     * @return void
49
     */
50
    public function setVariableCounter($variableCounter)
51
    {
52
        $this->variableCounter = $variableCounter;
53
    }
54
55
    /**
56
     * Returns an array with two elements:
57
     * - initialization: contains PHP code which is inserted *before* the actual rendering call. Must be valid, i.e. end with semi-colon.
58
     * - execution: contains *a single PHP instruction* which needs to return the rendered output of the given element. Should NOT end with semi-colon.
59
     *
60
     * @param NodeInterface $node
61
     * @return array two-element array, see above
62
     * @throws FluidException
63
     */
64
    public function convert(NodeInterface $node)
65
    {
66
        switch (true) {
67
            case $node instanceof TextNode:
68
                $converted = $this->convertTextNode($node);
69
                break;
70
            case $node instanceof ExpressionNodeInterface:
71
                $converted = $this->convertExpressionNode($node);
72
                break;
73
            case $node instanceof NumericNode:
74
                $converted = $this->convertNumericNode($node);
75
                break;
76
            case $node instanceof ViewHelperNode:
77
                $converted = $this->convertViewHelperNode($node);
78
                break;
79
            case $node instanceof ObjectAccessorNode:
80
                $converted = $this->convertObjectAccessorNode($node);
81
                break;
82
            case $node instanceof ArrayNode:
83
                $converted = $this->convertArrayNode($node);
84
                break;
85
            case $node instanceof RootNode:
86
                $converted = $this->convertListOfSubNodes($node);
87
                break;
88
            case $node instanceof BooleanNode:
89
                $converted = $this->convertBooleanNode($node);
90
                break;
91
            case $node instanceof EscapingNode:
92
                $converted = $this->convertEscapingNode($node);
93
                break;
94
            default:
95
                $converted = [
96
                    'initialization' => '// Uncompilable/convertible node type: ' . get_class($node) . chr(10),
97
                    'execution' => ''
98
                ];
99
        }
100
        return $converted;
101
    }
102
103
    /**
104
     * @param EscapingNode $node
105
     * @return array
106
     */
107
    protected function convertEscapingNode(EscapingNode $node)
108
    {
109
        $configuration = $this->convert($node->getNode());
110
        if (!empty($configuration['execution'])) {
111
            $configuration['execution'] = sprintf(
112
                'call_user_func_array( function ($var) { ' .
113
                'return (is_string($var) || (is_object($var) && method_exists($var, \'__toString\')) ' .
114
                '? htmlspecialchars((string) $var, ENT_QUOTES) : $var); }, [%s])',
115
                $configuration['execution']
116
            );
117
        }
118
        return $configuration;
119
    }
120
121
    /**
122
     * @param TextNode $node
123
     * @return array
124
     * @see convert()
125
     */
126
    protected function convertTextNode(TextNode $node)
127
    {
128
        return [
129
            'initialization' => '',
130
            'execution' => '\'' . $this->escapeTextForUseInSingleQuotes($node->getText()) . '\''
131
        ];
132
    }
133
134
    /**
135
     * @param NumericNode $node
136
     * @return array
137
     * @see convert()
138
     */
139
    protected function convertNumericNode(NumericNode $node)
140
    {
141
        return [
142
            'initialization' => '',
143
            'execution' => $node->getValue()
144
        ];
145
    }
146
147
    /**
148
     * Convert a single ViewHelperNode into its cached representation. If the ViewHelper implements the "Compilable" facet,
149
     * the ViewHelper itself is asked for its cached PHP code representation. If not, a ViewHelper is built and then invoked.
150
     *
151
     * @param ViewHelperNode $node
152
     * @return array
153
     * @see convert()
154
     */
155
    protected function convertViewHelperNode(ViewHelperNode $node)
156
    {
157
        $initializationPhpCode = '// Rendering ViewHelper ' . $node->getViewHelperClassName() . chr(10);
158
159
        // Build up $arguments array
160
        $argumentsVariableName = $this->variableName('arguments');
161
        $renderChildrenClosureVariableName = $this->variableName('renderChildrenClosure');
162
        $viewHelperInitializationPhpCode = '';
163
164
        try {
165
            $convertedViewHelperExecutionCode = $node->getUninitializedViewHelper()->compile(
166
                $argumentsVariableName,
167
                $renderChildrenClosureVariableName,
168
                $viewHelperInitializationPhpCode,
169
                $node,
170
                $this->templateCompiler
171
            );
172
173
            $arguments = $node->getArgumentDefinitions();
174
            $argumentInitializationCode = sprintf('%s = array();', $argumentsVariableName) . chr(10);
175
            foreach ($arguments as $argumentName => $argumentDefinition) {
176
                if (!isset($alreadyBuiltArguments[$argumentName])) {
177
                    $argumentInitializationCode .= sprintf(
178
                        '%s[\'%s\'] = %s;%s',
179
                        $argumentsVariableName,
180
                        $argumentName,
181
                        var_export($argumentDefinition->getDefaultValue(), true),
182
                        chr(10)
183
                    );
184
                }
185
            }
186
187
            $alreadyBuiltArguments = [];
188
            foreach ($node->getArguments() as $argumentName => $argumentValue) {
189
                if ($argumentValue instanceof NodeInterface) {
190
                    $converted = $this->convert($argumentValue);
191
                } else {
192
                    $converted = [
193
                        'initialization' => '',
194
                        'execution' => $argumentValue
195
                    ];
196
                }
197
                $argumentInitializationCode .= $converted['initialization'];
198
                $argumentInitializationCode .= sprintf(
199
                    '%s[\'%s\'] = %s;',
200
                    $argumentsVariableName,
201
                    $argumentName,
202
                    $converted['execution']
203
                ) . chr(10);
204
                $alreadyBuiltArguments[$argumentName] = true;
205
            }
206
207
            // Build up closure which renders the child nodes
208
            $initializationPhpCode .= sprintf(
209
                '%s = %s;',
210
                $renderChildrenClosureVariableName,
211
                $this->templateCompiler->wrapChildNodesInClosure($node)
212
            ) . chr(10);
213
214
            $initializationPhpCode .= $argumentInitializationCode . $viewHelperInitializationPhpCode;
215
        } catch (StopCompilingChildrenException $stopCompilingChildrenException) {
216
            $convertedViewHelperExecutionCode = '\'' . $stopCompilingChildrenException->getReplacementString() . '\'';
217
        }
218
        $initializationArray = [
219
            'initialization' => $initializationPhpCode,
220
            'execution' => $convertedViewHelperExecutionCode === null ? 'NULL' : $convertedViewHelperExecutionCode
221
        ];
222
        return $initializationArray;
223
    }
224
225
    /**
226
     * @param ObjectAccessorNode $node
227
     * @return array
228
     * @see convert()
229
     */
230
    protected function convertObjectAccessorNode(ObjectAccessorNode $node)
231
    {
232
        $arrayVariableName = $this->variableName('array');
233
        $accessors = $node->getAccessors();
234
        $providerReference = '$renderingContext->getVariableProvider()';
235
        $path = $node->getObjectPath();
236
        $pathSegments = explode('.', $path);
237
        if ($path === '_all') {
238
            return [
239
                'initialization' => '',
240
                'execution' => sprintf('%s->getAll()', $providerReference)
241
            ];
242
        } elseif (1 === count(array_unique($accessors))
243
            && reset($accessors) === VariableExtractor::ACCESSOR_ARRAY
244
            && count($accessors) === count($pathSegments)
245
            && false === strpos($path, '{')
246
        ) {
247
            // every extractor used in this path is a straight-forward arrayaccess.
248
            // Create the compiled code as a plain old variable assignment:
249
            return [
250
                'initialization' => '',
251
                'execution' => sprintf(
252
                    'isset(%s[\'%s\']) ? %s[\'%s\'] : NULL',
253
                    $providerReference,
254
                    str_replace('.', '\'][\'', $path),
255
                    $providerReference,
256
                    str_replace('.', '\'][\'', $path)
257
                )
258
            ];
259
        }
260
        $accessorsVariable = var_export($accessors, true);
261
        $initialization = sprintf('%s = %s;', $arrayVariableName, $accessorsVariable);
262
        return [
263
            'initialization' => $initialization,
264
            'execution' => sprintf(
265
                '%s->getByPath(\'%s\', %s)',
266
                $providerReference,
267
                $path,
268
                $arrayVariableName
269
            )
270
        ];
271
    }
272
273
    /**
274
     * @param ArrayNode $node
275
     * @return array
276
     * @see convert()
277
     */
278
    protected function convertArrayNode(ArrayNode $node)
279
    {
280
        $initializationPhpCode = '// Rendering Array' . chr(10);
281
        $arrayVariableName = $this->variableName('array');
282
283
        $initializationPhpCode .= sprintf('%s = array();', $arrayVariableName) . chr(10);
284
285
        foreach ($node->getInternalArray() as $key => $value) {
286
            if ($value instanceof NodeInterface) {
287
                $converted = $this->convert($value);
288
                if (!empty($converted['initialization'])) {
289
                    $initializationPhpCode .= $converted['initialization'];
290
                }
291
                $initializationPhpCode .= sprintf(
292
                    '%s[\'%s\'] = %s;',
293
                    $arrayVariableName,
294
                    $key,
295
                    $converted['execution']
296
                ) . chr(10);
297
            } elseif (is_numeric($value)) {
298
                // this case might happen for simple values
299
                $initializationPhpCode .= sprintf(
300
                    '%s[\'%s\'] = %s;',
301
                    $arrayVariableName,
302
                    $key,
303
                    $value
304
                ) . chr(10);
305
            } else {
306
                // this case might happen for simple values
307
                $initializationPhpCode .= sprintf(
308
                    '%s[\'%s\'] = \'%s\';',
309
                    $arrayVariableName,
310
                    $key,
311
                    $this->escapeTextForUseInSingleQuotes($value)
312
                ) . chr(10);
313
            }
314
        }
315
        return [
316
            'initialization' => $initializationPhpCode,
317
            'execution' => $arrayVariableName
318
        ];
319
    }
320
321
    /**
322
     * @param NodeInterface $node
323
     * @return array
324
     * @see convert()
325
     */
326
    public function convertListOfSubNodes(NodeInterface $node)
327
    {
328
        switch (count($node->getChildNodes())) {
329
            case 0:
330
                return [];
331
            case 1:
332
                $childNode = current($node->getChildNodes());
333
                if ($childNode instanceof NodeInterface) {
334
                    return $this->convert($childNode);
335
                }
336
            default:
337
                $outputVariableName = $this->variableName('output');
338
                $initializationPhpCode = sprintf('%s = \'\';', $outputVariableName) . chr(10);
339
340
                foreach ($node->getChildNodes() as $childNode) {
341
                    $converted = $this->convert($childNode);
342
343
                    if (!empty($converted['initialization'])) {
344
                        $initializationPhpCode .= $converted['initialization'] . chr(10);
345
                    }
346
                    if (!empty($converted['execution'])) {
347
                        $initializationPhpCode .= sprintf('%s .= %s;', $outputVariableName, $converted['execution']) . chr(10);
348
                    }
349
                }
350
351
                return [
352
                    'initialization' => $initializationPhpCode,
353
                    'execution' => $outputVariableName
354
                ];
355
        }
356
    }
357
358
    /**
359
     * @param ExpressionNodeInterface $node
360
     * @return array
361
     * @see convert()
362
     */
363
    protected function convertExpressionNode(ExpressionNodeInterface $node)
364
    {
365
        return $node->compile($this->templateCompiler);
366
    }
367
368
    /**
369
     * @param BooleanNode $node
370
     * @return array
371
     * @see convert()
372
     */
373
    protected function convertBooleanNode(BooleanNode $node)
374
    {
375
        $booleanStack = $node->getStack();
376
        if (count($booleanStack) === 1) {
377
            if (!$booleanStack[0] instanceof NodeInterface) {
378
                return [
379
                    'initialization' => '',
380
                    'execution' => $booleanStack[0] && strtolower((string)$booleanStack[0]) !== 'false' ? 'true' : 'false',
381
                ];
382
            }
383
384
            $compiledOnlyNode = $this->convert($booleanStack[0]);
385
            return [
386
                'initialization' => $compiledOnlyNode['initialization'],
387
                'execution' => '(bool)(' . ($compiledOnlyNode['execution'] ? $compiledOnlyNode['execution'] : 'false') . ')',
388
            ];
389
        }
390
391
        $stack = $this->convertArrayNode(new ArrayNode($booleanStack));
392
        $initializationPhpCode .= $stack['initialization'] . chr(10);
0 ignored issues
show
Bug introduced by
The variable $initializationPhpCode does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
393
394
        $parser = new BooleanParser();
395
        $compiledExpression = $parser->compile(BooleanNode::reconcatenateExpression($node->getStack()));
396
        $functionName = $this->variableName('expression');
397
        $initializationPhpCode .= $functionName . ' = function($context) {return ' . $compiledExpression . ';};' . chr(10);
398
399
        return [
400
            'initialization' => $initializationPhpCode,
401
            'execution' => sprintf(
402
                '%s::convertToBoolean(
403
					%s(
404
						%s::gatherContext($renderingContext, %s)
405
					),
406
					$renderingContext
407
				)',
408
                BooleanNode::class,
409
                $functionName,
410
                BooleanNode::class,
411
                $stack['execution']
412
            )
413
        ];
414
    }
415
416
    /**
417
     * @param string $text
418
     * @return string
419
     */
420
    protected function escapeTextForUseInSingleQuotes($text)
421
    {
422
        return str_replace(['\\', '\''], ['\\\\', '\\\''], $text);
423
    }
424
425
    /**
426
     * Returns a unique variable name by appending a global index to the given prefix
427
     *
428
     * @param string $prefix
429
     * @return string
430
     */
431
    public function variableName($prefix)
432
    {
433
        return '$' . $prefix . $this->variableCounter++;
434
    }
435
}
436