StemplerEngine::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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