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

nette / latte / 30275971304

27 Jul 2026 02:32PM UTC coverage: 94.928% (+0.4%) from 94.512%
30275971304

push

github

dg
Released version 3.1.6

6064 of 6388 relevant lines covered (94.93%)

0.95 hits per line

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

89.13
/src/Latte/Engine.php
1
<?php declare(strict_types=1);
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
namespace Latte;
9

10
use Latte\Compiler\Nodes\TemplateNode;
11
use function array_map, array_merge, class_exists, extension_loaded, get_debug_type, preg_match, serialize, substr;
12

13

14
/**
15
 * Templating engine Latte.
16
 */
17
class Engine
18
{
19
        public const Version = '3.1.6';
20
        public const VersionId = 30106;
21

22
        /** @deprecated use Engine::Version */
23
        public const
24
                VERSION = self::Version,
25
                VERSION_ID = self::VersionId;
26

27
        #[\Deprecated('use Latte\ContentType::Html')]
28
        public const CONTENT_HTML = ContentType::Html;
29

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

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

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

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

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

45
        private ?Loader $loader = null;
46
        private Runtime\FilterExecutor $filters;
47
        private Runtime\FunctionExecutor $functions;
48
        private \stdClass $providers;
49

50
        /** @var Extension[] */
51
        private array $extensions = [];
52
        private string $contentType = ContentType::Html;
53
        private Runtime\Cache $cache;
54

55
        /** @var array<string, bool> */
56
        private array $features = [
57
                Feature::StrictTypes->name => true,
58
        ];
59

60
        private ?Policy $policy = null;
61
        private bool $sandboxed = false;
62
        private ?string $phpBinary = null;
63
        private ?string $configurationHash = null;
64
        private ?string $locale = null;
65
        private ?string $syntax = null;
66

67

68
        public function __construct()
69
        {
70
                $this->cache = new Runtime\Cache;
1✔
71
                $this->filters = new Runtime\FilterExecutor;
1✔
72
                $this->functions = new Runtime\FunctionExecutor;
1✔
73
                $this->providers = new \stdClass;
1✔
74
                $this->addDefaultExtensions();
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, Helpers::resolveParams($this, $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, Helpers::resolveParams($this, $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
                $template = $this->getLoader()->getContent($name);
1✔
131

132
                try {
133
                        $node = $this->parse($template);
1✔
134
                        $this->applyPasses($node);
1✔
135
                        $compiled = $this->generate($node, $name);
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
                        throw $e->setSource($template, $name);
1✔
143
                }
144

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

149
                return $compiled;
1✔
150
        }
151

152

153
        /**
154
         * Parses template to AST node.
155
         */
156
        public function parse(string $template): TemplateNode
1✔
157
        {
158
                $parser = new Compiler\TemplateParser;
1✔
159
                $parser->getLexer()->setSyntax($this->syntax);
1✔
160
                $parser->strict = $this->hasFeature(Feature::StrictParsing);
1✔
161
                $parser->dedent = $this->hasFeature(Feature::Dedent);
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
         * Runs all registered compiler passes over the AST.
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 $name): string
1✔
197
        {
198
                $generator = new Compiler\TemplateGenerator;
1✔
199
                $generator->buildClass($node, $this->features);
1✔
200
                return $generator->generateCode($this->getTemplateClass($name), $name, $this->hasFeature(Feature::StrictTypes));
1✔
201
        }
202

203

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

214
                $this->loadTemplate($name);
1✔
215
        }
1✔
216

217

218
        /** @return class-string<Runtime\Template> */
219
        private function loadTemplate(string $name): string
1✔
220
        {
221
                $class = $this->getTemplateClass($name);
1✔
222
                if (class_exists($class, autoload: false)) {
1✔
223
                        // nothing
224
                } elseif ($this->cache->directory) {
1✔
225
                        $this->cache->loadOrCreate($this, $name);
1✔
226
                } else {
227
                        $compiled = $this->compile($name);
1✔
228
                        try {
229
                                eval(substr($compiled, 5)); // substr removes <?php
1✔
230
                        } catch (\ParseError $e) {
1✔
231
                                throw (new CompileException('Error in template: ' . $e->getMessage(), previous: $e))
1✔
232
                                        ->setSource($compiled, "$name (compiled)");
1✔
233
                        }
234
                }
235
                return $class;
1✔
236
        }
237

238

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

247

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

256

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

269

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

288

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

298
                $this->filters->add($name, $callback);
1✔
299
                $this->configurationHash = null;
1✔
300
                return $this;
1✔
301
        }
302

303

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

312

313
        /**
314
         * Returns all run-time filters.
315
         * @return array<string, callable>
316
         */
317
        public function getFilters(): array
318
        {
319
                return $this->filters->getAll();
1✔
320
        }
321

322

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

332

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

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

347
                foreach ($extension->getProviders() as $name => $value) {
1✔
348
                        $this->providers->$name = $value;
×
349
                }
350

351
                $this->configurationHash = null;
1✔
352
                return $this;
1✔
353
        }
354

355

356
        /** @return list<Extension> */
357
        public function getExtensions(): array
358
        {
359
                return array_values($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✔
369
                        throw new \LogicException("Invalid function name '$name'.");
×
370
                }
371

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

377

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

387

388
        /**
389
         * Returns all run-time functions.
390
         * @return array<string, callable>
391
         */
392
        public function getFunctions(): array
393
        {
394
                return $this->functions->getAll();
1✔
395
        }
396

397

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

407
                $this->providers->$name = $provider;
1✔
408
                return $this;
1✔
409
        }
