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

nette / latte / 22368407632

24 Feb 2026 08:17PM UTC coverage: 94.825% (-0.2%) from 95.054%
22368407632

push

github

dg
phpstan

135 of 147 new or added lines in 38 files covered. (91.84%)

186 existing lines in 70 files now uncovered.

5534 of 5836 relevant lines covered (94.83%)

0.95 hits per line

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

89.42
/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, get_object_vars, is_array, preg_match, serialize, substr;
12
use const PHP_VERSION_ID;
13

14

15
/**
16
 * Templating engine Latte.
17
 */
18
class Engine
19
{
20
        public const Version = '3.1.1';
21
        public const VersionId = 30101;
22

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

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

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

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

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

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

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

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

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

65

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

76

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

88

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

100

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

119

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

129
                $template = $this->getLoader()->getContent($name);
1✔
130

131
                try {
132
                        $node = $this->parse($template);
1✔
133
                        $this->applyPasses($node);
1✔
134
                        $compiled = $this->generate($node, $name);
1✔
135

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

141
                        throw $e->setSource($template, $name);
1✔
142
                }
143

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

148
                return $compiled;
1✔
149
        }
150

151

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

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

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

172

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

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

190

191
        /**
192
         * Generates compiled PHP code.
193
         */
194
        public function generate(TemplateNode $node, string $name): string
1✔
195
        {
196
                $generator = new Compiler\TemplateGenerator;
1✔
197
                $generator->buildClass($node, $this->migrationWarnings);
1✔
198
                return $generator->generateCode($this->getTemplateClass($name), $name, $this->strictTypes);
1✔
199
        }
200

201

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

212
                $this->loadTemplate($name);
1✔
213
        }
1✔
214

215

216
        /** @return class-string<Runtime\Template> */
217
        private function loadTemplate(string $name): string
1✔
218
        {
219
                $class = $this->getTemplateClass($name);
1✔
220
                if (class_exists($class, autoload: false)) {
1✔
221
                        // nothing
222
                } elseif ($this->cache->directory) {
1✔
223
                        $this->cache->loadOrCreate($this, $name);
1✔
224
                } else {
225
                        $compiled = $this->compile($name);
1✔
226
                        if (@eval(substr($compiled, 5)) === false) { // @ is escalated to exception, substr removes <?php
1✔
NEW
227
                                throw (new CompileException('Error in template: ' . (error_get_last()['message'] ?? '')))
×
228
                                        ->setSource($compiled, "$name (compiled)");
×
229
                        }
230
                }
231
                return $class;
1✔
232
        }
233

234

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

243

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

252

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

265

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

285

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

295
                $this->filters->add($name, $callback);
1✔
296
                return $this;
1✔
297
        }
298

299

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

308

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

318

319
        /**
320
         * Call a run-time filter.
321
         * @param  mixed[]  $args
322
         */
323
        public function invokeFilter(string $name, array $args): mixed
1✔
324
        {
325
                return ($this->filters->$name)(...$args);
1✔
326
        }
327

328

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

339
                foreach ($extension->getFunctions() as $name => $value) {
1✔
340
                        $this->functions->add($name, $value);
1✔
341
                }
342

343
                foreach ($extension->getProviders() as $name => $value) {
1✔
344
                        $this->providers->$name = $value;
×
345
                }
346
                return $this;
1✔
347
        }
348

349

350
        /** @return list<Extension> */
351
        public function getExtensions(): array
352
        {
353
                return array_values($this->extensions);
1✔
354
        }
355

356

357
        /**
358
         * Registers run-time function.
359
         */
360
        public function addFunction(string $name, callable $callback): static
1✔
361
        {
362
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
363
                        throw new \LogicException("Invalid function name '$name'.");
×
364
                }
365

366
                $this->functions->add($name, $callback);
1✔
367
                return $this;
1✔
368
        }
369

370

371
        /**
372
         * Call a run-time function.
373
         * @param  mixed[]  $args
374
         */
375
        public function invokeFunction(string $name, array $args): mixed
