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

nette / latte / 15763494949

19 Jun 2025 05:37PM UTC coverage: 93.578% (-0.02%) from 93.597%
15763494949

push

github

dg
optimized global function calls

7 of 12 new or added lines in 8 files covered. (58.33%)

2 existing lines in 2 files now uncovered.

5129 of 5481 relevant lines covered (93.58%)

0.94 hits per line

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

84.18
/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.21';
23
        public const VersionId = 30021;
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 $environmentHash;
54
        private ?string $locale = null;
55

56

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

67

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

79

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

91

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

110

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

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

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

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

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

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

139
                return $compiled;
1✔
140
        }
141

142

143
        /**
144
         * Parses template to AST node.
145
         */
146
        public function parse(string $template): TemplateNode
1✔
147
        {
148
                $parser = new Compiler\TemplateParser;
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
        public function getCacheFile(string $name): string
1✔
229
        {
230
                return $this->cache->generateFileName($name, $this->generateTemplateHash($name));
1✔
231
        }
232

233

234
        public function getTemplateClass(string $name): string
1✔
235
        {
236
                return 'Template_' . $this->generateTemplateHash($name);
1✔
237
        }
238

239

240
        private function generateTemplateHash(string $name): string
1✔
241
        {
242
                $this->environmentHash ??= md5(serialize($this->getCacheKey()));
1✔
243
                $hash = $this->environmentHash . $this->getLoader()->getUniqueId($name);
1✔
244
                return substr(md5($hash), 0, 10);
1✔
245
        }
246

247

248
        /**
249
         * Values that affect the results of compilation and the name of the cache file.
250
         */
251
        protected function getCacheKey(): array
252
        {
253
                return [
254
                        $this->contentType,
1✔
255
                        array_map(
1✔
256
                                fn($extension) => [
1✔
257
                                        get_debug_type($extension),
1✔
258
                                        $extension->getCacheKey($this),
1✔
259
                                        filemtime((new \ReflectionObject($extension))->getFileName()),
1✔
260
                                ],
1✔
261
                                $this->extensions,
1✔
262
                        ),
263
                ];
264
        }
265

266

267
        /**
268
         * Registers run-time filter.
269
         */
270
        public function addFilter(string $name, callable $callback): static
1✔
271
        {
272
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
273
                        throw new \LogicException("Invalid filter name '$name'.");
×
274
                }
275

276
                $this->filters->add($name, $callback);
1✔
277
                return $this;
1✔
278
        }
279

280

281
        /**
282
         * Registers filter loader.
283
         */
284
        public function addFilterLoader(callable $loader): static
1✔
285
        {
286
                $this->filters->add(null, $loader);
1✔
287
                return $this;
1✔
288
        }
289

290

291
        /**
292
         * Returns all run-time filters.
293
         * @return callable[]
294
         */
295
        public function getFilters(): array
296
        {
297
                return $this->filters->getAll();
1✔
298
        }
299

300

301
        /**
302
         * Call a run-time filter.
303
         * @param  mixed[]  $args
304
         */
305
        public function invokeFilter(string $name, array $args): mixed
1✔
306
        {
307
                return ($this->filters->$name)(...$args);
1✔
308
        }
309

310

311
        /**
312
         * Adds new extension.
313
         */
314
        public function addExtension(Extension $extension): static
1✔
315
        {
316
                $this->extensions[] = $extension;
1✔
317
                foreach ($extension->getFilters() as $name => $value) {
1✔
318
                        $this->filters->add($name, $value);
1✔
319
                }
320

321
                foreach ($extension->getFunctions() as $name => $value) {
1✔
322
                        $this->functions->add($name, $value);
1✔
323
                }
324

325
                foreach ($extension->getProviders() as $name => $value) {
1✔
326
                        $this->providers->$name = $value;
×
327
                }
328
                return $this;
1✔
329
        }
330

331

332
        /** @return Extension[] */
333
        public function getExtensions(): array
334
        {
335
                return $this->extensions;
1✔
336
        }
337

338

339
        /**
340
         * Registers run-time function.
341
         */
342
        public function addFunction(string $name, callable $callback): static
1✔
343
        {
344
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
345
                        throw new \LogicException("Invalid function name '$name'.");
×
346
                }
347

348
                $this->functions->add($name, $callback);
1✔
349
                return $this;
1✔
350
        }
351

352

353
        /**
354
         * Call a run-time function.
355
         * @param  mixed[]  $args
356
         */
357
        public function invokeFunction(string $name, array $args): mixed
1✔
358
        {
359
                return ($this->functions->$name)(null, ...$args);
1✔
360
        }
361

362

