Completed
Pull Request — master (#252)
by Claus
02:29
created

TemplateCompiler::reset()   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 0
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
        if (isset($this->syntaxTreeInstanceCache[$identifier])) {
146
            return true;
147
        }
148
        if (!$this->renderingContext->isCacheEnabled()) {
149
            return false;
150
        }
151
        $identifier = $this->sanitizeIdentifier($identifier);
152
        return !empty($identifier) && $this->renderingContext->getCache()->get($identifier);
153
    }
154
155
    /**
156
     * @param string $identifier
157
     * @return ParsedTemplateInterface
158
     */
159
    public function get($identifier)
160
    {
161
        $identifier = $this->sanitizeIdentifier($identifier);
162
163
        if (!isset($this->syntaxTreeInstanceCache[$identifier])) {
164
            $this->renderingContext->getCache()->get($identifier);
165
            $this->syntaxTreeInstanceCache[$identifier] = new $identifier();
166
        }
167
168
        return $this->syntaxTreeInstanceCache[$identifier];
169
    }
170
171
    /**
172
     * Resets the currently processing state
173
     *
174
     * @return void
175
     */
176
    public function reset()
177
    {
178
        $this->currentlyProcessingState = null;
179
    }
180
181
    /**
182
     * @param string $identifier
183
     * @param ParsingState $parsingState
184
     * @return void
185
     */
186
    public function store($identifier, ParsingState $parsingState)
187
    {
188
        if ($this->isDisabled()) {
189
            if ($this->renderingContext->isCacheEnabled()) {
190
                // Compiler is disabled but cache is enabled. Flush cache to make sure.
191
                $this->renderingContext->getCache()->flush($identifier);
192
            }
193
            $parsingState->setCompilable(false);
194
            return;
195
        }
196
197
        $this->currentlyProcessingState = $parsingState;
198
        $identifier = $this->sanitizeIdentifier($identifier);
199
        $this->nodeConverter->setVariableCounter(0);
200
        $generatedRenderFunctions = $this->generateSectionCodeFromParsingState($parsingState);
201
202
        $generatedRenderFunctions .= $this->generateCodeForSection(
203
            $this->nodeConverter->convertListOfSubNodes($parsingState->getRootNode()),
204
            'render',
205
            'Main Render function'
206
        );
207
208
        $classDefinition = 'class ' . $identifier . ' extends \TYPO3Fluid\Fluid\Core\Compiler\AbstractCompiledTemplate';
209
210
        $templateCode = <<<EOD
211
<?php
212
213
%s {
214
215
public function getLayoutName(\TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
216
\$self = \$this; 
217
%s;
218
}
219
public function hasLayout() {
220
return %s;
221
}
222
public function addCompiledNamespaces(\TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
223
\$renderingContext->getViewHelperResolver()->addNamespaces(%s);
224
}
225
226
%s
227
228
}
229
EOD;
230
        $storedLayoutName = $parsingState->getVariableContainer()->get('layoutName');
231
        $templateCode = sprintf(
232
            $templateCode,
233
            $classDefinition,
234
            $this->generateCodeForLayoutName($storedLayoutName),
235
            ($parsingState->hasLayout() ? 'TRUE' : 'FALSE'),
236
            var_export($this->renderingContext->getViewHelperResolver()->getNamespaces(), true),
237
            $generatedRenderFunctions
238
        );
239
        $this->renderingContext->getCache()->set($identifier, $templateCode);
240
    }
241
242
    /**
243
     * @param RootNode|string $storedLayoutNameArgument
244
     * @return string
245
     */
246
    protected function generateCodeForLayoutName($storedLayoutNameArgument)
247
    {
248
        if ($storedLayoutNameArgument instanceof RootNode) {
249
            list ($initialization, $execution) = array_values($this->nodeConverter->convertListOfSubNodes($storedLayoutNameArgument));
250
            return $initialization . PHP_EOL . 'return ' . $execution;
251
        } else {
252
            return 'return (string) \'' . $storedLayoutNameArgument . '\'';
253
        }
254
    }