1✔
376
        {
377
                return ($this->functions->$name)(null, ...$args);
1✔
378
        }
379

380

381
        /**
382
         * @return array<string, callable>
383
         */
384
        public function getFunctions(): array
385
        {
386
                return $this->functions->getAll();
1✔
387
        }
388

389

390
        /**
391
         * Adds new provider.
392
         */
393
        public function addProvider(string $name, mixed $provider): static
1✔
394
        {
395
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
396
                        throw new \LogicException("Invalid provider name '$name'.");
×
397
                }
398

399
                $this->providers->$name = $provider;
1✔
400
                return $this;
1✔
401
        }
402

403

404
        /**
405
         * Returns all providers.
406
         * @return array<string, mixed>
407
         */
408
        public function getProviders(): array
409
        {
410
                return (array) $this->providers;
1✔
411
        }
412

413

414
        public function setPolicy(?Policy $policy): static
1✔
415
        {
416
                $this->policy = $policy;
1✔
417
                return $this;
1✔
418
        }
419

420

421
        public function getPolicy(bool $effective = false): ?Policy
1✔
422
        {
423
                return !$effective || $this->sandboxed
1✔
424
                        ? $this->policy
1✔
425
                        : null;
1✔
426
        }
427

428

429
        public function setExceptionHandler(callable $handler): static
1✔
430
        {
431
                $this->providers->coreExceptionHandler = $handler(...);
1✔
432
                return $this;
1✔
433
        }
434

435

436
        public function setSandboxMode(bool $state = true): static
1✔
437
        {
438
                $this->sandboxed = $state;
1✔
439
                return $this;
1✔
440
        }
441

442

443
        public function setContentType(string $type): static
1✔
444
        {
445
                $this->contentType = $type;
1✔
446
                return $this;
1✔
447
        }
448

449

450
        /**
451
         * Sets path to temporary directory.
452
         */
453
        public function setTempDirectory(?string $path): static
1✔
454
        {
455
                $this->cache->directory = $path;
1✔
456
                return $this;
1✔
457
        }
458

459

460
        /**
461
         * Sets auto-refresh mode.
462
         */
463
        public function setAutoRefresh(bool $state = true): static
464
        {
465
                $this->cache->autoRefresh = $state;
×
466
                return $this;
×
467
        }
468

469

470
        /**
471
         * Enables declare(strict_types=1) in templates.
472
         */
473
        public function setStrictTypes(bool $state = true): static
1✔
474
        {
475
                $this->strictTypes = $state;
1✔
476
                return $this;
1✔
477
        }
478

479

480
        public function setStrictParsing(bool $state = true): static
1✔
481
        {
482
                $this->strictParsing = $state;
1✔
483
                return $this;
1✔
484
        }
485

486

487
        public function isStrictParsing(): bool
488
        {
489
                return $this->strictParsing;
1✔
490
        }
491

492

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

505

506
        public function getLocale(): ?string
507
        {
508
                return $this->locale;
1✔
509
        }
510

511

512
        public function setLoader(Loader $loader): static
1✔
513
        {
514
                $this->loader = $loader;
1✔
515
                return $this;
1✔
516
        }
517

518

519
        public function getLoader(): Loader
520
        {
521
                return $this->loader ??= new Loaders\FileLoader;
1✔
522
        }
523

524

525
        public function enablePhpLinter(?string $phpBinary): static
526
        {
527
                $this->phpBinary = $phpBinary;
×
528
                return $this;
×
529
        }
530

531

532
        /**
533
         * Sets default Latte syntax. Available options: 'single', 'double', 'off'
534
         */
535
        public function setSyntax(string $syntax): static
1✔
536
        {
537
                $this->syntax = $syntax;
1✔
538
                return $this;
1✔
539
        }
540

541

542
        public function setMigrationWarnings(bool $state = true): static
1✔
543
        {
544
                $this->migrationWarnings = $state;
1✔
545
                return $this;
1✔
546
        }
547

548

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

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

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

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

581
                return $res;
1✔
582
        }
583
}
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