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

NodeConverter   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
dl 0
loc 402
rs 8.8798
c 0
b 0
f 0
wmc 44
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 convertEscapingNode() 0 11 1
A convertTextNode() 0 7 1
A convertNumericNode() 0 7 1
C convertViewHelperNode() 0 76 9
B convertObjectAccessorNode() 0 42 6
B convertArrayNode() 0 44 5
A convertListOfSubNodes() 0 30 5
A convertExpressionNode() 0 4 1
A convertBooleanNode() 0 27 1
A escapeTextForUseInSingleQuotes() 0 4 1
A variableName() 0 4 1

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
        $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
            $alreadyBuiltArguments = [];
0 ignored issues
show
Unused Code introduced by
$alreadyBuiltArguments 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...
176
            foreach ($arguments as $argumentName => $argumentValue) {
177
                if ($argumentValue instanceof NodeInterface) {
178
                    $converted = $this->convert($argumentValue);
179
                } else {
180
                    $converted = [
181
                        'initialization' => '',
182
                        'execution' => $argumentValue
183
                    ];
184
                }
185
                $accumulatedArgumentInitializationCode .= $converted['initialization'];
186
                $argumentInitializationCode .= sprintf(
187
                    '\'%s\' => %s',
188
                    $argumentName,
189
                    $converted['execution']
190
                );
191
192
                $argumentInitializationCode .= ', ';
193
            }
194
195
            foreach ($node->getArgumentDefinitions() as $argumentName => $argumentDefinition) {
196
                if (!isset($arguments[$argumentName])) {
197
                    $defaultValue = $argumentDefinition->getDefaultValue();
198
                    $argumentInitializationCode .= sprintf(
199
                        '\'%s\' => %s',
200
                        $argumentName,
201
                        is_array($defaultValue) && empty($defaultValue) ? '[]' : var_export($defaultValue, true)
202
                    );
203
                    $argumentInitializationCode .= ', ';
204
                }
205
            }
206
207
            $argumentInitializationCode = rtrim($argumentInitializationCode, ', ');
208
            $argumentInitializationCode .= '];' . PHP_EOL;
209
210
            $initializationPhpCode = '// Rendering ViewHelper ' . $node->getViewHelperClassName() . chr(10);
211
212
            // Build up closure which renders the child nodes
213
            $initializationPhpCode .= sprintf(
214
                '%s = %s;',
215
                $renderChildrenClosureVariableName,
216
                $this->templateCompiler->wrapChildNodesInClosure($node)
217
            ) . chr(10);
218
219
            $initializationPhpCode .= $accumulatedArgumentInitializationCode . PHP_EOL . $argumentInitializationCode . $viewHelperInitializationPhpCode;
220
        } catch (StopCompilingChildrenException $stopCompilingChildrenException) {
221
            $convertedViewHelperExecutionCode = '\'' . $stopCompilingChildrenException->getReplacementString() . '\'';
222
        }
223
        $initializationArray = [
224
            'initialization' => $initializationPhpCode,
225
            'execution' => $convertedViewHelperExecutionCode === null ? 'NULL' : $convertedViewHelperExecutionCode
226
        ];
227
        return $initializationArray;
228
    }
229
230
    /**
231
     * @param ObjectAccessorNode $node
232
     * @return array
233
     * @see convert()
234
     */
235
    protected function convertObjectAccessorNode(ObjectAccessorNode $node)
236
    {
237
        $arrayVariableName = $this->variableName('array');
238
        $accessors = $node->getAccessors();
239
        $providerReference = '$renderingContext->getVariableProvider()';
240
        $path = $node->getObjectPath();
241
        $pathSegments = explode('.', $path);
242
        if ($path === '_all') {
243
            return [
244
                'initialization' => '',
245
                'execution' => sprintf('%s->getAll()', $providerReference)
246
            ];
247
        } elseif (1 === count(array_unique($accessors))
248
            && reset($accessors) === VariableExtractor::ACCESSOR_ARRAY
249
            && count($accessors) === count($pathSegments)
250
            && false === strpos($path, '{')
251
        ) {
252
            // every extractor used in this path is a straight-forward arrayaccess.
253
            // Create the compiled code as a plain old variable assignment:
254
            return [
255
                'initialization' => '',
256
                'execution' => sprintf(
257
                    'isset(%s[\'%s\']) ? %s[\'%s\'] : NULL',
258
                    $providerReference,
259
                    str_replace('.', '\'][\'', $path),
260
                    $providerReference,
261
                    str_replace('.', '\'][\'', $path)
262
                )
263
            ];
264
        }
265
        $accessorsVariable = var_export($accessors, true);
266
        $initialization = sprintf('%s = %s;', $arrayVariableName, $accessorsVariable);
267
        return [
268
            'initialization' => $initialization,
269
            'execution' => sprintf(
270
                '%s->getByPath(\'%s\', %s)',
271
                $providerReference,
272
                $path,
273
                $arrayVariableName
274
            )
275
        ];
276
    }
