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

nette / latte / 22371225351

24 Feb 2026 04:03PM UTC coverage: 93.907% (-0.05%) from 93.959%
22371225351

push

github

dg
merged PositionAwareException into exceptions.php

14 of 15 new or added lines in 1 file covered. (93.33%)

29 existing lines in 1 file now uncovered.

5271 of 5613 relevant lines covered (93.91%)

0.94 hits per line

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

83.5
/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, md5, preg_match, serialize, strpos, substr;
12
use const PHP_VERSION_ID;
13

14

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

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

28
        /** @deprecated use ContentType::* */
29
        public const
30
                CONTENT_HTML = ContentType::Html,
31
                CONTENT_XML = ContentType::Xml,
32
                CONTENT_JS = ContentType::JavaScript,
33
                CONTENT_CSS = ContentType::Css,
34
                CONTENT_ICAL = ContentType::ICal,
35
                CONTENT_TEXT = ContentType::Text;
36

37
        private ?Loader $loader = null;
38
        private Runtime\FilterExecutor $filters;
39
        private Runtime\FunctionExecutor $functions;
40
        private \stdClass $providers;
41

42
        /** @var Extension[] */
43
        private array $extensions = [];
44
        private string $contentType = ContentType::Html;
45
        private Runtime\Cache $cache;
46

47
        /** @var array<string, bool> */
48
        private array $features = [
49
                Feature::StrictTypes => false,
50
                Feature::StrictParsing => false,
51
        ];
52

53
        private ?Policy $policy = null;
54
        private bool $sandboxed = false;
55
        private ?string $phpBinary = null;
56
        private ?string $configurationHash;
57
        private ?string $locale = null;
58
        private ?string $syntax = null;
59

60

61
        public function __construct()
62
        {
63
                $this->cache = new Runtime\Cache;
1✔
64
                $this->filters = new Runtime\FilterExecutor;
1✔
65
                $this->functions = new Runtime\FunctionExecutor;
1✔
66
                $this->providers = new \stdClass;
1✔
67
                $this->addExtension(new Essential\CoreExtension);
1✔
68
                $this->addExtension(new Sandbox\SandboxExtension);
1✔
69
        }
1✔
70

71

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

83

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

95

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

114

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

124
                $template = $this->getLoader()->getContent($name);
1✔
125

126
                try {
127
                        $node = $this->parse($template);
1✔
128
                        $this->applyPasses($node);
1✔
129
                        $compiled = $this->generate($node, $name);
1✔
130

131
                } catch (\Throwable $e) {
1✔
132
                        if (!$e instanceof CompileException && !$e instanceof SecurityViolationException) {
1✔
133
                                $e = new CompileException("Thrown exception '{$e->getMessage()}'", previous: $e);
1✔
134
                        }
135

136
                        throw $e->setSource($template, $name);
1✔
137
                }
138

139
                if ($this->phpBinary) {
1✔
UNCOV
140
                        Compiler\PhpHelpers::checkCode($this->phpBinary, $compiled, "(compiled $name)");
×
141
                }
142

143
                return $compiled;
1✔
144
        }
145

146

147
        /**
148
         * Parses template to AST node.
149
         */
150
        public function parse(string $template): TemplateNode
1✔
151
        {
152
                $parser = new Compiler\TemplateParser;
1✔
153
                $parser->getLexer()->setSyntax($this->syntax);
1✔
154
                $parser->strict = $this->features[Feature::StrictParsing];
1✔
155

156
                foreach ($this->extensions as $extension) {
1✔
157
                        $extension->beforeCompile($this);
1✔
158
                        $parser->addTags($extension->getTags());
1✔
159
                }
160

161
                return $parser
162
                        ->setContentType($this->contentType)
1✔
163
                        ->setPolicy($this->getPolicy(effective: true))
1✔
164
                        ->parse($template);
1✔
165
        }
166

167

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

178
                $passes = Helpers::sortBeforeAfter($passes);
