• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / latte / 18955994117

30 Oct 2025 09:46PM UTC coverage: 94.624% (+0.3%) from 94.374%
18955994117

push

github

dg
ExpressionAttributeNode: optionally rendered depending on the content

18 of 19 new or added lines in 4 files covered. (94.74%)

79 existing lines in 13 files now uncovered.

5333 of 5636 relevant lines covered (94.62%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.23
/src/Latte/Engine.php
1
<?php
2

3
/**
4
 * This file is part of the Latte (https://latte.nette.org)
5
 * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Latte;
11

12
use Latte\Compiler\Nodes\TemplateNode;
13
use function array_map, array_merge, class_exists, extension_loaded, filemtime, get_debug_type, get_object_vars, is_array, preg_match, serialize, substr;
14
use const PHP_VERSION_ID;
15

16

17
/**
18
 * Templating engine Latte.
19
 */
20
class Engine
21
{
22
        public const Version = '3.1.0';
23
        public const VersionId = 30100;
24

25
        /** @deprecated use Engine::Version */
26
        public const
27
                VERSION = self::Version,
28
                VERSION_ID = self::VersionId;
29

30
        #[\Deprecated('use Latte\ContentType::Html')]
31
        public const CONTENT_HTML = ContentType::Html;
32

33
        #[\Deprecated('use Latte\ContentType::Xml')]
34
        public const CONTENT_XML = ContentType::Xml;
35

36
        #[\Deprecated('use Latte\ContentType::JavaScript')]
37
        public const CONTENT_JS = ContentType::JavaScript;
38

39
        #[\Deprecated('use Latte\ContentType::Css')]
40
        public const CONTENT_CSS = ContentType::Css;
41

42
        #[\Deprecated('use Latte\ContentType::ICal')]
43
        public const CONTENT_ICAL = ContentType::ICal;
44

45
        #[\Deprecated('use Latte\ContentType::Text')]
46
        public const CONTENT_TEXT = ContentType::Text;
47

48
        private ?Loader $loader = null;
49
        private Runtime\FilterExecutor $filters;
50
        private Runtime\FunctionExecutor $functions;
51
        private \stdClass $providers;
52

53
        /** @var Extension[] */
54
        private array $extensions = [];
55
        private string $contentType = ContentType::Html;
56
        private Cache $cache;
57
        private bool $strictTypes = true;
58
        private bool $strictParsing = false;
59
        private ?Policy $policy = null;
60
        private bool $sandboxed = false;
61
        private ?string $phpBinary = null;
62
        private ?string $configurationHash;
63
        private ?string $locale = null;
64
        private ?string $syntax = null;
65

66

67
        public function __construct()
68
        {
69
                $this->cache = new Cache;
1✔
70
                $this->filters = new Runtime\FilterExecutor;
1✔
71
                $this->functions = new Runtime\FunctionExecutor;
1✔
72
                $this->providers = new \stdClass;
1✔
73
                $this->addExtension(new Essential\CoreExtension);
1✔
74
                $this->addExtension(new Sandbox\SandboxExtension);
1✔
75
        }
1✔
76

77

78
        /**
79
         * Renders template to output.
80
         * @param  object|mixed[]  $params
81
         */
82
        public function render(string $name, object|array $params = [], ?string $block = null): void
1✔
83
        {
84
                $template = $this->createTemplate($name, $this->processParams($params));
1✔
85
                $template->global->coreCaptured = false;
1✔
86
                $template->render($block);
1✔
87
        }
1✔
88

89

90
        /**
91
         * Renders template to string.
92
         * @param  object|mixed[]  $params
93
         */
94
        public function renderToString(string $name, object|array $params = [], ?string $block = null): string
1✔
95
        {
96
                $template = $this->createTemplate($name, $this->processParams($params));
1✔
97
                $template->global->coreCaptured = true;
1✔
98
                return $template->capture(fn() => $template->render($block));
1✔
99
        }
100

101

102
        /**
103
         * Creates template object.
104
         * @param  mixed[]  $params
105
         */
106
        public function createTemplate(string $name, array $params = [], bool $clearCache = true): Runtime\Template
1✔
107
        {
108
                $this->configurationHash = $clearCache ? null : $this->configurationHash;
1✔
109
                $class = $this->loadTemplate($name);
1✔
110
                $this->providers->fn = $this->functions;
1✔
111
                return new $class(
1✔
112
                        $this,
1✔
113
                        $params,
114
                        $this->filters,
1✔
115
                        $this->providers,
1✔
116
                        $name,
117
                );
118
        }
119

120

121
        /**
122
         * Compiles template to PHP code.
123
         */
124
        public function compile(string $name): string
1✔
125
        {
126
                if ($this->sandboxed && !$this->policy) {
1✔
127
                        throw new \LogicException('In sandboxed mode you need to set a security policy.');
1✔
128
                }
129

130
                $source = $this->loadCompatible($name);
1✔
131

132
                try {
133
                        $node = $this->parse($source->content);
1✔
134
                        $this->applyPasses($node);
1✔
135
                        $compiled = $this->generate($node, $this->getTemplateClass($name), $source->sourceName);
1✔
136

137
                } catch (\Throwable $e) {
1✔
138
                        if (!$e instanceof CompileException && !$e instanceof SecurityViolationException) {
1✔
139
                                $e = new CompileException("Thrown exception '{$e->getMessage()}'", previous: $e);
1✔
140
                        }
141

142
                        $e->setSource(new SourceReference($source->sourceName, $e->getSource()?->line, $e->getSource()?->column, $source->content));
1✔
143
                        throw $e;
1✔
144
                }
145

146
                if ($this->phpBinary) {
1✔
UNCOV
147
                        Compiler\PhpHelpers::checkCode($this->phpBinary, $compiled, "(compiled $source->sourceName)");
×
148
                }
149

150
                return $compiled;
1✔
151
        }
152

153

154
        /**
155
         * Parses template to AST node.
156
         */
157
        public function parse(string $template): TemplateNode
1✔
158
        {
159
                $parser = new Compiler\TemplateParser;
1✔
160
                $parser->getLexer()->setSyntax($this->syntax);
1✔
161
                $parser->strict = $this->strictParsing;
1✔
162

163
                foreach ($this->extensions as $extension) {
1✔
164
                        $extension->beforeCompile($this);
1✔
165
                        $parser->addTags($extension->getTags());
1✔
166
                }
167

168
                return $parser
169
                        ->setContentType($this->contentType)
1✔
170
                        ->setPolicy($this->getPolicy(effective: true))
1✔
171
                        ->parse($template);
1✔
172
        }
173

174

175
        /**
176
         * Calls node visitors.
177
         */
178
        public function applyPasses(TemplateNode &$node): void
1✔
179
        {
180
                $passes = [];
1✔
181
                foreach ($this->extensions as $extension) {
1✔
182
                        $passes = array_merge($passes, $extension->getPasses());
1✔
183
                }
184

185
                $passes = Helpers::sortBeforeAfter($passes);
1✔
186
                foreach ($passes as $pass) {
1✔
187
                        $pass = $pass instanceof \stdClass ? $pass->subject : $pass;
1✔
188
                        ($pass)($node);
1✔
189
                }
190
        }
1✔
191

192

193
        /**
194
         * Generates compiled PHP code.
195
         */
196
        public function generate(TemplateNode $node, string $className, ?string $sourceName): string
1✔
197
        {
198
                $generator = new Compiler\TemplateGenerator;
1✔
199
                $generator->buildClass($node);
1✔
200
                return $generator->generateCode(
1✔
201
                        $className,
1✔
202
                        $sourceName,
203
                        $this->strictTypes,
1✔
204
                );
205
        }
206

207

208
        /**
209
         * Compiles template to cache.
210
         * @throws \LogicException
211
         */
212
        public function warmupCache(string $name): void
1✔
213
        {
214
                if (!$this->cache->directory) {
1✔
UNCOV
215
                        throw new \LogicException('Path to temporary directory is not set.');
×
216
                }
217

218
                $this->loadTemplate($name);
1✔
219
        }
1✔
220

221

222
        private function loadTemplate(string $name): string
1✔
223
        {
224
                $class = $this->getTemplateClass($name);
1✔
225
                if (class_exists($class, false)) {
1✔
226
                        // nothing
227
                } elseif ($this->cache->directory) {
1✔
228
                        $this->cache->loadOrCreate($this, $name);
1✔
229
                } else {
230
                        $compiled = $this->compile($name);
1✔
231
                        if (@eval(substr($compiled, 5)) === false) { // @ is escalated to exception, substr removes <?php
1✔
UNCOV
232
                                throw new CompileException(
×
UNCOV
233
                                        'Error in template: ' . error_get_last()['message'],
×
UNCOV
234
                                        new SourceReference("$name (compiled)", code: $compiled),
×
235
                                );
236
                        }
237
                }
238
                return $class;
1✔
239
        }
240

241

242
        /**
243
         * Returns the file path where compiled template will be cached.
244
         */
245
        public function getCacheFile(string $name): string
1✔
246
        {
247
                return $this->cache->generateFilePath($this, $name);
1✔
248
        }
249

250

251
        /**
252
         * Returns the PHP class name for compiled template.
253
         */
254
        public function getTemplateClass(string $name): string
1✔
255
        {
256
                return 'Template_' . $this->generateTemplateHash($name);
1✔
257
        }
258

259

260
        /**
261
         * Generates unique hash for template based on current configuration.
262
         * Used to create isolated cache files for different engine configurations.
263
         * @internal
264
         */
265
        public function generateTemplateHash(string $name): string
1✔
266
        {
267
                $hash = $this->configurationHash ?? hash('xxh128', serialize($this->generateConfigurationSignature()));
1✔
268
                $hash .= $this->getLoader()->getUniqueId($name);
1✔
269
                return substr(hash('xxh128', $hash), 0, 10);
1✔
270
        }
271

272

273
        /**
274
         * Returns values that determine isolation for different configurations.
275
         * When any of these values change, a new compiled template is created to avoid conflicts.
276
         */
277
        protected function generateConfigurationSignature(): array
278
        {
279
                return [
280
                        $this->contentType,
1✔
281
                        $this->strictTypes,
1✔
282
                        $this->strictParsing,
1✔
283
                        $this->syntax,
1✔
284
                        array_map(
1✔
285
                                fn($extension) => [get_debug_type($extension), $extension->getCacheKey($this)],
1✔
286
                                $this->extensions,
1✔
287
                        ),
288
                ];
289
        }
290

291

292
        /**
293
         * Registers run-time filter.
294
         */
295
        public function addFilter(string $name, callable $callback): static
1✔
296
        {
297
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
UNCOV
298
                        throw new \LogicException("Invalid filter name '$name'.");
×
299
                }
300

301
                $this->filters->add($name, $callback);
1✔
302
                return $this;
1✔
303
        }
304

305

306
        #[\Deprecated('Use addFilter() instead.')]
307
        public function addFilterLoader(callable $loader): static
1✔
308
        {
309
                trigger_error('Filter loader is deprecated, use addFilter() instead.', E_USER_DEPRECATED);
1✔
310
                $this->filters->add(null, $loader);
1✔
311
                return $this;
1✔
312
        }
313

314

315
        /**
316
         * Returns all run-time filters.
317
         * @return callable[]
318
         */
319
        public function getFilters(): array
320
        {
321
                return $this->filters->getAll();
1✔
322
        }
323

324

325
        /**
326
         * Call a run-time filter.
327
         * @param  mixed[]  $args
328
         */
329
        public function invokeFilter(string $name, array $args): mixed
1✔
330
        {
331
                return ($this->filters->$name)(...$args);
1✔
332
        }
333

334

335
        /**
336
         * Adds new extension.
337
         */
338
        public function addExtension(Extension $extension): static
1✔
339
        {
340
                $this->extensions[] = $extension;
1✔
341
                foreach ($extension->getFilters() as $name => $value) {
1✔
342
                        $this->filters->add($name, $value);
1✔
343
                }
344

345
                foreach ($extension->getFunctions() as $name => $value) {
1✔
346
                        $this->functions->add($name, $value);
1✔
347
                }
348

349
                foreach ($extension->getProviders() as $name => $value) {
1✔
UNCOV
350
                        $this->providers->$name = $value;
×
351
                }
352
                return $this;
1✔
353
        }
354

355

356
        /** @return Extension[] */
357
        public function getExtensions(): array
358
        {
359
                return $this->extensions;
1✔
360
        }
361

362

363
        /**
364
         * Registers run-time function.
365
         */
366
        public function addFunction(string $name, callable $callback): static
1✔
367
        {
368
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
UNCOV
369
                        throw new \LogicException("Invalid function name '$name'.");
×
370
                }
371

372
                $this->functions->add($name, $callback);
1✔
373
                return $this;
1✔
374
        }
