Completed
Pull Request — master (#335)
by Claus
04:28 queued 02:03
created

TemplateCompiler::variableName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
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\ParsedTemplateInterface;
10
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
15
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
16
17
/**
18
 * Class TemplateCompiler
19
 */
20
class TemplateCompiler
21
{
22
23
    const SHOULD_GENERATE_VIEWHELPER_INVOCATION = '##should_gen_viewhelper##';
24
    const MODE_NORMAL = 'normal';
25
    const MODE_WARMUP = 'warmup';
26
27
    /**
28
     * @var array
29
     */
30
    protected $syntaxTreeInstanceCache = [];
31
32
    /**
33
     * @var NodeConverter
34
     */
35
    protected $nodeConverter;
36
37
    /**
38
     * @var RenderingContextInterface
39
     */
40
    protected $renderingContext;
41
42
    /**
43
     * @var string
44
     */
45
    protected $mode = self::MODE_NORMAL;
46
47
    /**
48
     * @var ParsedTemplateInterface
49
     */
50
    protected $currentlyProcessingState;
51
52
    /**
53
     * Constructor
54
     */
55
    public function __construct()
56
    {
57
        $this->nodeConverter = new NodeConverter($this);
58
    }
59
60
    /**
61
     * Instruct the TemplateCompiler to enter warmup mode, assigning
62
     * additional context allowing cache-related implementations to
63
     * subsequently check the mode.
64
     *
65
     * Cannot be reversed once done - should only be used from within
66
     * FluidCacheWarmerInterface implementations!
67
     */
68
    public function enterWarmupMode()
69
    {
70
        $this->mode = static::MODE_WARMUP;
71
    }
72
73
    /**
74
     * Returns TRUE only if the TemplateCompiler is in warmup mode.
75
     */
76
    public function isWarmupMode()
77
    {
78
        return $this->mode === static::MODE_WARMUP;
79
    }
80
81
    /**
82
     * @return ParsedTemplateInterface|NULL
83
     */
84
    public function getCurrentlyProcessingState()
85
    {
86
        return $this->currentlyProcessingState;
87
    }
88
89
    /**
90
     * @param RenderingContextInterface $renderingContext
91
     * @return void
92
     */
93
    public function setRenderingContext(RenderingContextInterface $renderingContext)
94
    {
95
        $this->renderingContext = $renderingContext;
96
    }
97
98
    /**
99
     * @return RenderingContextInterface
100
     */
101
    public function getRenderingContext()
102
    {
103
        return $this->renderingContext;
104
    }
105
106
    /**
107
     * @param NodeConverter $nodeConverter
108
     * @return void
109
     */
110
    public function setNodeConverter(NodeConverter $nodeConverter)
111
    {
112
        $this->nodeConverter = $nodeConverter;
113
    }
114
115
    /**
116
     * @return NodeConverter
117
     */
118
    public function getNodeConverter()
119
    {
120
        return $this->nodeConverter;
121
    }
122
123
    /**
124
     * @return void
125
     */
126
    public function disable()
127
    {
128
        throw new StopCompilingException('Compiling stopped');
129
    }
130
131
    /**
132
     * @return boolean
133
     */
134
    public function isDisabled()
135
    {
136
        return !$this->renderingContext->isCacheEnabled();
137
    }
138
139
    /**
140
     * @param string $identifier
141
     * @return boolean
142
     */
143
    public function has($identifier)
144
    {
145
        $identifier = $this->sanitizeIdentifier($identifier);
146
147
        if (isset($this->syntaxTreeInstanceCache[$identifier]) || class_exists($identifier, false)) {
148
            return true;
149
        }
150
        if (!$this->renderingContext->isCacheEnabled()) {
151
            return false;
152
        }
153
        if (!empty($identifier)) {
154
            return (boolean) $this->renderingContext->getCache()->get($identifier);
155
        }
156
        return false;
157
    }
158
159
    /**
160
     * @param string $identifier
161
     * @return ParsedTemplateInterface
162
     */
163
    public function get($identifier)
164
    {
165
        $identifier = $this->sanitizeIdentifier($identifier);
166
167
        if (!isset($this->syntaxTreeInstanceCache[$identifier])) {
168
            if (!class_exists($identifier, false)) {
169
                $this->renderingContext->getCache()->get($identifier);
170
            }
171
            if (!is_a($identifier, UncompilableTemplateInterface::class, true)) {
172
                $this->syntaxTreeInstanceCache[$identifier] = new $identifier();
173
            } else {
174
                return new $identifier();
175
            }
176
        }
177
178
179
        return $this->syntaxTreeInstanceCache[$identifier];
180
    }
181
182
    /**
183
     * Resets the currently processing state
184
     *
185
     * @return void
186
     */
187
    public function reset()
188
    {
189
        $this->currentlyProcessingState = null;
190
    }
191
192
    /**
193
     * @param string $identifier
194
     * @param ParsingState $parsingState
195
     * @return string|null
196
     */
197
    public function store($identifier, ParsingState $parsingState)
198
    {
199
        if ($this->isDisabled()) {
200
            $parsingState->setCompilable(false);
201
            return null;
202
        }
203
204
        $identifier = $this->sanitizeIdentifier($identifier);
205
        $cache = $this->renderingContext->getCache();
206
        if (!$parsingState->isCompilable()) {
207
            $templateCode = '<?php' . PHP_EOL . 'class ' . $identifier .
208
                ' extends \TYPO3Fluid\Fluid\Core\Compiler\AbstractCompiledTemplate' . PHP_EOL .
209
                ' implements \TYPO3Fluid\Fluid\Core\Compiler\UncompilableTemplateInterface' . PHP_EOL .
210
                '{' . PHP_EOL . '}';
211
            $cache->set($identifier, $templateCode);
212
            return $templateCode;
213
        }
214
215
        $this->currentlyProcessingState = $parsingState;
216
        $this->nodeConverter->setVariableCounter(0);
217
        $generatedRenderFunctions = $this->generateSectionCodeFromParsingState($parsingState);
218
219
        $generatedRenderFunctions .= $this->generateCodeForSection(
220
            $this->nodeConverter->convertListOfSubNodes($parsingState->getRootNode()),
221
            'render',
222
            'Main Render function'
223
        );
224
225
        $classDefinition = 'class ' . $identifier . ' extends \TYPO3Fluid\Fluid\Core\Compiler\AbstractCompiledTemplate';
226
227
        $templateCode = <<<EOD
228
<?php
229
230
%s {
231
232
public function getLayoutName(\TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
233
\$self = \$this; 
234
%s;
235
}
236
public function hasLayout() {
237
return %s;
238
}
239
public function addCompiledNamespaces(\TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
240
\$renderingContext->getViewHelperResolver()->addNamespaces(%s);
241
}
242
243
%s
244
245
}
246
EOD;
247
        $storedLayoutName = $parsingState->getVariableContainer()->get('layoutName');
248
        $templateCode = sprintf(
249
            $templateCode,
250
            $classDefinition,
251
            $this->generateCodeForLayoutName($storedLayoutName),
252
            ($parsingState->hasLayout() ? 'TRUE' : 'FALSE'),
253
            var_export($this->renderingContext->getViewHelperResolver()->getNamespaces(), true),
254
            $generatedRenderFunctions
255
        );
256
        $this->renderingContext->getCache()->set($identifier, $templateCode);
257
        return $templateCode;
258
    }
259
260
    /**
261
     * @param RootNode|string $storedLayoutNameArgument
262
     * @return string
263
     */
264
    protected function generateCodeForLayoutName($storedLayoutNameArgument)
265
    {
266
        if ($storedLayoutNameArgument instanceof RootNode) {
267
            list ($initialization, $execution) = array_values($this->nodeConverter->convertListOfSubNodes($storedLayoutNameArgument));
268
            return $initialization . PHP_EOL . 'return ' . $execution;
269
        } else {
270
            return 'return (string) \'' . $storedLayoutNameArgument . '\'';
271
        }
272
    }
273
274
    /**
275
     * @param ParsingState $parsingState
276
     * @return string
277
     */
278
    protected function generateSectionCodeFromParsingState(ParsingState $parsingState)
279
    {
280
        $generatedRenderFunctions = '';
281
        if ($parsingState->getVariableContainer()->exists('1457379500_sections')) {
282
            $sections = $parsingState->getVariableContainer()->get('1457379500_sections'); // TODO: refactor to $parsedTemplate->getSections()
283
            foreach ($sections as $sectionName => $sectionRootNode) {
284
                $generatedRenderFunctions .= $this->generateCodeForSection(
285
                    $this->nodeConverter->convertListOfSubNodes($sectionRootNode),
286
                    'section_' . sha1($sectionName),
287
                    'section ' . $sectionName
288
                );
289
            }
290
        }
291
        return $generatedRenderFunctions;
292
    }
293
294
    /**
295
     * Replaces special characters by underscores
296
     * @see http://www.php.net/manual/en/language.variables.basics.php
297
     *
298
     * @param string $identifier
299
     * @return string the sanitized identifier
300
     */
301
    protected function sanitizeIdentifier($identifier)
302
    {
303
        return preg_replace('([^a-zA-Z0-9_\x7f-\xff])', '_', $identifier);
304
    }
305
306
    /**
307
     * @param array $converted
308
     * @param string $expectedFunctionName
309
     * @param string $comment
310
     * @return string
311
     */
312
    protected function generateCodeForSection(array $converted, $expectedFunctionName, $comment)
313
    {
314
        $templateCode = <<<EOD
315
/**
316
 * %s
317
 */
318
public function %s(\TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
319
\$self = \$this;
320
%s
321
return %s;
322
}
323
324
EOD;
325
        return sprintf($templateCode, $comment, $expectedFunctionName, $converted['initialization'], $converted['execution']);
326
    }
327
328
    /**
329
     * Returns a unique variable name by appending a global index to the given prefix
330
     *
331
     * @param string $prefix
332
     * @return string
333
     */
334
    public function variableName($prefix)
335
    {
336
        return $this->nodeConverter->variableName($prefix);
337
    }
338
339
    /**
340
     * @param NodeInterface $node
341
     * @return string
342
     */
343
    public function wrapChildNodesInClosure(NodeInterface $node)
344
    {
345
        $closure = '';
346
        $closure .= 'function() use ($renderingContext, $self) {' . chr(10);
347
        $convertedSubNodes = $this->nodeConverter->convertListOfSubNodes($node);
348
        $closure .= $convertedSubNodes['initialization'];
349
        $closure .= sprintf('return %s;', $convertedSubNodes['execution']) . chr(10);
350
        $closure .= '}';
351
        return $closure;
352
    }
353
354
    /**
355
     * Wraps one ViewHelper argument evaluation in a closure that can be
356
     * rendered by passing a rendering context.
357
     *
358
     * @param ViewHelperNode $node
359
     * @param string $argumentName
360
     * @return string
361
     */
362
    public function wrapViewHelperNodeArgumentEvaluationInClosure(ViewHelperNode $node, $argumentName)
363
    {
364
        $arguments = $node->getArguments();
365
        $argument = $arguments[$argumentName];
366
        $closure = 'function() use ($renderingContext, $self) {' . chr(10);
367
        if ($node->getArgumentDefinition($argumentName)->getType() === 'boolean') {
368
            // We treat boolean nodes by compiling a closure to evaluate the stack of the boolean argument
369
            $compiledIfArgumentStack = $this->nodeConverter->convert(new ArrayNode($argument->getStack()));
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TYPO3Fluid\Fluid\Core\Pa...yntaxTree\NodeInterface as the method getStack() does only exist in the following implementations of said interface: TYPO3Fluid\Fluid\Core\Pa...\SyntaxTree\BooleanNode.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
370
            $closure .= $compiledIfArgumentStack['initialization'] . chr(10);
371
            $closure .= sprintf(
372
                'return \TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode::evaluateStack($renderingContext, %s);',
373
                $compiledIfArgumentStack['execution']
374
            ) . chr(10);
375
        } else {
376
            $closure .= sprintf('$argument = unserialize(\'%s\'); return $argument->evaluate($renderingContext);', serialize($argument)) . chr(10);
377
        }
378
        $closure .= '}';
379
        return $closure;
380
    }
381
}
382