277
278
    /**
279
     * @param ArrayNode $node
280
     * @return array
281
     * @see convert()
282
     */
283
    protected function convertArrayNode(ArrayNode $node)
284
    {
285
        $arrayVariableName = $this->variableName('array');
286
287
        $accumulatedInitializationPhpCode = '';
288
        $initializationPhpCode = sprintf('%s = [', $arrayVariableName);
289
290
        foreach ($node->getInternalArray() as $key => $value) {
291
            if ($value instanceof NodeInterface) {
292
                $converted = $this->convert($value);
293
                if (!empty($converted['initialization'])) {
294
                    $accumulatedInitializationPhpCode .= $converted['initialization'];
295
                }
296
                $initializationPhpCode .= sprintf(
297
                    '\'%s\' => %s',
298
                    $key,
299
                    $converted['execution']
300
                );
301
            } elseif (is_numeric($value)) {
302
                // this case might happen for simple values
303
                $initializationPhpCode .= sprintf(
304
                    '\'%s\' => %s',
305
                    $key,
306
                    $value
307
                );
308
            } else {
309
                // this case might happen for simple values
310
                $initializationPhpCode .= sprintf(
311
                    '\'%s\' => \'%s\'',
312
                    $key,
313
                    $this->escapeTextForUseInSingleQuotes($value)
314
                );
315
            }
316
            $initializationPhpCode .= ', ';
317
        }
318
319
        $initializationPhpCode = rtrim($initializationPhpCode, ', ');
320
        $initializationPhpCode .= '];' . PHP_EOL;
321
322
        return [
323
            'initialization' => $accumulatedInitializationPhpCode . PHP_EOL . $initializationPhpCode,
324
            'execution' => $arrayVariableName
325
        ];
326
    }
327
328
    /**
329
     * @param NodeInterface $node
330
     * @return array
331
     * @see convert()
332
     */
333
    public function convertListOfSubNodes(NodeInterface $node)
334
    {
335
        switch (count($node->getChildNodes())) {
336
            case 0:
337
                return [
338
                    'initialization' => '',
339
                    'execution' => 'NULL'
340
                ];
341
            case 1:
342
                $childNode = current($node->getChildNodes());
343
                if ($childNode instanceof NodeInterface) {
344
                    return $this->convert($childNode);
345
                }
346
            default:
347
                $outputVariableName = $this->variableName('output');
348
                $initializationPhpCode = sprintf('%s = \'\';', $outputVariableName) . chr(10);
349
350
                foreach ($node->getChildNodes() as $childNode) {
351
                    $converted = $this->convert($childNode);
352
353
                    $initializationPhpCode .= $converted['initialization'] . chr(10);
354
                    $initializationPhpCode .= sprintf('%s .= %s;', $outputVariableName, $converted['execution']) . chr(10);
355
                }
356
357
                return [
358
                    'initialization' => $initializationPhpCode,
359
                    'execution' => $outputVariableName
360
                ];
361
        }
362
    }
363
364
    /**
365
     * @param ExpressionNodeInterface $node
366
     * @return array
367
     * @see convert()
368
     */
369
    protected function convertExpressionNode(ExpressionNodeInterface $node)
370
    {
371
        return $node->compile($this->templateCompiler);
372
    }
373
374
    /**
375
     * @param BooleanNode $node
376
     * @return array
377
     * @see convert()
378
     */
379
    protected function convertBooleanNode(BooleanNode $node)
380
    {
381
        $stack = $this->convertArrayNode(new ArrayNode($node->getStack()));
382
        $initializationPhpCode = '// Rendering Boolean node' . chr(10);
383
        $initializationPhpCode .= $stack['initialization'] . chr(10);
384
385
        $parser = new BooleanParser();
386
        $compiledExpression = $parser->compile(BooleanNode::reconcatenateExpression($node->getStack()));
387
        $functionName = $this->variableName('expression');
388
        $initializationPhpCode .= $functionName . ' = function($context) {return ' . $compiledExpression . ';};' . chr(10);
389
390
        return [
391
            'initialization' => $initializationPhpCode,
392
            'execution' => sprintf(
393
                '%s::convertToBoolean(
394
					%s(
395
						%s::gatherContext($renderingContext, %s)
396
					),
397
					$renderingContext
398
				)',
399
                BooleanNode::class,
400
                $functionName,
401
                BooleanNode::class,
402
                $stack['execution']
403
            )
404
        ];
405
    }
406
407
    /**
408
     * @param string $text
409
     * @return string
410
     */
411
    protected function escapeTextForUseInSingleQuotes($text)
412
    {
413
        return str_replace(['\\', '\''], ['\\\\', '\\\''], $text);
414
    }
415
416
    /**
417
     * Returns a unique variable name by appending a global index to the given prefix
418
     *
419
     * @param string $prefix
420
     * @return string
421
     */
422
    public function variableName($prefix)
423
    {
424
        return '$' . $prefix . $this->variableCounter++;
425
    }
426
}
427