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

nette / latte / 21300823682

23 Jan 2026 03:51PM UTC coverage: 93.907%. Remained the same
21300823682

push

github

dg
added setCacheDirectory(), replces setTempDirectory()

"temp" was confusing with "template"

1 of 2 new or added lines in 1 file covered. (50.0%)

21 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
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, get_debug_type, get_object_vars, is_array, md5, preg_match, serialize, strpos, substr;
14
use const PHP_VERSION_ID;
15

16

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

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

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

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

44
        /** @var Extension[] */
45
        private array $extensions = [];
46
        private string $contentType = ContentType::Html;
47
        private Cache $cache;
48

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

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

62

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

73

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

85

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

97

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

116

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

126
                $template = $this->getLoader()->getContent($name);
1✔
127

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

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

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

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

145
                return $compiled;
1✔
146
        }
147

148

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

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

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

169

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

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

187

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

202

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

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

216

217
        private function loadTemplate(string $name): string
1✔
218
        {
219
                $class = $this->getTemplateClass($name);
1✔
220
                if (class_exists($class, 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✔
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 ?? md5(serialize($this->getCacheKey()));
1✔
261
                $hash .= $this->getLoader()->getUniqueId($name);
1✔
262
                return substr(md5($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
         */
270
        protected function getCacheKey(): array
271
        {
272
                return [
273
                        $this->contentType,
1✔
274
                        $this->features,
1✔
275
                        $this->syntax,
1✔
276
                        array_map(
1✔
277
                                fn($extension) => [get_debug_type($extension), $extension->getCacheKey($this)],
1✔
278
                                $this->extensions,
1✔
279
                        ),
280
                ];
281
        }
282

283

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

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

297

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

307

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

317

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

327

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

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

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

348

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

355

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

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

369

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

379

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

388

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

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

402

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

412

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

419

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

427

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

434

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

441

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

448

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

458

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

464

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

474

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

484

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

493

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

502

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

508

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

514

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

527

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

533

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

540

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

546

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

553

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

563

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

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

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

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

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

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

606
                return $res;
1✔
607
        }
608

609

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