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

NodeConverter::convertViewHelperNode()   B

Complexity

Conditions 8
Paths 126

Size

Total Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 126
nop 1
dl 0
loc 73
rs 7.1713
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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