375

376

377
        /**
378
         * Call a run-time function.
379
         * @param  mixed[]  $args
380
         */
381
        public function invokeFunction(string $name, array $args): mixed
1✔
382
        {
383
                return ($this->functions->$name)(null, ...$args);
1✔
384
        }
385

386

387
        /**
388
         * @return callable[]
389
         */
390
        public function getFunctions(): array
391
        {
392
                return $this->functions->getAll();
1✔
393
        }
394

395

396
        /**
397
         * Adds new provider.
398
         */
399
        public function addProvider(string $name, mixed $provider): static
1✔
400
        {
401
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
UNCOV
402
                        throw new \LogicException("Invalid provider name '$name'.");
×
403
                }
404

405
                $this->providers->$name = $provider;
1✔
406
                return $this;
1✔
407
        }
408

409

410
        /**
411
         * Returns all providers.
412
         * @return mixed[]
413
         */
414
        public function getProviders(): array
415
        {
416
                return (array) $this->providers;
1✔
417
        }
418

419

420
        public function setPolicy(?Policy $policy): static
1✔
421
        {
422
                $this->policy = $policy;
1✔
423
                return $this;
1✔
424
        }
425

426

427
        public function getPolicy(bool $effective = false): ?Policy
1✔
428
        {
429
                return !$effective || $this->sandboxed
1✔
430
                        ? $this->policy
1✔
431
                        : null;
1✔
432
        }
