Completed
Pull Request — master (#446)
by Claus
01:49
created

NodeConverter::convertArrayNode()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 1
dl 0
loc 44
rs 8.9048
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
        $configuration['execution'] = sprintf(
111
            'call_user_func_array( function ($var) { ' .
112
            'return (is_string($var) || (is_object($var) && method_exists($var, \'__toString\')) ' .
113
            '? htmlspecialchars((string) $var, ENT_QUOTES) : $var); }, [%s])',
114
            $configuration['execution']
115
        );
116
        return $configuration;
117
    }
118
119
    /**
120
     * @param TextNode $node
121
     * @return array
122
     * @see convert()
123
     */
124
    protected function convertTextNode(TextNode $node)
125
    {
126
        return [
127
            'initialization' => '',
128
            'execution' => '\'' . $this->escapeTextForUseInSingleQuotes($node->getText()) . '\''
129
        ];
130
    }
131
132
    /**
133
     * @param NumericNode $node
134
     * @return array
135
     * @see convert()
136
     */
137
    protected function convertNumericNode(NumericNode $node)
138
    {
139
        return [
140
            'initialization' => '',
141
            'execution' => $node->getValue()
142
        ];
143
    }
144
145
    /**
146
     * Convert a single ViewHelperNode into its cached representation. If the ViewHelper implements the "Compilable" facet,
147
     * the ViewHelper itself is asked for its cached PHP code representation. If not, a ViewHelper is built and then invoked.
148
     *
149
     * @param ViewHelperNode $node
150
     * @return array
151
     * @see convert()
152
     */
153
    protected function convertViewHelperNode(ViewHelperNode $node)
154
    {
155
        $initializationPhpCode = '// Rendering ViewHelper ' . $node->getViewHelperClassName() . chr(10);
156
157
        // Build up $arguments array
158
        $argumentsVariableName = $this->variableName('arguments');
159
        $renderChildrenClosureVariableName = $this->variableName('renderChildrenClosure');
160
        $viewHelperInitializationPhpCode = '';
161
162
        try {
163
            $convertedViewHelperExecutionCode = $node->getUninitializedViewHelper()->compile(
164
                $argumentsVariableName,
165
                $renderChildrenClosureVariableName,
166
                $viewHelperInitializationPhpCode,
167
                $node,
168
                $this->templateCompiler
169
            );
170
171
            $accumulatedArgumentInitializationCode = '';
172
            $argumentInitializationCode = sprintf('%s = [', $argumentsVariableName);
173
174
            $arguments = $node->getArguments();
175
            foreach ($arguments as $argumentName => $argumentValue) {
176
                if ($argumentValue instanceof NodeInterface) {
177
                    $converted = $this->convert($argumentValue);
178
                } else {
179
                    $converted = [
180
                        'initialization' => '',
181
                        'execution' => $argumentValue
182
                    ];
183
                }
184
                $accumulatedArgumentInitializationCode .= $converted['initialization'];
185
                $argumentInitializationCode .= sprintf(
186
                    '\'%s\' => %s',
187
                    $argumentName,
188
                    $converted['execution']
189
                );
190
191
                $argumentInitializationCode .= ', ';
192
            }
193
194
            foreach ($node->getArgumentDefinitions() as $argumentName => $argumentDefinition) {
195
                if (!isset($arguments[$argumentName])) {
196
                    $defaultValue = $argumentDefinition->getDefaultValue();
197
                    $argumentInitializationCode .= sprintf(
198
                        '\'%s\' => %s',
199
                        $argumentName,
200
                        is_array($defaultValue) && empty($defaultValue) ? '[]' : var_export($defaultValue, true)
201
                    );
202
                    $argumentInitializationCode .= ', ';
203
                }
204
            }
205
206
            $argumentInitializationCode = rtrim($argumentInitializationCode, ', ');
207
            $argumentInitializationCode .= '];' . PHP_EOL;
208
209
            $initializationPhpCode = '// Rendering ViewHelper ' . $node->getViewHelperClassName() . chr(10);
210
211
            // Build up closure which renders the child nodes
212
            $initializationPhpCode .= sprintf(
213
                '%s = %s;',
214
                $renderChildrenClosureVariableName,
215
                $this->templateCompiler->wrapChildNodesInClosure($node)
216
            ) . chr(10);
217
218
            $initializationPhpCode .= $accumulatedArgumentInitializationCode . PHP_EOL . $argumentInitializationCode . $viewHelperInitializationPhpCode;
219
        } catch (StopCompilingChildrenException $stopCompilingChildrenException) {
220
            $convertedViewHelperExecutionCode = '\'' . str_replace("'", "\'", $stopCompilingChildrenException->getReplacementString()) . '\'';
221
        }
222
        $initializationArray = [
223
            'initialization' => $initializationPhpCode,
224
            'execution' => $convertedViewHelperExecutionCode === null ? 'NULL' : $convertedViewHelperExecutionCode
225
        ];
226
        return $initializationArray;
227
    }