255
256
    /**
257
     * @param ParsingState $parsingState
258
     * @return string
259
     */
260
    protected function generateSectionCodeFromParsingState(ParsingState $parsingState)
261
    {
262
        $generatedRenderFunctions = '';
263
        if ($parsingState->getVariableContainer()->exists('1457379500_sections')) {
264
            $sections = $parsingState->getVariableContainer()->get('1457379500_sections'); // TODO: refactor to $parsedTemplate->getSections()
265
            foreach ($sections as $sectionName => $sectionRootNode) {
266
                $generatedRenderFunctions .= $this->generateCodeForSection(
267
                    $this->nodeConverter->convertListOfSubNodes($sectionRootNode),
268
                    'section_' . sha1($sectionName),
269
                    'section ' . $sectionName
270
                );
271
            }
272
        }
273
        return $generatedRenderFunctions;
274
    }
275
276
    /**
277
     * Replaces special characters by underscores
278
     * @see http://www.php.net/manual/en/language.variables.basics.php
279
     *
280
     * @param string $identifier
281
     * @return string the sanitized identifier
282
     */
283
    protected function sanitizeIdentifier($identifier)
284
    {
285
        return preg_replace('([^a-zA-Z0-9_\x7f-\xff])', '_', $identifier);
286
    }
287
288
    /**
289
     * @param array $converted
290
     * @param string $expectedFunctionName
291
     * @param string $comment
292
     * @return string
293
     */
294
    protected function generateCodeForSection(array $converted, $expectedFunctionName, $comment)
295
    {
296
        $templateCode = <<<EOD
297
/**
298
 * %s
299
 */
300
public function %s(\TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
301
\$self = \$this;
302
%s
303
return %s;
304
}
305
306
EOD;
307
        return sprintf($templateCode, $comment, $expectedFunctionName, $converted['initialization'], $converted['execution']);
308
    }
309
310
    /**
311
     * Returns a unique variable name by appending a global index to the given prefix
312
     *
313
     * @param string $prefix
314
     * @return string
315
     */
316
    public function variableName($prefix)
317
    {
318
        return $this->nodeConverter->variableName($prefix);
319
    }
320
321
    /**
322
     * @param NodeInterface $node
323
     * @return string
324
     */
325
    public function wrapChildNodesInClosure(NodeInterface $node)
326
    {
327
        $closure = '';
328
        $closure .= 'function() use ($renderingContext, $self) {' . chr(10);
329
        $convertedSubNodes = $this->nodeConverter->convertListOfSubNodes($node);
330
        $closure .= $convertedSubNodes['initialization'];
331
        $closure .= sprintf('return %s;', $convertedSubNodes['execution']) . chr(10);
332
        $closure .= '}';
333
        return $closure;
334
    }
335
336
    /**
337
     * Wraps one ViewHelper argument evaluation in a closure that can be
338
     * rendered by passing a rendering context.
339
     *
340
     * @param ViewHelperNode $node
341
     * @param string $argumentName
342
     * @return string
343
     */
344
    public function wrapViewHelperNodeArgumentEvaluationInClosure(ViewHelperNode $node, $argumentName)
345
    {
346
        $arguments = $node->getArguments();
347
        $argument = $arguments[$argumentName];
348
        $closure = 'function() use ($renderingContext, $self) {' . chr(10);
349
        if ($node->getArgumentDefinition($argumentName)->getType() === 'boolean') {
350
            // We treat boolean nodes by compiling a closure to evaluate the stack of the boolean argument
351
            $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...
352
            $closure .= $compiledIfArgumentStack['initialization'] . chr(10);
353
            $closure .= sprintf(
354
                'return \TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode::evaluateStack($renderingContext, %s);',
355
                $compiledIfArgumentStack['execution']
356
            ) . chr(10);
357
        } else {
358
            $closure .= sprintf('$argument = unserialize(\'%s\'); return $argument->evaluate($renderingContext);', serialize($argument)) . chr(10);
359
        }
360
        $closure .= '}';
361
        return $closure;
362
    }
363
}
364