1✔
179
                foreach ($passes as $pass) {
1✔
180
                        $pass = $pass instanceof \stdClass ? $pass->subject : $pass;
1✔
181
                        ($pass)($node);
1✔
182
                }
183
        }
1✔
184

185

186
        /**
187
         * Generates compiled PHP code.
188
         */
189
        public function generate(TemplateNode $node, string $name): string
1✔
190
        {
191
                $generator = new Compiler\TemplateGenerator;
1✔
192
                return $generator->generate(
1✔
193
                        $node,
1✔
194
                        $this->getTemplateClass($name),
1✔
195
                        $name,
196
                        $this->features[Feature::StrictTypes],
1✔
197
                );
198
        }
199

200

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

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

214

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

232

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

241

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

250

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

263

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

281

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

291
                $this->filters->add($name, $callback);
1✔
292
                return $this;
1✔
293
        }
294

295

296
        /**
297
         * Registers filter loader.
298
         */
299
        public function addFilterLoader(callable $loader): static
1✔
300
        {
301
                $this->filters->add(null, $loader);
1✔
302
                return $this;
1✔
303
        }
304

305

306
        /**
307
         * Returns all run-time filters.
308
         * @return callable[]
309
         */
310
        public function getFilters(): array
311
        {
312
                return $this->filters->getAll();
1✔
313
        }
314

315

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

325

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

336
                foreach ($extension->getFunctions() as $name => $value) {
1✔
337
                        $this->functions->add($name, $value);
1✔
338
                }
339

340
                foreach ($extension->getProviders() as $name => $value) {
1✔
UNCOV
341
                        $this->providers->$name = $value;
×
342
                }
343
                return $this;
1✔
344
        }
345

346

347
        /** @return Extension[] */
348
        public function getExtensions(): array
349
        {
350
                return $this->extensions;
1✔
351
        }
352

353

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

363
                $this->functions->add($name, $callback);
1✔
364
                return $this;
1✔
365
        }
366

367

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

377

378
        /**
379
         * @return callable[]
380
         */
381
        public function getFunctions(): array
382
        {
383
                return $this->functions->getAll();
1✔
384
        }
385

386

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

396
                $this->providers->$name = $provider;
1✔
397
                return $this;
1✔
398
        }
399

400

401
        /**
402
         * Returns all providers.
403
         * @return mixed[]
404
         */
405
        public function getProviders(): array
406
        {
407
                return (array) $this->providers;
1✔
408
        }
409

410

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

417

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

425

426
        public function setExceptionHandler(callable $handler): static
1✔
427
        {
428
                $this->providers->coreExceptionHandler = $handler;
1✔
429
                return $this;
1✔
430
        }
431

432

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

439

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

446

447
        /**
448
         * Sets path to cache directory.
449
         */
450
        public function setCacheDirectory(?string $path): static
1✔
451
        {
452
                $this->cache->directory = $path;
1✔
453
                return $this;
1✔
454
        }
455

456

457
        public function setTempDirectory(?string $path): static
458
        {
459
                return $this->setCacheDirectory($path);
×
460
        }
461

462

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

472

473
        /**
474
         * Enables or disables an engine feature.
475
         */
476
        public function setFeature(string $feature, bool $state = true): static
1✔
477
        {
478
                $this->features[$feature] = $state;
1✔
479
                return $this;
1✔
480
        }
481

482

483
        /**
484
         * Checks if a feature is enabled.
485
         */
486
        public function hasFeature(string $feature): bool
1✔
487
        {
488
                return $this->features[$feature] ?? throw new \LogicException("Unknown feature '$feature'.");
1✔
489
        }
490

491

492
        /**
493
         * Enables declare(strict_types=1) in templates.
494
         */
495
        public function setStrictTypes(bool $state = true): static
496
        {
UNCOV
497
                return $this->setFeature(Feature::StrictTypes, $state);
×
498
        }
499

500

501
        public function setStrictParsing(bool $state = true): static