228
229
    /**
230
     * @param ObjectAccessorNode $node
231
     * @return array
232
     * @see convert()
233
     */
234
    protected function convertObjectAccessorNode(ObjectAccessorNode $node)
235
    {
236
        $arrayVariableName = $this->variableName('array');
237
        $accessors = $node->getAccessors();
238
        $providerReference = '$renderingContext->getVariableProvider()';
239
        $path = $node->getObjectPath();
240
        $pathSegments = explode('.', $path);
241
        if ($path === '_all') {
242
            return [
243
                'initialization' => '',
244
                'execution' => sprintf('%s->getAll()', $providerReference)
245
            ];
246
        } elseif (1 === count(array_unique($accessors))
247
            && reset($accessors) === VariableExtractor::ACCESSOR_ARRAY
248
            && count($accessors) === count($pathSegments)
249
            && false === strpos($path, '{')
250
        ) {
251
            // every extractor used in this path is a straight-forward arrayaccess.
252
            // Create the compiled code as a plain old variable assignment:
253
            return [
254
                'initialization' => '',
255
                'execution' => sprintf(
256
                    'isset(%s[\'%s\']) ? %s[\'%s\'] : NULL',
257
                    $providerReference,
258
                    str_replace('.', '\'][\'', $path),
259
                    $providerReference,
260
                    str_replace('.', '\'][\'', $path)
261
                )
262
            ];
263
        }
264
        $accessorsVariable = var_export($accessors, true);
265
        $initialization = sprintf('%s = %s;', $arrayVariableName, $accessorsVariable);
266
        return [
267
            'initialization' => $initialization,
268
            'execution' => sprintf(
269
                '%s->getByPath(\'%s\', %s)',
270
                $providerReference,
271
                $path,
272
                $arrayVariableName
273
            )
274
        ];
275
    }
276
277
    /**
278
     * @param ArrayNode $node
279
     * @return array
280
     * @see convert()
281
     */
282
    protected function convertArrayNode(ArrayNode $node)
283
    {
284
        $arrayVariableName = $this->variableName('array');
285
286
        $accumulatedInitializationPhpCode = '';
287
        $initializationPhpCode = sprintf('%s = [', $arrayVariableName);
288
289
        foreach ($node->getInternalArray() as $key => $value) {
290
            if ($value instanceof NodeInterface) {
291
                $converted = $this->convert($value);
292
                if (!empty($converted['initialization'])) {
293
                    $accumulatedInitializationPhpCode .= $converted['initialization'];
294
                }
295
                $initializationPhpCode .= sprintf(
296
                    '\'%s\' => %s',
297
                    $key,
298
                    $converted['execution']
299
                );
300
            } elseif (is_numeric($value)) {
301
                // this case might happen for simple values
302
                $initializationPhpCode .= sprintf(
303
                    '\'%s\' => %s',
304
                    $key,
305
                    $value
306
                );
307
            } else {
308
                // this case might happen for simple values
309
                $initializationPhpCode .= sprintf(
310
                    '\'%s\' => \'%s\'',
311
                    $key,
312
                    $this->escapeTextForUseInSingleQuotes($value)
313
                );
314
            }
315
            $initializationPhpCode .= ', ';
316
        }
317
318
        $initializationPhpCode = rtrim($initializationPhpCode, ', ');
319
        $initializationPhpCode .= '];' . PHP_EOL;
320
321
        return [
322
            'initialization' => $accumulatedInitializationPhpCode . PHP_EOL . $initializationPhpCode,
323
            'execution' => $arrayVariableName
324
        ];
325
    }
