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

nette / latte / 16321661677

16 Jul 2025 01:41PM UTC coverage: 94.38% (+0.6%) from 93.814%
16321661677

push

github

dg
Filter |escape is globally forbidden

To avoid problems with automatic and forced escaping

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

39 existing lines in 5 files now uncovered.

5223 of 5534 relevant lines covered (94.38%)

0.94 hits per line

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

85.79
/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.1.0';
23
        public const VersionId = 30100;
24

25
        #[\Deprecated('use Latte\Engine::Version')]
26
        public const VERSION = self::Version;
27

28
        #[\Deprecated('use Latte\Engine::VersionId')]
29
        public const VERSION_ID = self::VersionId;
30

31
        #[\Deprecated('use Latte\ContentType::Html')]
32
        public const CONTENT_HTML = ContentType::Html;
33

34
        #[\Deprecated('use Latte\ContentType::Xml')]
35
        public const CONTENT_XML = ContentType::Xml;
36

37
        #[\Deprecated('use Latte\ContentType::JavaScript')]
38
        public const CONTENT_JS = ContentType::JavaScript;
39

40
        #[\Deprecated('use Latte\ContentType::Css')]
41
        public const CONTENT_CSS = ContentType::Css;
42

43
        #[\Deprecated('use Latte\ContentType::ICal')]
44
        public const CONTENT_ICAL = ContentType::ICal;
45

46
        #[\Deprecated('use Latte\ContentType::Text')]
47
        public const CONTENT_TEXT = ContentType::Text;
48

49
        private ?Loader $loader = null;
50
        private Runtime\FilterExecutor $filters;
51
        private Runtime\FunctionExecutor $functions;
52
        private \stdClass $providers;
53

54
        /** @var Extension[] */
55
        private array $extensions = [];
56
        private string $contentType = ContentType::Html;
57
        private Cache $cache;
58
        private bool $strictTypes = true;
59
        private bool $strictParsing = false;
60
        private ?Policy $policy = null;
61
        private bool $sandboxed = false;
62
        private ?string $phpBinary = null;
63
        private ?string $environmentHash;
64
        private ?string $locale = null;
65

66

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

77

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

89

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

101

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

120

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

130
                $template = $this->getLoader()->getContent($name);
1✔
131

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

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

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

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

149
                return $compiled;
1✔
150
        }
151

152

153
        /**
154
         * Parses template to AST node.
155
         */
156
        public function parse(string $template): TemplateNode
1✔
157
        {
158
                $parser = new Compiler\TemplateParser;
1✔
159
                $parser->strict = $this->strictParsing;
1✔
160

161
                foreach ($this->extensions as $extension) {
1✔
162
                        $extension->beforeCompile($this);
1✔
163
                        $parser->addTags($extension->getTags());
1✔
164
                }
165

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

172

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

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

190

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

205

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

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

219

220
        private function loadTemplate(string $name): string
1✔
221
        {
222
                $class = $this->getTemplateClass($name);
1✔
223
                if (class_exists($class, 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✔
UNCOV
230
                                throw (new CompileException('Error in template: ' . error_get_last()['message']))
×
UNCOV
231
                                        ->setSource($compiled, "$name (compiled)");
×
232
                        }
233
                }
234
                return $class;
1✔
235
        }
236

237

238
        public function getCacheFile(string $name): string
1✔
239
        {
240
                return $this->cache->generateFileName($name, $this->generateTemplateHash($name));
1✔
241
        }
242

243

244
        public function getTemplateClass(string $name): string
1✔
245
        {
246
                return 'Template_' . $this->generateTemplateHash($name);
1✔
247
        }
248

249

250
        private function generateTemplateHash(string $name): string
1✔
251
        {
252
                $this->environmentHash ??= hash('xxh128', serialize($this->getCacheKey()));
1✔
253
                $hash = $this->environmentHash . $this->getLoader()->getUniqueId($name);
1✔
254
                return substr(hash('xxh128', $hash), 0, 10);
1✔
255
        }
256

257

258
        /**
259
         * Values that affect the results of compilation and the name of the cache file.
260
         */
261
        protected function getCacheKey(): array
262
        {
263
                return [
264
                        $this->contentType,
1✔
265
                        array_map(
1✔
266
                                fn($extension) => [
1✔
267
                                        get_debug_type($extension),
1✔
268
                                        $extension->getCacheKey($this),
1✔
269
                                        filemtime((new \ReflectionObject($extension))->getFileName()),
1✔
270
                                ],
1✔
271
                                $this->extensions,
1✔
272
                        ),
273
                ];
274
        }
275

276

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

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

290

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

300

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

310

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

320

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

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

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

341

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

348

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

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

362

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

372

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

381

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

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

395

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

405

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

412

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

420

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

427

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

434

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

441

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

451

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

461

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

471

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

478

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

484

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

497

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

503

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

510

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

516

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

523

524
        /**
525
         * @param  object|mixed[]  $params
526
         * @return mixed[]
527
         */
528
        private function processParams(object|array $params): array
1✔
529
        {
530
                if (is_array($params)) {
1✔
531
                        return $params;
1✔
532
                }
533

534
                $rc = new \ReflectionClass($params);
1✔
535
                $methods = $rc->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
536
                foreach ($methods as $method) {
1✔
537
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
538
                                $this->addFilter($method->name, [$params, $method->name]);
1✔
539
                        }
540

541
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
542
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
543
                        }
544
                }
545

546
                $res = get_object_vars($params);
1✔
547
                if (PHP_VERSION_ID >= 80400) {
1✔
548
                        foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
×
549
                                if ($property->isVirtual() && $property->hasHook(\PropertyHookType::Get)) {
×
550
                                        $name = $property->getName();
×
551
                                        $res[$name] = $params->$name;
×
552
                                }
553
                        }
554
                }
555

556
                return $res;
1✔
557
        }
558

559

560
        public function __get(string $name)
561
        {
562
                if ($name === 'onCompile') {
×
563
                        $trace = debug_backtrace(0)[0];
×
564
                        $loc = isset($trace['file'], $trace['line'])
×
565
                                ? ' (in ' . $trace['file'] . ' on ' . $trace['line'] . ')'
×
566
                                : '';
×
567
                        throw new \LogicException('You use Latte 3 together with the code designed for Latte 2' . $loc);
×
568
                }
569
        }
570
}
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