433

434

435
        public function setExceptionHandler(callable $handler): static
1✔
436
        {
437
                $this->providers->coreExceptionHandler = $handler;
1✔
438
                return $this;
1✔
439
        }
440

441

442
        public function setSandboxMode(bool $state = true): static
1✔
443
        {
444
                $this->sandboxed = $state;
1✔
445
                return $this;
1✔
446
        }
447

448

449
        public function setContentType(string $type): static
1✔
450
        {
451
                $this->contentType = $type;
1✔
452
                return $this;
1✔
453
        }
454

455

456
        /**
457
         * Sets path to temporary directory.
458
         */
459
        public function setTempDirectory(?string $path): static
1✔
460
        {
461
                $this->cache->directory = $path;
1✔
462
                return $this;
1✔
463
        }
464

465

466
        /**
467
         * Sets auto-refresh mode.
468
         */
469
        public function setAutoRefresh(bool $state = true): static
470
        {
UNCOV
471
                $this->cache->autoRefresh = $state;
×
UNCOV
472
                return $this;
×
473
        }
474

475

476
        /**
477
         * Enables declare(strict_types=1) in templates.
478
         */
479
        public function setStrictTypes(bool $state = true): static
1✔
480
        {
481
                $this->strictTypes = $state;
1✔
482
                return $this;
1✔
483
        }