326
327
    /**
328
     * @param NodeInterface $node
329
     * @return array
330
     * @see convert()
331
     */
332
    public function convertListOfSubNodes(NodeInterface $node)
333
    {
334
        switch (count($node->getChildNodes())) {
335
            case 0:
336
                return [
337
                    'initialization' => '',
338
                    'execution' => 'NULL'
339
                ];
340
            case 1:
341
                $childNode = current($node->getChildNodes());
342
                if ($childNode instanceof NodeInterface) {
343
                    return $this->convert($childNode);
344
                }
345
            default:
346
                $outputVariableName = $this->variableName('output');
347
                $initializationPhpCode = sprintf('%s = \'\';', $outputVariableName) . chr(10);
348
349
                foreach ($node->getChildNodes() as $childNode) {
350
                    $converted = $this->convert($childNode);
351
352
                    $initializationPhpCode .= $converted['initialization'] . chr(10);
353
                    $initializationPhpCode .= sprintf('%s .= %s;', $outputVariableName, $converted['execution']) . chr(10);
354
                }
355
356
                return [
357
                    'initialization' => $initializationPhpCode,
358
                    'execution' => $outputVariableName
359
                ];
360
        }
361
    }
362
363
    /**
364
     * @param ExpressionNodeInterface $node
365
     * @return array
366
     * @see convert()
367
     */
368
    protected function convertExpressionNode(ExpressionNodeInterface $node)
369
    {
370
        return $node->compile($this->templateCompiler);
371
    }
372
373
    /**
374
     * @param BooleanNode $node
375
     * @return array
376
     * @see convert()
377
     */
378
    protected function convertBooleanNode(BooleanNode $node)
379
    {
380
        $stack = $this->convertArrayNode(new ArrayNode($node->getStack()));
381
        $initializationPhpCode = '// Rendering Boolean node' . chr(10);
382
        $initializationPhpCode .= $stack['initialization'] . chr(10);
383
384
        $parser = new BooleanParser();
385
        $compiledExpression = $parser->compile(BooleanNode::reconcatenateExpression($node->getStack()));
386
        $functionName = $this->variableName('expression');
387
        $initializationPhpCode .= $functionName . ' = function($context) {return ' . $compiledExpression . ';};' . chr(10);
388
389
        return [
390
            'initialization' => $initializationPhpCode,
391
            'execution' => sprintf(
392
                '%s::convertToBoolean(
393
					%s(
394
						%s::gatherContext($renderingContext, %s)
395
					),
396
					$renderingContext
397
				)',
398
                BooleanNode::class,
399
                $functionName,
400
                BooleanNode::class,
401
                $stack['execution']
402
            )
403
        ];
404
    }
405
406
    /**
407
     * @param string $text
408
     * @return string
409
     */
410
    protected function escapeTextForUseInSingleQuotes($text)
411
    {
412
        return str_replace(['\\', '\''], ['\\\\', '\\\''], $text);
413
    }
414
415
    /**
416
     * Returns a unique variable name by appending a global index to the given prefix
417
     *
418
     * @param string $prefix
419
     * @return string
420
     */
421
    public function variableName($prefix)
422
    {
423
        return '$' . $prefix . $this->variableCounter++;
424
    }
425
}
426