502
        {
UNCOV
503
                return $this->setFeature(Feature::StrictParsing, $state);
×
504
        }
505

506

507
        public function isStrictParsing(): bool
508
        {
509
                return $this->hasFeature(Feature::StrictParsing);
1✔
510
        }
511

512

513
        /**
514
         * Sets the locale. It uses the same identifiers as the PHP intl extension.
515
         */
516
        public function setLocale(?string $locale): static
517
        {
UNCOV
518
                if ($locale && !extension_loaded('intl')) {
×
UNCOV
519
                        throw new RuntimeException("Setting a locale requires the 'intl' extension to be installed.");
×
520
                }
521
                $this->locale = $locale;
×
UNCOV
522
                return $this;
×
523
        }
524

525

526
        public function getLocale(): ?string
527
        {
528
                return $this->locale;
1✔
529
        }
530

531

532
        public function setLoader(Loader $loader): static
1✔
533
        {
534
                $this->loader = $loader;
1✔
535
                return $this;
1✔
536
        }
537

538

539
        public function getLoader(): Loader
540
        {
541
                return $this->loader ??= new Loaders\FileLoader;
1✔
542
        }
543

544

545
        public function enablePhpLinter(?string $phpBinary): static
546
        {
UNCOV
547
                $this->phpBinary = $phpBinary;
×
UNCOV
548
                return $this;
×
549
        }
550

551

552
        /**
553
         * Sets default Latte syntax. Available options: 'single', 'double', 'off'
554
         */
555
        public function setSyntax(string $syntax): static
1✔
556
        {
557
                $this->syntax = $syntax;
1✔
558
                return $this;
1✔
559
        }
560

561

562
        /**
563
         * @param  object|mixed[]  $params
564
         * @return mixed[]
565
         */
566
        private function processParams(object|array $params): array
1✔
567
        {
568
                if (is_array($params)) {
1✔
569
                        return $params;
1✔
570
                }
571

572
                $rc = new \ReflectionClass($params);
1✔
573
                $methods = $rc->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
574
                foreach ($methods as $method) {
1✔
575
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
576
                                $this->addFilter($method->name, [$params, $method->name]);
1✔
577
                        }
578

579
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
580
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
581
                        }
582

583
                        if (strpos((string) $method->getDocComment(), '@filter')) {
1✔
584
                                trigger_error('Annotation @filter is deprecated, use attribute #[Latte\Attributes\TemplateFilter]');
×
585
                                $this->addFilter($method->name, [$params, $method->name]);
×
586
                        }
587

588
                        if (strpos((string) $method->getDocComment(), '@function')) {
1✔
UNCOV
589
                                trigger_error('Annotation @function is deprecated, use attribute #[Latte\Attributes\TemplateFunction]');
×
UNCOV
590
                                $this->addFunction($method->name, [$params, $method->name]);
×
591
                        }
592
                }
593

594
                $res = get_object_vars($params);
1✔
595
                if (PHP_VERSION_ID >= 80400) {
1✔
UNCOV
596
                        foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
×
UNCOV
597
                                if ($property->isVirtual() && $property->hasHook(\PropertyHookType::Get)) {
×
UNCOV
598
                                        $name = $property->getName();
×
UNCOV
599
                                        $res[$name] = $params->$name;
×
600
                                }
601
                        }
602
                }
603

604
                return $res;
1✔
605
        }
606

607

608
        public function __get(string $name)
609
        {
UNCOV
610
                if ($name === 'onCompile') {
×
UNCOV
611
                        $trace = debug_backtrace(0)[0];
×
UNCOV
612
                        $loc = isset($trace['file'], $trace['line'])
×
UNCOV
613
                                ? ' (in ' . $trace['file'] . ' on ' . $trace['line'] . ')'
×
UNCOV
614
                                : '';
×
UNCOV
615
                        throw new \LogicException('You use Latte 3 together with the code designed for Latte 2' . $loc);
×
616
                }
617
        }
618
}
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