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

nette / latte / 15082309049

17 May 2025 05:59AM UTC coverage: 93.597% (-0.06%) from 93.653%
15082309049

push

github

dg
Released version 3.0.21

5116 of 5466 relevant lines covered (93.6%)

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

14

15
/**
16
 * Templating engine Latte.
17
 */
18
class Engine
19
{
20
        public const Version = '3.0.21';
21
        public const VersionId = 30021;
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 $environmentHash;
52
        private ?string $locale = null;
53

54

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

65

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

77

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

89

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

108

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

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

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

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

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

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

137
                return $compiled;
1✔
138
        }
139

140

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

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

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

160

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

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

178

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

193

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

204
                $this->loadTemplate($name);
1✔
205
        }
1✔
206

207

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

225

226
        public function getCacheFile(string $name): string
1✔
227
        {
228
                return $this->cache->generateFileName($name, $this->generateTemplateHash($name));
1✔
229
        }
230

231

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

237

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

245

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

264

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

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

278

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

288

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

298

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

308

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

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

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

329

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

336

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

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

350

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

360

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

369

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

379
                $this->providers->$name = $provider;
1✔
380
                return $this;
1✔
381
        }
382

383

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

393

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

400

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

408

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

415

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

422

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

429

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

439

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

449

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

459

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

466

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

472

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

485

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

491

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

498

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

504

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

511

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

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

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

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

538
                        if (strpos((string) $method->getDocComment(), '@function')) {
1✔
539
                                trigger_error('Annotation @function is deprecated, use attribute #[Latte\Attributes\TemplateFunction]', E_USER_DEPRECATED);
×
540
                                $this->addFunction($method->name, [$params, $method->name]);
×
541
                        }
542
                }
543

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

554
                return $res;
1✔
555
        }
556

557

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