Completed
Pull Request — master (#315)
by Oliver
02:06
created

TemplateCompiler::has()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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