410

411

412
        /**
413
         * Returns all providers.
414
         * @return array<string, mixed>
415
         */
416
        public function getProviders(): array
417
        {
418
                return (array) $this->providers;
1✔
419
        }
420

421

422
        public function setPolicy(?Policy $policy): static
1✔
423
        {
424
                $this->policy = $policy;
1✔
425
                $this->configurationHash = null;
1✔
426
                return $this;
1✔
427
        }
428

429

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

437

438
        /**
439
         * Sets a handler called when an exception occurs during template rendering.
440
         */
441
        public function setExceptionHandler(callable $handler): static
1✔
442
        {
443
                $this->providers->coreExceptionHandler = $handler(...);
1✔
444
                return $this;
1✔
445
        }
446

447

448
        public function setSandboxMode(bool $state = true): static
1✔
449
        {
450
                $this->sandboxed = $state;
1✔
451
                $this->configurationHash = null;
1✔
452
                return $this;
1✔
453
        }
454

455

456
        public function setContentType(string $type): static
1✔
457
        {
458
                $this->contentType = $type;
1✔
459
                $this->configurationHash = null;
1✔
460
                return $this;
1✔
461
        }
462

463

464
        /**
465
         * Sets path to cache directory.
466
         */
467
        public function setCacheDirectory(?string $path): static
1✔
468
        {
469
                $this->cache->directory = $path;
1✔
470
                return $this;
1✔
471
        }
472

473

474
        /** @deprecated use setCacheDirectory() instead */
475
        public function setTempDirectory(?string $path): static
476
        {
477
                return $this->setCacheDirectory($path);
×
478
        }
479

480

481
        /**
482
         * Sets auto-refresh mode.
483
         */
484
        public function setAutoRefresh(bool $state = true): static
485
        {
486
                $this->cache->autoRefresh = $state;
×
487
                return $this;
×
488
        }
489

490

491
        /**
492
         * Enables or disables an engine feature.
493
         */
494
        public function setFeature(Feature $feature, bool $state = true): static
1✔
495
        {
496
                $this->features[$feature->name] = $state;
1✔
497
                $this->configurationHash = null;
1✔
498
                return $this;
1✔
499
        }
500

501

502
        /**
503
         * Checks if a feature is enabled.
504
         */
505
        public function hasFeature(Feature $feature): bool
1✔
506
        {
507
                return $this->features[$feature->name] ?? false;
1✔
508
        }
509

510

511
        /**
512
         * Enables declare(strict_types=1) in templates.
513
         * @deprecated use setFeature(Feature::StrictTypes, ...) instead
514
         */
515
        public function setStrictTypes(bool $state = true): static
516
        {
517
                return $this->setFeature(Feature::StrictTypes, $state);
×
518
        }
519

520

521
        /** @deprecated use setFeature(Feature::StrictParsing, ...) instead */
522
        public function setStrictParsing(bool $state = true): static
523
        {
524
                return $this->setFeature(Feature::StrictParsing, $state);
×
525
        }
526

527

528
        /** @deprecated use hasFeature(Feature::StrictParsing) instead */
529
        public function isStrictParsing(): bool
530
        {
531
                return $this->hasFeature(Feature::StrictParsing);
×
532
        }
533

534

535
        /**
536
         * Sets the locale. It uses the same identifiers as the PHP intl extension.
537
         */
538
        public function setLocale(?string $locale): static
539
        {
540
                if ($locale && !extension_loaded('intl')) {
×
541
                        throw new RuntimeException("Setting a locale requires the 'intl' extension to be installed.");
×
542
                }
543
                $this->locale = $locale;
×
544
                $this->configurationHash = null;
×
545
                return $this;
×
546
        }
547

548

549
        public function getLocale(): ?string
550
        {
551
                return $this->locale;
1✔
552
        }
553

554

555
        public function setLoader(Loader $loader): static
1✔
556
        {
557
                $this->loader = $loader;
1✔
558
                return $this;
1✔
559
        }
560

561

562
        public function getLoader(): Loader
563
        {
564
                return $this->loader ??= new Loaders\FileLoader;
1✔
565
        }
566

567

568
        /**
569
         * Validates compiled PHP code using the given PHP binary. Pass null to disable.
570
         */
571
        public function enablePhpLinter(?string $phpBinary): static
572
        {
573
                $this->phpBinary = $phpBinary;
×
574
                return $this;
×
575
        }
576

577

578
        /**
579
         * Sets default Latte syntax. Available options: 'single', 'double', 'off'
580
         */
581
        public function setSyntax(string $syntax): static
1✔
582
        {
583
                $this->syntax = $syntax;
1✔
584
                $this->configurationHash = null;
1✔
585
                return $this;
1✔
586
        }
587

588

589
        /** @deprecated use setFeature(Feature::MigrationWarnings, ...) instead */
590
        public function setMigrationWarnings(bool $state = true): static
591
        {
592
                return $this->setFeature(Feature::MigrationWarnings, $state);
×
593
        }
594

595

596
        protected function addDefaultExtensions(): void
597
        {
598
                $this->addExtension(new Essential\CoreExtension);
1✔
599
                $this->addExtension(new Sandbox\SandboxExtension);
1✔
600
        }
1✔
601
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc