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

nette / latte / 18959260431

31 Oct 2025 12:49AM UTC coverage: 93.916% (+0.08%) from 93.838%
18959260431

push

github

dg
foo

5264 of 5605 relevant lines covered (93.92%)

0.94 hits per line

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

84.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, filemtime, 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
        private bool $strictTypes = false;
49
        private bool $strictParsing = false;
50
        private ?Policy $policy = null;
51
        private bool $sandboxed = false;
52
        private ?string $phpBinary = null;
53
        private ?string $configurationHash;
54
        private ?string $locale = null;
55
        private ?string $syntax = null;
56

57

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

68

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

80

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

92

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

111

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

121
                $template = $this->getLoader()->getContent($name);
1✔
122

123
                try {
124
                        $node = $this->parse($template);
1✔
125
                        $this->applyPasses($node);
1✔
126
                        $compiled = $this->generate($node, $name);
1✔
127

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

133
                        throw $e->setSource($template, $name);
1✔
134
                }
135

136
                if ($this->phpBinary) {
1✔
137
                        Compiler\PhpHelpers::checkCode($this->phpBinary, $compiled, "(compiled $name)");
×
138
                }
139

140
                return $compiled;
1✔
141
        }
142

143

144
        /**
145
         * Parses template to AST node.
146
         */
147
        public function parse(string $template): TemplateNode
1✔
148
        {
149
                $parser = new Compiler\TemplateParser;
1✔
150
                $parser->getLexer()->setSyntax($this->syntax);
1✔
151
                $parser->strict = $this->strictParsing;
1✔
152

153
                foreach ($this->extensions as $extension) {
1✔
154
                        $extension->beforeCompile($this);
1✔
155
                        $parser->addTags($extension->getTags());
1✔
156
                }
157

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

164

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

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

182

183
        /**
184
         * Generates compiled PHP code.
185
         */
186
        public function generate(TemplateNode $node, string $name): string
1✔
187
        {
188
                $generator = new Compiler\TemplateGenerator;
1✔
189
                return $generator->generate(
1✔
190
                        $node,
1✔
191
                        $this->getTemplateClass($name),
1✔
192
                        $name,
193
                        $this->strictTypes,
1✔
194
                );
195
        }
196

197

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

208
                $this->loadTemplate($name);
1✔
209
        }
1✔
210

211

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

231

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

240

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

249

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

262

263
        /**
264
         * Returns values that determine isolation for different configurations.
265
         * When any of these values change, a new compiled template is created to avoid conflicts.
266
         */
267
        protected function getCacheKey(): array
268
        {
269
                return [
270
                        $this->contentType,
1✔
271
                        $this->strictTypes,
1✔
272
                        $this->strictParsing,
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✔
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✔
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✔
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✔
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 temporary directory.
449
         */
450
        public function setTempDirectory(?string $path): static
1✔
451
        {
452
                $this->cache->directory = $path;
1✔
453
                return $this;
1✔
454
        }
455

456

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

466

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

476

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

483

484
        public function isStrictParsing(): bool
485
        {
486
                return $this->strictParsing;
1✔
487
        }
488

489

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

502

503
        public function getLocale(): ?string
504
        {
505
                return $this->locale;
1✔
506
        }
507

508

509
        public function setLoader(Loader $loader): static
1✔
510
        {
511
                $this->loader = $loader;
1✔
512
                return $this;
1✔
513
        }
514

515

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

521

522
        public function enablePhpLinter(?string $phpBinary): static
523
        {
524
                $this->phpBinary = $phpBinary;
×
525
                return $this;
×
526
        }
527

528

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

538

539
        /**
540
         * @param  object|mixed[]  $params
541
         * @return mixed[]
542
         */
543
        private function processParams(object|array $params): array
1✔
544
        {
545
                if (is_array($params)) {
1✔
546
                        return $params;
1✔
547
                }
548

549
                $rc = new \ReflectionClass($params);
1✔
550
                $methods = $rc->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
551
                foreach ($methods as $method) {
1✔
552
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
553
                                $this->addFilter($method->name, [$params, $method->name]);
1✔
554
                        }
555

556
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
557
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
558
                        }
559

560
                        if (strpos((string) $method->getDocComment(), '@filter')) {
1✔
561
                                trigger_error('Annotation @filter is deprecated, use attribute #[Latte\Attributes\TemplateFilter]');
×
562
                                $this->addFilter($method->name, [$params, $method->name]);
×
563
                        }
564

565
                        if (strpos((string) $method->getDocComment(), '@function')) {
1✔
566
                                trigger_error('Annotation @function is deprecated, use attribute #[Latte\Attributes\TemplateFunction]');
×
567
                                $this->addFunction($method->name, [$params, $method->name]);
×
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

584

585
        public function __get(string $name)
586
        {
587
                if ($name === 'onCompile') {
×
588
                        $trace = debug_backtrace(0)[0];
×
589
                        $loc = isset($trace['file'], $trace['line'])
×
590
                                ? ' (in ' . $trace['file'] . ' on ' . $trace['line'] . ')'
×
591
                                : '';
×
592
                        throw new \LogicException('You use Latte 3 together with the code designed for Latte 2' . $loc);
×
593
                }
594
        }
595
}
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