Passed
Push — master ( 865ea9...f5a1ef )
by butschster
29:08 queued 21:20
created

StemplerEngine::getDirectives()   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 0
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 21
    public function __construct(
45
        private readonly ContainerInterface $container,
46
        private readonly StemplerConfig $config,
47
        private readonly ?StemplerCache $cache = null
48
    ) {
49 21
    }
50
51 15
    public function getContainer(): ContainerInterface
52
    {
53 15
        return $this->container;
54
    }
55
56 21
    public function withLoader(LoaderInterface $loader): EngineInterface
57
    {
58 21
        $engine = clone $this;
59 21
        $engine->loader = $loader->withExtension(static::EXTENSION);
60 21
        $engine->builder = $engine->makeBuilder(new StemplerLoader($engine->loader, $this->getProcessors()));
61
62 21
        return $engine;
63
    }
64
65 21
    public function getLoader(): LoaderInterface
66
    {
67 21
        if ($this->loader === null) {
68
            throw new EngineException('No associated loader found');
69
        }
70
71 21
        return $this->loader;
72
    }
73
74
    /**
75
     * Return builder locked to specific context.
76
     */
77 1
    public function getBuilder(ContextInterface $context): Builder
78
    {
79 1
        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 1
        $loader = $this->builder->getLoader();
85 1
        if ($loader instanceof StemplerLoader) {
86 1
            $loader->setContext($context);
87
        }
88
89 1
        return $this->builder;
90
    }
91
92 15
    public function compile(string $path, ContextInterface $context): ViewInterface
93
    {
94
        // for name generation only
95 15
        $view = $this->getLoader()->load($path);
96
97
        // expected template class name
98 15
        $class = $this->className($view, $context);
99
100
        // cache key
101 15
        $key = $this->cacheKey($view, $context);
102
103 15
        if ($this->cache !== null && $this->cache->isFresh($key)) {
104
            $this->cache->load($key);
105 15
        } elseif (!\class_exists($class)) {
106
            try {
107 1
                $builder = $this->getBuilder($context);
108
109 1
                $result = $builder->compile($path);
110
            } catch (Throwable $e) {
111
                throw new CompileException($e);
112
            }
113
114 1
            $compiled = $this->compileClass($class, $result);
115
116 1
            if ($this->cache !== null) {
117 1
                $this->cache->write(
118 1
                    $key,
119 1
                    $compiled,
120 1
                    \array_map(
121 1
                        fn ($path) => $this->getLoader()->load($path)->getFilename(),
122 1
                        $result->getPaths()
123 1
                    )
124 1
                );
125
126 1
                $this->cache->load($key);
127
            }
128
129 1
            if (!\class_exists($class)) {
130
                // runtime initialization
131
                eval('?>' . $compiled);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
132
            }
133
        }
134
135 15
        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 15
        return new $class($this, $view, $context);
140
    }
141
142 11
    public function reset(string $path, ContextInterface $context): void
143
    {
144 11
        if ($this->cache === null) {
145 11
            return;
146
        }
147
148
        $source = $this->getLoader()->load($path);
149
150
        $this->cache->delete($this->cacheKey($source, $context));
151
    }
152
153 4
    public function get(string $path, ContextInterface $context): ViewInterface
154
    {
155 4
        return $this->compile($path, $context);
156
    }
157
158
    /**
159
     * Calculate sourcemap for exception highlighting.
160
     */
161
    public function makeSourceMap(string $path, ContextInterface $context): ?SourceMap
162
    {
163
        try {
164
            $builder = $this->getBuilder($context);
165
166
            // there is no need to cache sourcemaps since they are used during the exception only
167
            return $builder->compile($path)->getSourceMap($builder->getLoader());
168
        } catch (Throwable) {
169
            return null;
170
        }
171
    }
172
173 1
    private function compileClass(string $class, Result $result): string
174
    {
175 1
        $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 1
        }';
195
196 1
        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 15
    private function className(ViewSource $source, ContextInterface $context): string
203
    {
204 15
        return $this->classPrefix . $this->cacheKey($source, $context);
205
    }
206
207 15
    private function cacheKey(ViewSource $source, ContextInterface $context): string
208
    {
209 15
        $key = \sprintf(
210 15
            '%s.%s.%s',
211 15
            $source->getNamespace(),
212 15
            $source->getName(),
213 15
            $context->getID()
214 15
        );
215
216 15
        return \hash('sha256', $key);
217
    }