363
        /**
364
         * @return callable[]
365
         */
366
        public function getFunctions(): array
367
        {
368
                return $this->functions->getAll();
1✔
369
        }
370

371

372
        /**
373
         * Adds new provider.
374
         */
375
        public function addProvider(string $name, mixed $provider): static
1✔
376
        {
377
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
378
                        throw new \LogicException("Invalid provider name '$name'.");
×
379
                }
380

381
                $this->providers->$name = $provider;
1✔
382
                return $this;
1✔
383
        }
384

385

386
        /**
387
         * Returns all providers.
388
         * @return mixed[]
389
         */
390
        public function getProviders(): array
391
        {
392
                return (array) $this->providers;
1✔
393
        }
394

395

396
        public function setPolicy(?Policy $policy): static
1✔
397
        {
398
                $this->policy = $policy;
1✔
399
                return $this;
1✔
400
        }
401

402

403
        public function getPolicy(bool $effective = false): ?Policy
1✔
404
        {
405
                return !$effective || $this->sandboxed
1✔
406
                        ? $this->policy
1✔
407
                        : null;
1✔
408
        }
409

410

411
        public function setExceptionHandler(callable $handler): static
1✔
412
        {
413
                $this->providers->coreExceptionHandler = $handler;
1✔
414
                return $this;
1✔
415
        }
416

417

418
        public function setSandboxMode(bool $state = true): static
1✔
419
        {
420
                $this->sandboxed = $state;
1✔
421
                return $this;
1✔
422
        }
423

424

425
        public function setContentType(string $type): static
1✔
426
        {
427
                $this->contentType = $type;
1✔
428
                return $this;
1✔
429
        }
430

431

432
        /**
433
         * Sets path to temporary directory.
434
         */
435
        public function setTempDirectory(?string $path): static
1✔
436
        {
437
                $this->cache->directory = $path;
1✔
438
                return $this;
1✔
439
        }
440

441

442
        /**
443
         * Sets auto-refresh mode.
444
         */
445
        public function setAutoRefresh(bool $state = true): static
446
        {
447
                $this->cache->autoRefresh = $state;
×
448
                return $this;
×
449
        }
450

451

452
        /**
453
         * Enables declare(strict_types=1) in templates.
454
         */
455
        public function setStrictTypes(bool $state = true): static
1✔
456
        {
457
                $this->strictTypes = $state;
1✔
458
                return $this;
1✔
459
        }
460

461

462
        public function setStrictParsing(bool $state = true): static
1✔
463
        {
464
                $this->strictParsing = $state;
1✔
465
                return $this;
1✔
466
        }
467

468

469
        public function isStrictParsing(): bool
470
        {
471
                return $this->strictParsing;
1✔
472
        }
473

474

475
        /**
476
         * Sets the locale. It uses the same identifiers as the PHP intl extension.
477
         */
478
        public function setLocale(?string $locale): static
479
        {
480
                if ($locale && !extension_loaded('intl')) {
×
481
                        throw new RuntimeException("Setting a locale requires the 'intl' extension to be installed.");
×
482
                }
483
                $this->locale = $locale;
×
484
                return $this;
×
485
        }
486

487

488
        public function getLocale(): ?string
489
        {
490
                return $this->locale;
1✔
491
        }
492

493

494
        public function setLoader(Loader $loader): static
1✔
495
        {
496
                $this->loader = $loader;
1✔
497
                return $this;
1✔
498
        }
499

500

501
        public function getLoader(): Loader
502
        {
503
                return $this->loader ??= new Loaders\FileLoader;
1✔
504
        }
505

506

507
        public function enablePhpLinter(?string $phpBinary): static
508
        {
509
                $this->phpBinary = $phpBinary;
×
510
                return $this;
×
511
        }
512

513

514
        /**
515
         * @param  object|mixed[]  $params
516
         * @return mixed[]
517
         */
518
        private function processParams(object|array $params): array
1✔
519
        {
520
                if (is_array($params)) {
1✔
521
                        return $params;
1✔
522
                }
523

524
                $rc = new \ReflectionClass($params);
1✔
525
                $methods = $rc->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
526
                foreach ($methods as $method) {
1✔
527
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
528
                                $this->addFilter($method->name, [$params, $method->name]);
1✔
529
                        }
530

531
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
532
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
533
                        }
534

535
                        if (strpos((string) $method->getDocComment(), '@filter')) {
1✔
NEW
536
                                trigger_error('Annotation @filter is deprecated, use attribute #[Latte\Attributes\TemplateFilter]');
×
537
                                $this->addFilter($method->name, [$params, $method->name]);
×
538
                        }
539

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

© 2025 Coveralls, Inc