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

nette / latte / 22359082277

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

push

github

dg
fixed operator ! priority

5273 of 5612 relevant lines covered (93.96%)

0.94 hits per line

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

84.92
/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 Cache $cache;
46
        private bool $strictTypes = false;
47
        private bool $strictParsing = false;
48
        private ?Policy $policy = null;
49
        private bool $sandboxed = false;
50
        private ?string $phpBinary = null;
51
        private ?string $configurationHash;
52
        private ?string $locale = null;
53
        private ?string $syntax = null;
54

55

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

66

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

78

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

90

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

109

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

119
                $template = $this->getLoader()->getContent($name);
1✔
120

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

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

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

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

138
                return $compiled;
1✔
139
        }
140

141

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

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

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

162

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

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

180

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

195

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

206
                $this->loadTemplate($name);
1✔
207
        }
1✔
208

209

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

227

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

236

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

245

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

258

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

277

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

287
                $this->filters->add($name, $callback);
1✔
288
                return $this;
1✔
289
        }
290

291

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

301

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

311

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

321

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

332
                foreach ($extension->getFunctions() as $name => $value) {
1✔
333
                        $this->functions->add($name, $value);
1✔
334
                }
335

336
                foreach ($extension->getProviders() as $name => $value) {
1✔
337
                        $this->providers->$name = $value;
×
338
                }
339
                return $this;
1✔
340
        }
341

342

343
        /** @return Extension[] */
344
        public function getExtensions(): array
345
        {
346
                return $this->extensions;
1✔
347
        }
348

349

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

359
                $this->functions->add($name, $callback);
1✔
360
                return $this;
1✔
361
        }
362

363

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

373

374
        /**
375
         * @return callable[]
376
         */
377
        public function getFunctions(): array
378
        {
379
                return $this->functions->getAll();
1✔
380
        }
381

382

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

392
                $this->providers->$name = $provider;
1✔
393
                return $this;
1✔
394
        }
395

396

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

406

407
        public function setPolicy(?Policy $policy): static
1✔
408
        {
409
                $this->policy = $policy;
1✔
410
                return $this;
1✔
411
        }
412

413

414
        public function getPolicy(bool $effective = false): ?Policy
1✔
415
        {
416
                return !$effective || $this->sandboxed
1✔
417
                        ? $this->policy
1✔
418
                        : null;
1✔
419
        }
420

421

422
        public function setExceptionHandler(callable $handler): static
1✔
423
        {
424
                $this->providers->coreExceptionHandler = $handler;
1✔
425
                return $this;
1✔
426
        }
427

428

429
        public function setSandboxMode(bool $state = true): static
1✔
430
        {
431
                $this->sandboxed = $state;
1✔
432
                return $this;
1✔
433
        }
434

435

436
        public function setContentType(string $type): static
1✔
437
        {
438
                $this->contentType = $type;
1✔
439
                return $this;
1✔
440
        }
441

442

443
        /**
444
         * Sets path to temporary directory.
445
         */
446
        public function setTempDirectory(?string $path): static
1✔
447
        {
448
                $this->cache->directory = $path;
1✔
449
                return $this;
1✔
450
        }
451

452

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

462

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

472

473
        public function setStrictParsing(bool $state = true): static
1✔
474
        {
475
                $this->strictParsing = $state;
1✔
476
                return $this;
1✔
477
        }
478

479

480
        public function isStrictParsing(): bool
481
        {
482
                return $this->strictParsing;
1✔
483
        }
484

485

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

498

499
        public function getLocale(): ?string
500
        {
501
                return $this->locale;
1✔
502
        }
503

504

505
        public function setLoader(Loader $loader): static
1✔
506
        {
507
                $this->loader = $loader;
1✔
508
                return $this;
1✔
509
        }
510

511

512
        public function getLoader(): Loader
513
        {
514
                return $this->loader ??= new Loaders\FileLoader;
1✔
515
        }
516

517

518
        public function enablePhpLinter(?string $phpBinary): static
519
        {
520
                $this->phpBinary = $phpBinary;
×
521
                return $this;
×
522
        }
523

524

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

534

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

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

552
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
553
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
554
                        }
555

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

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

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

577
                return $res;
1✔
578
        }
579

580

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