218
219 21
    private function makeBuilder(StemplerLoader $loader): Builder
220
    {
221 21
        $builder = new Builder($loader);
222
223 21
        $directivesGroup = new DirectiveGroup();
224 21
        foreach ($this->getDirectives() as $directive) {
225 21
            $directivesGroup->addDirective($directive);
226
        }
227
228
        // we are using fixed set of grammars and renderers for now
229 21
        $builder->getParser()->addSyntax(
230 21
            new Grammar\PHPGrammar(),
231 21
            new Syntax\PHPSyntax()
232 21
        );
233
234 21
        $builder->getParser()->addSyntax(
235 21
            new Grammar\InlineGrammar(),
236 21
            new Syntax\InlineSyntax()
237 21
        );
238
239 21
        $builder->getParser()->addSyntax(
240 21
            new Grammar\DynamicGrammar($directivesGroup),
241 21
            new Syntax\DynamicSyntax()
242 21
        );
243
244 21
        $builder->getParser()->addSyntax(
245 21
            new Grammar\HTMLGrammar(),
246 21
            new Syntax\HTMLSyntax()
247 21
        );
248
249 21
        $builder->getCompiler()->addRenderer(new CoreRenderer());
250 21
        $builder->getCompiler()->addRenderer(new PHPRenderer());
251 21
        $builder->getCompiler()->addRenderer(new HTMLRenderer());
252 21
        $builder->getCompiler()->addRenderer(new DynamicRenderer(new DirectiveGroup($this->getDirectives())));
253
254
        // ATS modifications
255 21
        foreach ($this->getVisitors(Builder::STAGE_PREPARE) as $visitor) {
256 21
            $builder->addVisitor($visitor, Builder::STAGE_PREPARE);
257
        }
258
259
        // php conversion
260 21
        $builder->addVisitor(
261 21
            new DynamicToPHP(DynamicToPHP::DEFAULT_FILTER, $this->getDirectives()),
262 21
            Builder::STAGE_TRANSFORM
263 21
        );
264
265 21
        $builder->addVisitor(new ResolveImports($builder), Builder::STAGE_TRANSFORM);
266 21
        $builder->addVisitor(new ExtendsParent($builder), Builder::STAGE_TRANSFORM);
267
268 21
        foreach ($this->getVisitors(Builder::STAGE_TRANSFORM) as $visitor) {
269
            $builder->addVisitor($visitor, Builder::STAGE_TRANSFORM);
270
        }
271
272 21
        foreach ($this->getVisitors(Builder::STAGE_FINALIZE) as $visitor) {
273 21
            $builder->addVisitor($visitor, Builder::STAGE_FINALIZE);
274
        }
275
276 21
        foreach ($this->getVisitors(Builder::STAGE_COMPILE) as $visitor) {
277
            $builder->addVisitor($visitor, Builder::STAGE_COMPILE);
278
        }
279
280 21
        return $builder;
281
    }
282
283
    /**
284
     * @return VisitorInterface[]
285
     */
286 21
    private function getVisitors(int $stage): iterable
287
    {
288 21
        $result = [];
289 21
        foreach ($this->config->getVisitors($stage) as $visitor) {
290 21
            if ($visitor instanceof Autowire) {
291 21
                $result[] = $visitor->resolve($this->container->get(FactoryInterface::class));
292 21
                continue;
293
            }
294
295
            $result[] = $visitor;
296
        }
297
298 21
        return $result;
299
    }
300
301
    /**
302
     * @return ProcessorInterface[]
303
     */
304 21
    private function getProcessors(): iterable
305
    {
306 21
        $result = [];
307 21
        foreach ($this->config->getProcessors() as $processor) {
308 21
            if ($processor instanceof Autowire) {
309 21
                $result[] = $processor->resolve($this->container->get(FactoryInterface::class));
310 21
                continue;
311
            }
312
313
            $result[] = $processor;
314
        }
315
316 21
        return $result;
317
    }
318
319
    /**
320
     * @return DirectiveRendererInterface[]
321
     */
322 21
    private function getDirectives(): iterable
323
    {
324 21
        $result = [];
325 21
        foreach ($this->config->getDirectives() as $directive) {
326 21
            if ($directive instanceof Autowire) {
327 21
                $result[] = $directive->resolve($this->container->get(FactoryInterface::class));
328 21
                continue;
329
            }
330
331
            $result[] = $directive;
332
        }
333
334 21
        return $result;
335
    }
336
}
337