Passed
Pull Request — master (#1005)
by Maxim
10:03
created

StemplerEngine::getVisitors()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 13
ccs 7
cts 8
cp 0.875
crap 3.0175
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Stempler;
6
7
use Psr\Container\ContainerInterface;
8
use Spiral\Core\Container\Autowire;
9
use Spiral\Core\FactoryInterface;
10
use Spiral\Stempler\Compiler\Renderer\CoreRenderer;
11
use Spiral\Stempler\Compiler\Renderer\DynamicRenderer;
12
use Spiral\Stempler\Compiler\Renderer\HTMLRenderer;
13
use Spiral\Stempler\Compiler\Renderer\PHPRenderer;
14
use Spiral\Stempler\Compiler\Result;
15
use Spiral\Stempler\Compiler\SourceMap;
16
use Spiral\Stempler\Config\StemplerConfig;
17
use Spiral\Stempler\Directive\DirectiveGroup;
18
use Spiral\Stempler\Directive\DirectiveRendererInterface;
19
use Spiral\Stempler\Lexer\Grammar;
20
use Spiral\Stempler\Parser\Syntax;
21
use Spiral\Stempler\Transform\Finalizer\DynamicToPHP;
22
use Spiral\Stempler\Transform\Merge\ExtendsParent;
23
use Spiral\Stempler\Transform\Merge\ResolveImports;
24
use Spiral\Views\ContextInterface;
25
use Spiral\Views\EngineInterface;
26
use Spiral\Views\Exception\CompileException;
27
use Spiral\Views\Exception\EngineException;
28
use Spiral\Views\LoaderInterface;
29
use Spiral\Views\ProcessorInterface;
30
use Spiral\Views\ViewInterface;
31
use Spiral\Views\ViewSource;
32
use Throwable;
33
34
final class StemplerEngine implements EngineInterface
35
{
36
    // default file extension
37
    public const EXTENSION = 'dark.php';
38
39
    private string $classPrefix = '__StemplerView__';
40
41
    private ?Builder $builder = null;
42
    private ?LoaderInterface $loader = null;
43
44 28
    public function __construct(
45
        private readonly ContainerInterface $container,
46
        private readonly StemplerConfig $config,
47
        private readonly ?StemplerCache $cache = null
48
    ) {
49 28
    }
50
51 18
    public function getContainer(): ContainerInterface
52
    {
53 18
        return $this->container;
54
    }
55
56 28
    public function withLoader(LoaderInterface $loader): EngineInterface
57
    {
58 28
        $engine = clone $this;
59 28
        $engine->loader = $loader->withExtension(static::EXTENSION);
60 28
        $engine->builder = $engine->makeBuilder(new StemplerLoader($engine->loader, $this->getProcessors()));
61
62 28
        return $engine;
63
    }
64
65 27
    public function getLoader(): LoaderInterface
66
    {
67 27
        if ($this->loader === null) {
68
            throw new EngineException('No associated loader found');
69
        }
70
71 27
        return $this->loader;
72
    }
73
74
    /**
75
     * Return builder locked to specific context.
76
     */
77 17
    public function getBuilder(ContextInterface $context): Builder
78
    {
79 17
        if ($this->builder === null) {
80
            throw new EngineException('No associated builder found');
81
        }
82
83
        // since view source support pre-processing we must ensure that context is always set
84 17
        $loader = $this->builder->getLoader();
85 17
        if ($loader instanceof StemplerLoader) {
86 17
            $loader->setContext($context);
87
        }
88
89 17
        return $this->builder;
90
    }
91
92 20
    public function compile(string $path, ContextInterface $context): ViewInterface
93
    {
94
        // for name generation only
95 20
        $view = $this->getLoader()->load($path);
96
97
        // expected template class name
98 20
        $class = $this->className($view, $context);
99
100
        // cache key
101 20
        $key = $this->cacheKey($view, $context);
102
103 20
        if ($this->cache !== null && $this->cache->isFresh($key)) {
104
            $this->cache->load($key);
105 20
        } elseif (!\class_exists($class)) {
106
            try {
107 16
                $builder = $this->getBuilder($context);
108
109 16
                $result = $builder->compile($path);
110 2
            } catch (Throwable $e) {
111 2
                throw new CompileException($e);
112
            }
113
114 14
            $compiled = $this->compileClass($class, $result);
115
116 14
            if ($this->cache !== null) {
117 2
                $this->cache->write(
118 2
                    $key,
119 2
                    $compiled,
120 2
                    \array_map(
121 2
                        fn ($path) => $this->getLoader()->load($path)->getFilename(),
122 2
                        $result->getPaths()
123 2
                    )
124 2
                );
125
126 2
                $this->cache->load($key);
127
            }
128
129 14
            if (!\class_exists($class)) {
130
                // runtime initialization
131 12
                eval('?>' . $compiled);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
132
            }
133
        }
134
135 18
        if (!\class_exists($class) || !\is_subclass_of($class, ViewInterface::class)) {
136
            throw new EngineException(\sprintf('Unable to load `%s`, cache might be corrupted.', $path));
137
        }
138
139 18
        return new $class($this, $view, $context);
140
    }
141
142 1
    public function reset(string $path, ContextInterface $context): void
143
    {
144 1
        if ($this->cache === null) {
145
            return;
146
        }
147
148 1
        $source = $this->getLoader()->load($path);
149
150 1
        $this->cache->delete($this->cacheKey($source, $context));
151
    }
152
153 20
    public function get(string $path, ContextInterface $context): ViewInterface
154
    {
155 20
        return $this->compile($path, $context);
156
    }
157
158
    /**
159
     * Calculate sourcemap for exception highlighting.
160
     */
161 3
    public function makeSourceMap(string $path, ContextInterface $context): ?SourceMap
162
    {
163
        try {
164 3
            $builder = $this->getBuilder($context);
165
166
            // there is no need to cache sourcemaps since they are used during the exception only
167 3
            return $builder->compile($path)->getSourceMap($builder->getLoader());
168
        } catch (Throwable) {
169
            return null;
170
        }
171
    }
172
173 14
    private function compileClass(string $class, Result $result): string
174
    {
175 14
        $template = '<?php class %s extends \Spiral\Stempler\StemplerView {
176
            public function render(array $data=[]): string {
177
                \ob_start();
178
                $__outputLevel__ = \ob_get_level();
179
180
                try {
181
                    Spiral\Core\ContainerScope::runScope($this->container, function () use ($data) {
182
                        \extract($data, EXTR_OVERWRITE);
183
                        ?>%s<?php
184
                    });
185
                } catch (\Throwable $e) {
186
                    while (\ob_get_level() >= $__outputLevel__) { \ob_end_clean(); }
187
                    throw $this->mapException(8, $e, $data);
188
                } finally {
189
                    while (\ob_get_level() > $__outputLevel__) { \ob_end_clean(); }
190
                }
191
192
                return \ob_get_clean();
193
            }
194 14
        }';
195
196 14
        return \sprintf($template, $class, $result->getContent());
197
    }
198
199
    /**
200
     * @return class-string<ViewInterface>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<ViewInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<ViewInterface>.
Loading history...
201
     */
202 20
    private function className(ViewSource $source, ContextInterface $context): string
203
    {
204 20
        return $this->classPrefix . $this->cacheKey($source, $context);
205
    }
206
207 20
    private function cacheKey(ViewSource $source, ContextInterface $context): string
208
    {
209 20
        $key = \sprintf(
210 20
            '%s.%s.%s',
211 20
            $source->getNamespace(),
212 20
            $source->getName(),
213 20
            $context->getID()
214 20
        );
215
216 20
        return \hash('sha256', $key);
217
    }
218
219 28
    private function makeBuilder(StemplerLoader $loader): Builder
220
    {
221 28
        $builder = new Builder($loader);
222
223 28
        $directivesGroup = new DirectiveGroup();
224 28
        foreach ($this->getDirectives() as $directive) {
225 28
            $directivesGroup->addDirective($directive);
226
        }
227
228
        // we are using fixed set of grammars and renderers for now
229 28
        $builder->getParser()->addSyntax(
230 28
            new Grammar\PHPGrammar(),
231 28
            new Syntax\PHPSyntax()
232 28
        );
233
234 28
        $builder->getParser()->addSyntax(
235 28
            new Grammar\InlineGrammar(),
236 28
            new Syntax\InlineSyntax()
237 28
        );
238
239 28
        $builder->getParser()->addSyntax(
240 28
            new Grammar\DynamicGrammar($directivesGroup),
241 28
            new Syntax\DynamicSyntax()
242 28
        );
243
244 28
        $builder->getParser()->addSyntax(
245 28
            new Grammar\HTMLGrammar(),
246 28
            new Syntax\HTMLSyntax()
247 28
        );
248
249 28
        $builder->getCompiler()->addRenderer(new CoreRenderer());
250 28
        $builder->getCompiler()->addRenderer(new PHPRenderer());
251 28
        $builder->getCompiler()->addRenderer(new HTMLRenderer());
252 28
        $builder->getCompiler()->addRenderer(new DynamicRenderer(new DirectiveGroup($this->getDirectives())));
253
254
        // ATS modifications
255 28
        foreach ($this->getVisitors(Builder::STAGE_PREPARE) as $visitor) {
256 28
            $builder->addVisitor($visitor, Builder::STAGE_PREPARE);
257
        }
258
259
        // php conversion
260 28
        $builder->addVisitor(
261 28
            new DynamicToPHP(DynamicToPHP::DEFAULT_FILTER, $this->getDirectives()),
262 28
            Builder::STAGE_TRANSFORM
263 28
        );
264
265 28
        $builder->addVisitor(new ResolveImports($builder), Builder::STAGE_TRANSFORM);
266 28
        $builder->addVisitor(new ExtendsParent($builder), Builder::STAGE_TRANSFORM);
267
268 28
        foreach ($this->getVisitors(Builder::STAGE_TRANSFORM) as $visitor) {
269
            $builder->addVisitor($visitor, Builder::STAGE_TRANSFORM);
270
        }
271
272 28
        foreach ($this->getVisitors(Builder::STAGE_FINALIZE) as $visitor) {
273 28
            $builder->addVisitor($visitor, Builder::STAGE_FINALIZE);
274
        }
275
276 28
        foreach ($this->getVisitors(Builder::STAGE_COMPILE) as $visitor) {
277 18
            $builder->addVisitor($visitor, Builder::STAGE_COMPILE);
278
        }
279
280 28
        return $builder;
281
    }
282
283
    /**
284
     * @return VisitorInterface[]
285
     */
286 28
    private function getVisitors(int $stage): iterable
287
    {
288 28
        $result = [];
289 28
        foreach ($this->config->getVisitors($stage) as $visitor) {
290 28
            if ($visitor instanceof Autowire) {
291 28
                $result[] = $visitor->resolve($this->container->get(FactoryInterface::class));
292 28
                continue;
293
            }
294
295
            $result[] = $visitor;
296
        }
297
298 28
        return $result;
299
    }
300
301
    /**
302
     * @return ProcessorInterface[]
303
     */
304 28
    private function getProcessors(): iterable
305
    {
306 28
        $result = [];
307 28
        foreach ($this->config->getProcessors() as $processor) {
308 28
            if ($processor instanceof Autowire) {
309 28
                $result[] = $processor->resolve($this->container->get(FactoryInterface::class));
310 28
                continue;
311
            }
312
313
            $result[] = $processor;
314
        }
315
316 28
        return $result;
317
    }
318
319
    /**
320
     * @return DirectiveRendererInterface[]
321
     */
322 28
    private function getDirectives(): iterable
323
    {
324 28
        $result = [];
325 28
        foreach ($this->config->getDirectives() as $directive) {
326 28
            if ($directive instanceof Autowire) {
327 28
                $result[] = $directive->resolve($this->container->get(FactoryInterface::class));
328 28
                continue;
329
            }
330
331
            $result[] = $directive;
332
        }
333
334 28
        return $result;
335
    }
336
}
337