484

485

486
        public function setStrictParsing(bool $state = true): static
1✔
487
        {
488
                $this->strictParsing = $state;
1✔
489
                return $this;
1✔
490
        }
491

492

493
        public function isStrictParsing(): bool
494
        {
495
                return $this->strictParsing;
1✔
496
        }
497

498

499
        /**
500
         * Sets the locale. It uses the same identifiers as the PHP intl extension.
501
         */
502
        public function setLocale(?string $locale): static
503
        {
UNCOV
504
                if ($locale && !extension_loaded('intl')) {
×
UNCOV
505
                        throw new RuntimeException("Setting a locale requires the 'intl' extension to be installed.");
×
506
                }
UNCOV
507
                $this->locale = $locale;
×
UNCOV
508
                return $this;
×
509
        }
510

511

512
        public function getLocale(): ?string
513
        {
514
                return $this->locale;
1✔
515
        }
516

517

518
        public function setLoader(Loader $loader): static
1✔
519
        {
520
                $this->loader = $loader;
1✔
521
                return $this;
1✔
522
        }
523

524

525
        public function getLoader(): Loader
526
        {
527
                return $this->loader ??= new Loaders\FileLoader;
1✔
528
        }
529

530

531
        public function enablePhpLinter(?string $phpBinary): static
532
        {
UNCOV
533
                $this->phpBinary = $phpBinary;
×
UNCOV
534
                return $this;
×
535
        }
536

537

538
        /**
539
         * Sets default Latte syntax. Available options: 'single', 'double', 'off'
540
         */
541
        public function setSyntax(string $syntax): static
1✔
542
        {
543
                $this->syntax = $syntax;
1✔
544
                return $this;
1✔
545
        }
546

547

548
        /**
549
         * @param  object|mixed[]  $params
550
         * @return mixed[]
551
         */
552
        private function processParams(object|array $params): array
1✔
553
        {
554
                if (is_array($params)) {
1✔
555
                        return $params;
1✔
556
                }
557

558
                $rc = new \ReflectionClass($params);
1✔
559
                $methods = $rc->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
560
                foreach ($methods as $method) {
1✔
561
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
562
                                $this->addFilter($method->name, [$params, $method->name]);
1✔
563
                        }
564

565
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
566
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
567
                        }
568
                }
569

570
                $res = get_object_vars($params);
1✔
571
                if (PHP_VERSION_ID >= 80400) {
1✔
UNCOV
572
                        foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
×
UNCOV
573
                                if ($property->isVirtual() && $property->hasHook(\PropertyHookType::Get)) {
×
UNCOV
574
                                        $name = $property->getName();
×
UNCOV
575
                                        $res[$name] = $params->$name;
×
576
                                }
577
                        }
578
                }
579

580
                return $res;
1✔
581
        }
582

583

584
        /** @internal */
585
        public function loadCompatible(string $name): LoadedContent
1✔
586
        {
587
                $loader = $this->getLoader();
1✔
588
                return method_exists($loader, 'load') // back compatibility
1✔
589
                        ? $loader->load($name)
1✔
590
                        : new LoadedContent($loader->getContent($name));
1✔
591
        }
592
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc