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

nette / latte / 22836865504

09 Mar 2026 03:15AM UTC coverage: 94.972% (+0.2%) from 94.76%
22836865504

push

github

dg
github actions: code coverage job is non-blocking

5610 of 5907 relevant lines covered (94.97%)

0.95 hits per line

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

86.7
/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.2';
21
        public const VersionId = 30102;
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

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

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

68

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

79

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

91

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

103

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

122

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

132
                $template = $this->getLoader()->getContent($name);
1✔
133

134
                try {
135
                        $node = $this->parse($template);
1✔
136
                        $this->applyPasses($node);
1✔
137
                        $compiled = $this->generate($node, $name);
1✔
138

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

144
                        throw $e->setSource($template, $name);
1✔
145
                }
146

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

151
                return $compiled;
1✔
152
        }
153

154

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

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

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

175

176
        /**
177
         * Runs all registered compiler passes over the AST.
178
         */
179
        public function applyPasses(TemplateNode &$node): void
1✔
180
        {
181
                $passes = [];
1✔
182
                foreach ($this->extensions as $extension) {
1✔
183
                        $passes = array_merge($passes, $extension->getPasses());
1✔
184
                }
185

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

193

194
        /**
195
         * Generates compiled PHP code.
196
         */
197
        public function generate(TemplateNode $node, string $name): string
1✔
198
        {
199
                $generator = new Compiler\TemplateGenerator;
1✔
200
                $generator->buildClass($node, $this->features);
1✔
201
                return $generator->generateCode($this->getTemplateClass($name), $name, $this->hasFeature(Feature::StrictTypes));
1✔
202
        }
203

204

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

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

218

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

237

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

246

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

255

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

268

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

287

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

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

301

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

310

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

320

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

330

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

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

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

351

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

358

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

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

372

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

382

383
        /**
384
         * Returns all run-time functions.
385
         * @return array<string, callable>
386
         */
387
        public function getFunctions(): array
388
        {
389
                return $this->functions->getAll();
1✔
390
        }
391

392

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

402
                $this->providers->$name = $provider;
1✔
403
                return $this;
1✔
404
        }
405

406

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

416

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

423

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

431

432
        /**
433
         * Sets a handler called when an exception occurs during template rendering.
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 cache directory.
458
         */
459
        public function setCacheDirectory(?string $path): static
1✔
460
        {
461
                $this->cache->directory = $path;
1✔
462
                return $this;
1✔
463
        }
464

465

466
        /** @deprecated use setCacheDirectory() instead */
467
        public function setTempDirectory(?string $path): static
468
        {
469
                return $this->setCacheDirectory($path);
×
470
        }
471

472

473
        /**
474
         * Sets auto-refresh mode.
475
         */
476
        public function setAutoRefresh(bool $state = true): static
477
        {
478
                $this->cache->autoRefresh = $state;
×
479
                return $this;
×
480
        }
481

482

483
        /**
484
         * Enables or disables an engine feature.
485
         */
486
        public function setFeature(Feature $feature, bool $state = true): static
1✔
487
        {
488
                $this->features[$feature->name] = $state;
1✔
489
                return $this;
1✔
490
        }
491

492

493
        /**
494
         * Checks if a feature is enabled.
495
         */
496
        public function hasFeature(Feature $feature): bool
1✔
497
        {
498
                return $this->features[$feature->name] ?? false;
1✔
499
        }
500

501

502
        /**
503
         * Enables declare(strict_types=1) in templates.
504
         * @deprecated use setFeature(Feature::StrictTypes, ...) instead
505
         */
506
        public function setStrictTypes(bool $state = true): static
507
        {
508
                return $this->setFeature(Feature::StrictTypes, $state);
×
509
        }
510

511

512
        /** @deprecated use setFeature(Feature::StrictParsing, ...) instead */
513
        public function setStrictParsing(bool $state = true): static
514
        {
515
                return $this->setFeature(Feature::StrictParsing, $state);
×
516
        }
517

518

519
        /** @deprecated use hasFeature(Feature::StrictParsing) instead */
520
        public function isStrictParsing(): bool
521
        {
522
                return $this->hasFeature(Feature::StrictParsing);
×
523
        }
524

525

526
        /**
527
         * Sets the locale. It uses the same identifiers as the PHP intl extension.
528
         */
529
        public function setLocale(?string $locale): static
530
        {
531
                if ($locale && !extension_loaded('intl')) {
×
532
                        throw new RuntimeException("Setting a locale requires the 'intl' extension to be installed.");
×
533
                }
534
                $this->locale = $locale;
×
535
                return $this;
×
536
        }
537

538

539
        public function getLocale(): ?string
540
        {
541
                return $this->locale;
1✔
542
        }
543

544

545
        public function setLoader(Loader $loader): static
1✔
546
        {
547
                $this->loader = $loader;
1✔
548
                return $this;
1✔
549
        }
550

551

552
        public function getLoader(): Loader
553
        {
554
                return $this->loader ??= new Loaders\FileLoader;
1✔
555
        }
556

557

558
        /**
559
         * Validates compiled PHP code using the given PHP binary. Pass null to disable.
560
         */
561
        public function enablePhpLinter(?string $phpBinary): static
562
        {
563
                $this->phpBinary = $phpBinary;
×
564
                return $this;
×
565
        }
566

567

568
        /**
569
         * Sets default Latte syntax. Available options: 'single', 'double', 'off'
570
         */
571
        public function setSyntax(string $syntax): static
1✔
572
        {
573
                $this->syntax = $syntax;
1✔
574
                return $this;
1✔
575
        }
576

577

578
        /** @deprecated use setFeature(Feature::MigrationWarnings, ...) instead */
579
        public function setMigrationWarnings(bool $state = true): static
580
        {
581
                return $this->setFeature(Feature::MigrationWarnings, $state);
×
582
        }
583

584

585
        /**
586
         * @param  object|mixed[]  $params
587
         * @return array<string, mixed>
588
         */
589
        private function processParams(object|array $params): array
1✔
590
        {
591
                if (is_array($params)) {
1✔
592
                        return $params;
1✔
593
                }
594

595
                $rc = new \ReflectionClass($params);
1✔
596
                $methods = $rc->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
597
                foreach ($methods as $method) {
1✔
598
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
599
                                $this->addFilter($method->name, $method->getClosure($params));
1✔
600
                        }
601

602
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
603
                                $this->addFunction($method->name, $method->getClosure($params));
1✔
604
                        }
605
                }
606

607
                $res = get_object_vars($params);
1✔
608
                if (PHP_VERSION_ID >= 80400) {
1✔
609
                        foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
×
610
                                if ($property->isVirtual() && $property->hasHook(\PropertyHookType::Get)) {
×
611
                                        $name = $property->getName();
×
612
                                        $res[$name] = $params->$name;
×
613
                                }
614
                        }
615
                }
616

617
                return $res;
1✔
618
        }
619
}
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