Completed
Pull Request — master (#457)
by Claus
02:31
created

NodeConverter   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 434
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
dl 0
loc 434
rs 8.64
c 0
b 0
f 0
wmc 47
lcom 1
cbo 14

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A setVariableCounter() 0 4 1
C convert() 0 48 12
A convertEscapingNode() 0 11 1
A convertTextNode() 0 7 1
A convertNumericNode() 0 7 1
A convertViewHelper() 0 8 1
C convertViewHelperNode() 0 75 9
B convertObjectAccessorNode() 0 42 6
A convertArrayNode() 0 4 1
A convertArray() 0 40 4
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\Exception;
10
use TYPO3Fluid\Fluid\Core\Parser\BooleanParser;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\EscapingNode;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
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\PostponedViewHelperNode;
19
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
20
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
21
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
22
use TYPO3Fluid\Fluid\Core\Variables\VariableExtractor;
23
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface;
24
25
/**
26
 * Class NodeConverter
27
 */
28
class NodeConverter
29
{
30
31
    /**
32
     * @var integer
33
     */
34
    protected $variableCounter = 0;
35
36
    /**
37
     * @var TemplateCompiler
38
     */
39
    protected $templateCompiler;
40
41
    /**
42
     * @param TemplateCompiler $templateCompiler
43
     */
44
    public function __construct(TemplateCompiler $templateCompiler)
45
    {
46
        $this->templateCompiler = $templateCompiler;
47
    }
48
49
    /**
50
     * @param integer $variableCounter
51
     * @return void
52
     */
53
    public function setVariableCounter($variableCounter)
54
    {
55
        $this->variableCounter = $variableCounter;
56
    }
57
58
    /**
59
     * Returns an array with two elements:
60
     * - initialization: contains PHP code which is inserted *before* the actual rendering call. Must be valid, i.e. end with semi-colon.
61
     * - execution: contains *a single PHP instruction* which needs to return the rendered output of the given element. Should NOT end with semi-colon.
62
     *
63
     * @param NodeInterface $node
64
     * @return array two-element array, see above
65
     * @throws Exception
66
     */
67
    public function convert(NodeInterface $node)
68
    {
69
        switch (true) {
70
            case $node instanceof TextNode:
71
                $converted = $this->convertTextNode($node);
72
                break;
73
            case $node instanceof ExpressionNodeInterface:
74
                $converted = $this->convertExpressionNode($node);
75
                break;
76
            case $node instanceof NumericNode:
77
                $converted = $this->convertNumericNode($node);
78
                break;
79
            case $node instanceof ViewHelperInterface:
80
                $converted = $this->convertViewHelper($node);
81
                break;
82
            case $node instanceof ViewHelperNode:
83
                $converted = $this->convertViewHelperNode($node);
84
                break;
85
            case $node instanceof ObjectAccessorNode:
86
                // Flatten the node, which will return either the single child node or the node itself if it has no
87
                // children. If the resulting node is a ViewHelper, compile that ViewHelper instead.
88
                $flattened = $node->flatten();
89
                if ($flattened instanceof ViewHelperInterface) {
90
                    $converted = $this->convert($flattened);
91
                } else {
92
                    $converted = $this->convertObjectAccessorNode($node);
93
                }
94
                break;
95
            case $node instanceof ArrayNode:
96
                $converted = $this->convertArrayNode($node);
97
                break;
98
            case $node instanceof RootNode:
99
                $converted = $this->convertListOfSubNodes($node);
100
                break;
101
            case $node instanceof BooleanNode:
102
                $converted = $this->convertBooleanNode($node);
103
                break;
104
            case $node instanceof EscapingNode:
105
                $converted = $this->convertEscapingNode($node);
106
                break;
107
            default:
108
                $converted = [
109
                    'initialization' => '// Uncompilable/convertible node type: ' . get_class($node) . chr(10),
110
                    'execution' => ''
111
                ];
112
        }
113
        return $converted;
114
    }
115
116
    /**
117
     * @param EscapingNode $node
118
     * @return array
119
     */
120
    protected function convertEscapingNode(EscapingNode $node)
121
    {
122
        $configuration = $this->convert($node->getNode());
123
        $configuration['execution'] = sprintf(
124
            'call_user_func_array( function ($var) { ' .
125
            'return (is_string($var) || (is_object($var) && method_exists($var, \'__toString\')) ' .
126
            '? htmlspecialchars((string) $var, ENT_QUOTES) : $var); }, [%s])',
127
            $configuration['execution']
128
        );
129
        return $configuration;
130
    }
131
132
    /**
133
     * @param TextNode $node
134
     * @return array
135
     * @see convert()
136
     */
137
    protected function convertTextNode(TextNode $node)
138
    {
139
        return [
140
            'initialization' => '',
141
            'execution' => '\'' . $this->escapeTextForUseInSingleQuotes($node->getText()) . '\''
142
        ];
143
    }
144
145
    /**
146
     * @param NumericNode $node
147
     * @return array
148
     * @see convert()
149
     */
150
    protected function convertNumericNode(NumericNode $node)
151
    {
152
        return [
153
            'initialization' => '',
154
            'execution' => $node->getValue()
155
        ];
156
    }
157
158
    /**
159
     * Convert a single ViewHelperNode into its cached representation. If the ViewHelper implements the "Compilable" facet,
160
     * the ViewHelper itself is asked for its cached PHP code representation. If not, a ViewHelper is built and then invoked.
161
     *
162
     * @param ViewHelperNode $node
0 ignored issues
show
Bug introduced by
There is no parameter named $node. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
163
     * @return array
164
     * @see convert()
165
     */
166
    protected function convertViewHelper(ViewHelperInterface $viewHelper)
167
    {
168
        $viewHelperNode = new ViewHelperNodeProxy($this->templateCompiler->getRenderingContext());
169
        $viewHelperNode->setArguments($viewHelper->getParsedArguments());
170
        $viewHelperNode->setUninitializedViewHelper($viewHelper);
171
        $viewHelperNode->setChildNodes($viewHelper->getChildNodes());
172
        return $this->convertViewHelperNode($viewHelperNode);
173
    }
174
175
    /**
176
     * Convert a single ViewHelperNode into its cached representation. If the ViewHelper implements the "Compilable" facet,
177
     * the ViewHelper itself is asked for its cached PHP code representation. If not, a ViewHelper is built and then invoked.
178
     *
179
     * @param ViewHelperNode $node
180
     * @return array
181
     * @see convert()
182
     */
183
    protected function convertViewHelperNode(ViewHelperNode $node)
184
    {
185
        $initializationPhpCode = '// Rendering ViewHelper ' . $node->getViewHelperClassName() . chr(10);
186
187
        // Build up $arguments array
188
        $argumentsVariableName = $this->variableName('arguments');
189
        $renderChildrenClosureVariableName = $this->variableName('renderChildrenClosure');
190
        $viewHelperInitializationPhpCode = '';
191
192
        try {
193
            $viewHelper = $node->getUninitializedViewHelper();
194
            $convertedViewHelperExecutionCode = $viewHelper->compile(
195
                $argumentsVariableName,
196
                $renderChildrenClosureVariableName,
197
                $viewHelperInitializationPhpCode,
198
                $node,
199
                $this->templateCompiler
200
            );
201
202
            $arguments = $viewHelper->prepareArguments();
203
            $argumentInitializationCode = sprintf('%s = array();', $argumentsVariableName) . chr(10);
204
205
            $alreadyBuiltArguments = [];
206
            foreach ($node->getArguments() as $argumentName => $argumentValue) {
207
                if ($argumentValue instanceof NodeInterface) {
208
                    $converted = $this->convert($argumentValue);
209
                } elseif (is_numeric($argumentValue)) {
210
                    // this case might happen for simple values
211
                    $converted['execution'] = $argumentValue + 0;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$converted was never initialized. Although not strictly required by PHP, it is generally a good practice to add $converted = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
212
                } elseif (is_array($argumentValue)) {
213
                    $converted = $this->convertArray($argumentValue);
214
                } else {
215
                    // this case might happen for simple values
216
                    $converted['initialization'] = '';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$converted was never initialized. Although not strictly required by PHP, it is generally a good practice to add $converted = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
217
                    $converted['execution'] = var_export($argumentValue, true);
0 ignored issues
show
Bug introduced by
The variable $converted does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
218
                }
219
                $argumentInitializationCode .= ($converted['initialization'] ?? '');
220
                $argumentInitializationCode .= sprintf(
221
                    '%s[\'%s\'] = %s;',
222
                    $argumentsVariableName,
223
                    $argumentName,
224
                    $converted['execution']
225
                ) . chr(10);
226
                $alreadyBuiltArguments[$argumentName] = true;
227
            }
228
229
            foreach ($arguments as $argumentName => $argumentDefinition) {
230
                if (!isset($alreadyBuiltArguments[$argumentName])) {
231
                    $argumentInitializationCode .= sprintf(
232
                        '%s[\'%s\'] = %s;%s',
233
                        $argumentsVariableName,
234
                        $argumentName,
235
                        var_export($argumentDefinition->getDefaultValue(), true),
236
                        chr(10)
237
                    );
238
                }
239
            }
240
241
            // Build up closure which renders the child nodes
242
            $initializationPhpCode .= sprintf(
243
                '%s = %s;',
244
                $renderChildrenClosureVariableName,
245
                $this->templateCompiler->wrapChildNodesInClosure($node)
246
            ) . chr(10);
247
248
            $initializationPhpCode .= $argumentInitializationCode . $viewHelperInitializationPhpCode;
249
        } catch (StopCompilingChildrenException $stopCompilingChildrenException) {
250
            $convertedViewHelperExecutionCode = '\'' . $stopCompilingChildrenException->getReplacementString() . '\'';
251
        }
252
        $initializationArray = [
253
            'initialization' => $initializationPhpCode,
254
            'execution' => $convertedViewHelperExecutionCode === null ? 'NULL' : $convertedViewHelperExecutionCode
255
        ];
256
        return $initializationArray;
257
    }
258
259
    /**
260
     * @param ObjectAccessorNode $node
261
     * @return array
262
     * @see convert()
263
     */
264
    protected function convertObjectAccessorNode(ObjectAccessorNode $node)
265
    {
266
        $arrayVariableName = $this->variableName('array');
267
        $accessors = $node->getAccessors();
268
        $providerReference = '$renderingContext->getVariableProvider()';
269
        $path = $node->getObjectPath();
270
        $pathSegments = explode('.', $path);
271
        if ($path === '_all') {
272
            return [
273
                'initialization' => '',
274
                'execution' => sprintf('%s->getAll()', $providerReference)
275
            ];
276
        } elseif (1 === count(array_unique($accessors))
277
            && reset($accessors) === VariableExtractor::ACCESSOR_ARRAY
278
            && count($accessors) === count($pathSegments)
279
            && false === strpos($path, '{')
280
        ) {
281
            // every extractor used in this path is a straight-forward arrayaccess.
282
            // Create the compiled code as a plain old variable assignment:
283
            return [
284
                'initialization' => '',
285
                'execution' => sprintf(
286
                    'isset(%s[\'%s\']) ? %s[\'%s\'] : NULL',
287
                    $providerReference,
288
                    str_replace('.', '\'][\'', $path),
289
                    $providerReference,
290
                    str_replace('.', '\'][\'', $path)
291
                )
292
            ];
293
        }
294
        $accessorsVariable = var_export($accessors, true);
295
        $initialization = sprintf('%s = %s;', $arrayVariableName, $accessorsVariable);
296
        return [
297
            'initialization' => $initialization,
298
            'execution' => sprintf(
299
                '%s->getByPath(\'%s\', %s)',
300
                $providerReference,
301
                $path,
302
                $arrayVariableName
303
            )
304
        ];
305
    }
306
307
    /**
308
     * @param ArrayNode $node
309
     * @return array
310
     * @see convert()
311
     */
312
    protected function convertArrayNode(ArrayNode $node)
313
    {
314
        return $this->convertArray($node->getInternalArray());
315
    }
316
317
    /**
318
     * @param ArrayNode $node
0 ignored issues
show
Bug introduced by
There is no parameter named $node. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
319
     * @return array
320
     * @see convert()
321
     */
322
    protected function convertArray(array $array)
323
    {
324
        $initializationPhpCode = '// Rendering Array' . chr(10);
325
        $arrayVariableName = $this->variableName('array');
326
327
        $initializationPhpCode .= sprintf('%s = array();', $arrayVariableName) . chr(10);
328
329
        foreach ($array as $key => $value) {
330
            if ($value instanceof NodeInterface) {
331
                $converted = $this->convert($value);
332
                $initializationPhpCode .= $converted['initialization'];
333
                $initializationPhpCode .= sprintf(
334
                    '%s[\'%s\'] = %s;',
335
                    $arrayVariableName,
336
                    $key,
337
                    $converted['execution']
338
                ) . chr(10);
339
            } elseif (is_numeric($value)) {
340
                // this case might happen for simple values
341
                $initializationPhpCode .= sprintf(
342
                    '%s[\'%s\'] = %s;',
343
                    $arrayVariableName,
344
                    $key,
345
                    $value
346
                ) . chr(10);
347
            } else {
348
                // this case might happen for simple values
349
                $initializationPhpCode .= sprintf(
350
                    '%s[\'%s\'] = \'%s\';',
351
                    $arrayVariableName,
352
                    $key,
353
                    $this->escapeTextForUseInSingleQuotes($value)
354
                ) . chr(10);
355
            }
356
        }
357
        return [
358
            'initialization' => $initializationPhpCode,
359
            'execution' => $arrayVariableName
360
        ];
361
    }
362
363
    /**
364
     * @param NodeInterface $node
365
     * @return array
366
     * @see convert()
367
     */
368
    public function convertListOfSubNodes(NodeInterface $node)
369
    {
370
        switch (count($node->getChildNodes())) {
371
            case 0:
372
                return [
373
                    'initialization' => '',
374
                    'execution' => 'NULL'
375
                ];
376
            case 1:
377
                $childNode = current($node->getChildNodes());
378
                if ($childNode instanceof NodeInterface) {
379
                    return $this->convert($childNode);
380
                }
381
            default:
382
                $outputVariableName = $this->variableName('output');
383
                $initializationPhpCode = sprintf('%s = \'\';', $outputVariableName) . chr(10);
384
385
                foreach ($node->getChildNodes() as $childNode) {
386
                    $converted = $this->convert($childNode);
387
388
                    $initializationPhpCode .= $converted['initialization'] . chr(10);
389
                    $initializationPhpCode .= sprintf('%s .= %s;', $outputVariableName, $converted['execution']) . chr(10);
390
                }
391
392
                return [
393
                    'initialization' => $initializationPhpCode,
394
                    'execution' => $outputVariableName
395
                ];
396
        }
397
    }
398
399
    /**
400
     * @param ExpressionNodeInterface $node
401
     * @return array
402
     * @see convert()
403
     */
404
    protected function convertExpressionNode(ExpressionNodeInterface $node)
405
    {
406
        return $node->compile($this->templateCompiler);
407
    }
408
409
    /**
410
     * @param BooleanNode $node
411
     * @return array
412
     * @see convert()
413
     */
414
    protected function convertBooleanNode(BooleanNode $node)
415
    {
416
        $stack = $this->convertArrayNode(new ArrayNode($node->getStack()));
417
        $initializationPhpCode = '// Rendering Boolean node' . chr(10);
418
        $initializationPhpCode .= $stack['initialization'] . chr(10);
419
420
        $parser = new BooleanParser();
421
        $compiledExpression = $parser->compile(BooleanNode::reconcatenateExpression($node->getStack()));
422
        $functionName = $this->variableName('expression');
423
        $initializationPhpCode .= $functionName . ' = function($context) {return ' . $compiledExpression . ';};' . chr(10);
424
425
        return [
426
            'initialization' => $initializationPhpCode,
427
            'execution' => sprintf(
428
                '%s::convertToBoolean(
429
					%s(
430
						%s::gatherContext($renderingContext, %s)
431
					),
432
					$renderingContext
433
				)',
434
                BooleanNode::class,
435
                $functionName,
436
                BooleanNode::class,
437
                $stack['execution']
438
            )
439
        ];
440
    }
441
442
    /**
443
     * @param string $text
444
     * @return string
445
     */
446
    protected function escapeTextForUseInSingleQuotes($text)
447
    {
448
        return str_replace(['\\', '\''], ['\\\\', '\\\''], $text);
449
    }
450
451
    /**
452
     * Returns a unique variable name by appending a global index to the given prefix
453
     *
454
     * @param string $prefix
455
     * @return string
456
     */
457
    public function variableName($prefix)
458
    {
459
        return '$' . $prefix . $this->variableCounter++;
460
    }
461
}
462