Completed
Pull Request — master (#440)
by Claus
02:09
created

NodeConverter   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 426
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
dl 0
loc 426
rs 6
c 0
b 0
f 0
wmc 55
lcom 1
cbo 13

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A setVariableCounter() 0 4 1
B convert() 0 38 10
A convertTextNode() 0 7 1
A convertNumericNode() 0 7 1
B convertViewHelperNode() 0 69 7
B convertObjectAccessorNode() 0 42 6
B convertArrayNode() 0 42 5
B convertListOfSubNodes() 0 34 7
A convertExpressionNode() 0 4 1
A convertEscapingNode() 0 13 4
A escapeTextForUseInSingleQuotes() 0 4 1
A variableName() 0 4 1
B convertBooleanNode() 0 54 9

How to fix   Complexity   

Complex Class

Complex classes like NodeConverter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use NodeConverter, and based on these observations, apply Extract Interface, too.

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']) && strtolower($configuration['execution']) !== 'null' && !is_numeric(trim($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 = '\'' . str_replace("'", "\'", $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
                    'initialization' => '',
332
                    'execution' => 'NULL',
333
                ];
334
            case 1:
335
                $childNode = current($node->getChildNodes());
336
                if ($childNode instanceof NodeInterface) {
337
                    return $this->convert($childNode);
338
                }
339
            default:
340
                $outputVariableName = $this->variableName('output');
341
                $initializationPhpCode = sprintf('%s = \'\';', $outputVariableName) . chr(10);
342
343
                foreach ($node->getChildNodes() as $childNode) {
344
                    $converted = $this->convert($childNode);
345
346
                    if (!empty($converted['initialization'])) {
347
                        $initializationPhpCode .= $converted['initialization'] . chr(10);
348
                    }
349
                    if (!empty($converted['execution'])) {
350
                        $initializationPhpCode .= sprintf('%s .= %s;', $outputVariableName, $converted['execution']) . chr(10);
351
                    }
352
                }
353
354
                return [
355
                    'initialization' => $initializationPhpCode,
356
                    'execution' => $outputVariableName
357
                ];
358
        }
359
    }
360
361
    /**
362
     * @param ExpressionNodeInterface $node
363
     * @return array
364
     * @see convert()
365
     */
366
    protected function convertExpressionNode(ExpressionNodeInterface $node)
367
    {
368
        return $node->compile($this->templateCompiler);
369
    }
370
371
    /**
372
     * @param BooleanNode $node
373
     * @return array
374
     * @see convert()
375
     */
376
    protected function convertBooleanNode(BooleanNode $node)
377
    {
378
        $booleanStack = $node->getStack();
379
380
        // Quick decisions: if there is only one node and it can be determined to be a string/numeric/boolean already
381
        // then use it directly.
382
        if (count($booleanStack) === 1) {
383
            $compiledOnlyNode = $this->convert($booleanStack[0]);
384
            // Execution without possible quotation marks, then converted to lowercase for easier comparison.
385
            $execution = trim($compiledOnlyNode['execution'], '"\'\\');
386
            $lowercaseExecution = strtolower($execution);
387
            if ($lowercaseExecution === 'true') {
388
                $execution = 'true';
389
            } elseif ($lowercaseExecution === 'false' || empty($execution)) {
390
                $execution = 'false';
391
            } elseif (is_numeric($execution) || is_bool($execution)) {
392
                $execution = $execution ? 'true' : 'false';
393
            } else {
394
                $execution = null;
395
            }
396
            if ($execution !== null) {
397
                // Execution was re-written by one of the above cases, so we return this different execution. If it
398
                // was not rewritten we fall through and proceed with the stack-based strategy below.
399
                return [
400
                    'initialization' => $compiledOnlyNode['initialization'],
401
                    'execution' => $execution,
402
                ];
403
            }
404
        }
405
406
        $stack = $this->convertArrayNode(new ArrayNode($booleanStack));
407
        $initializationPhpCode = $stack['initialization'] . chr(10);
408
409
        $parser = new BooleanParser();
410
        $compiledExpression = $parser->compile(BooleanNode::reconcatenateExpression($node->getStack()));
411
        $functionName = $this->variableName('expression');
412
        $initializationPhpCode .= $functionName . ' = function($context) {return ' . $compiledExpression . ';};' . chr(10);
413
414
        return [
415
            'initialization' => $initializationPhpCode,
416
            'execution' => sprintf(
417
                '%s::convertToBoolean(
418
					%s(
419
						%s::gatherContext($renderingContext, %s)
420
					),
421
					$renderingContext
422
				)',
423
                BooleanNode::class,
424
                $functionName,
425
                BooleanNode::class,
426
                $stack['execution']
427
            )
428
        ];
429
    }
430
431
    /**
432
     * @param string $text
433
     * @return string
434
     */
435
    protected function escapeTextForUseInSingleQuotes($text)
436
    {
437
        return str_replace(['\\', '\''], ['\\\\', '\\\''], $text);
438
    }
439
440
    /**
441
     * Returns a unique variable name by appending a global index to the given prefix
442
     *
443
     * @param string $prefix
444
     * @return string
445
     */
446
    public function variableName($prefix)
447
    {
448
        return '$' . $prefix . $this->variableCounter++;
449
    }
450
}
451