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