Completed
Pull Request — master (#315)
by Claus
01:56
created

TemplateCompiler::has()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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