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

nette / latte / 8775682276

21 Apr 2024 09:37PM UTC coverage: 93.637% (+0.1%) from 93.539%
8775682276

push

github

dg
Engine: added underscore to template class

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

30 existing lines in 2 files now uncovered.

5018 of 5359 relevant lines covered (93.64%)

0.94 hits per line

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

86.27
/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.14';
21
        public const VersionId = 30014;
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 ?string $tempDirectory = null;
46
        private bool $autoRefresh = true;
47
        private bool $strictTypes = false;
48
        private bool $strictParsing = false;
49
        private ?Policy $policy = null;
50
        private bool $sandboxed = false;
51
        private ?string $phpBinary = null;
52
        private ?string $cacheKey;
53

54

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

64

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

76

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

88

89
        /**
90
         * Creates template object.
91
         * @param  mixed[]  $params
92
         */
93
        public function createTemplate(string $name, array $params = [], $clearCache = true): Runtime\Template
1✔
94
        {
95
                $this->cacheKey = $clearCache ? null : $this->cacheKey;
1✔
96
                $class = $this->getTemplateClass($name);
1✔
97
                if (!class_exists($class, false)) {
1✔
98
                        $this->loadTemplate($name);
1✔
99
                }
100

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✔
UNCOV
130
                                $e = new CompileException("Thrown exception '{$e->getMessage()}'", previous: $e);
×
131
                        }
132

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

136
                if ($this->phpBinary) {
1✔
UNCOV
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->strict = $this->strictParsing;
1✔
151

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

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

163

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

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

181

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

196

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

207
                $class = $this->getTemplateClass($name);
1✔
208
                if (!class_exists($class, false)) {
1✔
209
                        $this->loadTemplate($name);
1✔
210
                }
211
        }
1✔
212

213

214
        private function loadTemplate(string $name): void
1✔
215
        {
216
                if (!$this->tempDirectory) {
1✔
217
                        $compiled = $this->compile($name);
1✔
218
                        if (@eval(substr($compiled, 5)) === false) { // @ is escalated to exception, substr removes <?php
1✔
219
                                throw (new CompileException('Error in template: ' . error_get_last()['message']))
×
UNCOV
220
                                        ->setSource($compiled, "$name (compiled)");
×
221
                        }
222

223
                        return;
1✔
224
                }
225

226
                // Solving atomicity to work everywhere is really pain in the ass.
227
                // 1) We want to do as little as possible IO calls on production and also directory and file can be not writable
228
                // so on Linux we include the file directly without shared lock, therefore, the file must be created atomically by renaming.
229
                // 2) On Windows file cannot be renamed-to while is open (ie by include), so we have to acquire a lock.
230
                $cacheFile = $this->getCacheFile($name);
1✔
231
                $cacheKey = $this->autoRefresh
1✔
232
                        ? md5(serialize($this->getCacheSignature($name)))
1✔
UNCOV
233
                        : null;
×
234
                $lock = defined('PHP_WINDOWS_VERSION_BUILD') || $this->autoRefresh
1✔
235
                        ? $this->acquireLock("$cacheFile.lock", LOCK_SH)
1✔
UNCOV
236
                        : null;
×
237

238
                if (
239
                        !($this->autoRefresh && $cacheKey !== stream_get_contents($lock))
1✔
240
                        && (@include $cacheFile) !== false // @ - file may not exist
1✔
241
                ) {
UNCOV
242
                        return;
×
243
                }
244

245
                if ($lock) {
1✔
246
                        flock($lock, LOCK_UN); // release shared lock so we can get exclusive
1✔
247
                        fseek($lock, 0);
1✔
248
                }
249

250
                $lock = $this->acquireLock("$cacheFile.lock", LOCK_EX);
1✔
251

252
                // while waiting for exclusive lock, someone might have already created the cache
253
                if (!is_file($cacheFile) || ($this->autoRefresh && $cacheKey !== stream_get_contents($lock))) {
1✔
254
                        $compiled = $this->compile($name);
1✔
255
                        if (
256
                                file_put_contents("$cacheFile.tmp", $compiled) !== strlen($compiled)
1✔
257
                                || !rename("$cacheFile.tmp", $cacheFile)
1✔
258
                        ) {
UNCOV
259
                                @unlink("$cacheFile.tmp"); // @ - file may not exist
×
UNCOV
260
                                throw new RuntimeException("Unable to create '$cacheFile'.");
×
261
                        }
262

263
                        fseek($lock, 0);
1✔
264
                        fwrite($lock, $cacheKey ?? md5(serialize($this->getCacheSignature($name))));
1✔
265
                        ftruncate($lock, ftell($lock));
1✔
266

267
                        if (function_exists('opcache_invalidate')) {
1✔
268
                                @opcache_invalidate($cacheFile, true); // @ can be restricted
1✔
269
                        }
270
                }
271

272
                if ((include $cacheFile) === false) {
1✔
UNCOV
273
                        throw new RuntimeException("Unable to load '$cacheFile'.");
×
274
                }
275

276
                flock($lock, LOCK_UN);
1✔
277
        }
1✔
278

279

280
        /**
281
         * @return resource
282
         */
283
        private function acquireLock(string $file, int $mode)
1✔
284
        {
285
                $dir = dirname($file);
1✔
286
                if (!is_dir($dir) && !@mkdir($dir) && !is_dir($dir)) { // @ - dir may already exist
1✔
UNCOV
287
                        throw new RuntimeException("Unable to create directory '$dir'. " . error_get_last()['message']);
×
288
                }
289

290
                $handle = @fopen($file, 'c+'); // @ is escalated to exception
1✔
291
                if (!$handle) {
1✔
UNCOV
292
                        throw new RuntimeException("Unable to create file '$file'. " . error_get_last()['message']);
×
293
                } elseif (!@flock($handle, $mode)) { // @ is escalated to exception
1✔
UNCOV
294
                        throw new RuntimeException('Unable to acquire ' . ($mode & LOCK_EX ? 'exclusive' : 'shared') . " lock on file '$file'. " . error_get_last()['message']);
×
295
                }
296

297
                return $handle;
1✔
298
        }
299

300

301
        public function getCacheFile(string $name): string
1✔
302
        {
303
                $base = preg_match('#([/\\\\][\w@.-]{3,35}){1,3}$#D', $name, $m)
1✔
304
                        ? preg_replace('#[^\w@.-]+#', '-', substr($m[0], 1)) . '--'
1✔
305
                        : '';
1✔
306
                return $this->tempDirectory . '/' . $base . $this->generateCacheHash($name) . '.php';
1✔
307
        }
308

309

310
        public function getTemplateClass(string $name): string
1✔
311
        {
312
                return 'Template_' . $this->generateCacheHash($name);
1✔
313
        }
314

315

316
        private function generateCacheHash(string $name): string
1✔
317
        {
318
                $this->cacheKey ??= md5(serialize($this->getCacheKey()));
1✔
319
                $hash = $this->cacheKey . $this->getLoader()->getUniqueId($name);
1✔
320
                return substr(md5($hash), 0, 10);
1✔
321
        }
322

323

324
        /**
325
         * Values that affect the compiled template and its file name.
326
         */
327
        protected function getCacheKey(): array
328
        {
329
                return [
330
                        $this->contentType,
1✔
331
                        array_keys($this->getFunctions()),
1✔
332
                        array_map(
1✔
333
                                fn($extension) => [
1✔
334
                                        get_debug_type($extension),
1✔
335
                                        $extension->getCacheKey($this),
1✔
336
                                        filemtime((new \ReflectionObject($extension))->getFileName()),
1✔
337
                                ],
1✔
338
                                $this->extensions,
1✔
339
                        ),
340
                ];
341
        }
342

343

344
        /**
345
         * Values that check the expiration of the compiled template.
346
         */
347
        protected function getCacheSignature(string $name): array
1✔
348
        {
349
                return [
350
                        self::Version,
1✔
351
                        $this->getLoader()->getContent($name),
1✔
352
                        array_map(
1✔
353
                                fn($extension) => filemtime((new \ReflectionObject($extension))->getFileName()),
1✔
354
                                $this->extensions,
1✔
355
                        ),
356
                ];
357
        }
358

359

360
        /**
361
         * Registers run-time filter.
362
         */
363
        public function addFilter(string $name, callable $callback): static
1✔
364
        {
365
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
UNCOV
366
                        throw new \LogicException("Invalid filter name '$name'.");
×
367
                }
368

369
                $this->filters->add($name, $callback);
1✔
370
                return $this;
1✔
371
        }
372

373

374
        /**
375
         * Registers filter loader.
376
         */
377
        public function addFilterLoader(callable $loader): static
1✔
378
        {
379
                $this->filters->add(null, $loader);
1✔
380
                return $this;
1✔
381
        }
382

383

384
        /**
385
         * Returns all run-time filters.
386
         * @return callable[]
387
         */
388
        public function getFilters(): array
389
        {
390
                return $this->filters->getAll();
1✔
391
        }
392

393

394
        /**
395
         * Call a run-time filter.
396
         * @param  mixed[]  $args
397
         */
398
        public function invokeFilter(string $name, array $args): mixed
1✔
399
        {
400
                return ($this->filters->$name)(...$args);
1✔
401
        }
402

403

404
        /**
405
         * Adds new extension.
406
         */
407
        public function addExtension(Extension $extension): static
1✔
408
        {
409
                $this->extensions[] = $extension;
1✔
410
                foreach ($extension->getFilters() as $name => $value) {
1✔
411
                        $this->filters->add($name, $value);
1✔
412
                }
413

414
                foreach ($extension->getFunctions() as $name => $value) {
1✔
415
                        $this->functions->add($name, $value);
1✔
416
                }
417

418
                foreach ($extension->getProviders() as $name => $value) {
1✔
UNCOV
419
                        $this->providers->$name = $value;
×
420
                }
421
                return $this;
1✔
422
        }
423

424

425
        /** @return Extension[] */
426
        public function getExtensions(): array
427
        {
428
                return $this->extensions;
1✔
429
        }
430

431

432
        /**
433
         * Registers run-time function.
434
         */
435
        public function addFunction(string $name, callable $callback): static
1✔
436
        {
437
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
UNCOV
438
                        throw new \LogicException("Invalid function name '$name'.");
×
439
                }
440

441
                $this->functions->add($name, $callback);
1✔
442
                return $this;
1✔
443
        }
444

445

446
        /**
447
         * Call a run-time function.
448
         * @param  mixed[]  $args
449
         */
450
        public function invokeFunction(string $name, array $args): mixed
1✔
451
        {
452
                return ($this->functions->$name)(null, ...$args);
1✔
453
        }
454

455

456
        /**
457
         * @return callable[]
458
         */
459
        public function getFunctions(): array
460
        {
461
                return $this->functions->getAll();
1✔
462
        }
463

464

465
        /**
466
         * Adds new provider.
467
         */
468
        public function addProvider(string $name, mixed $provider): static
1✔
469
        {
470
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
UNCOV
471
                        throw new \LogicException("Invalid provider name '$name'.");
×
472
                }
473

474
                $this->providers->$name = $provider;
1✔
475
                return $this;
1✔
476
        }
477

478

479
        /**
480
         * Returns all providers.
481
         * @return mixed[]
482
         */
483
        public function getProviders(): array
484
        {
485
                return (array) $this->providers;
1✔
486
        }
487

488

489
        public function setPolicy(?Policy $policy): static
1✔
490
        {
491
                $this->policy = $policy;
1✔
492
                return $this;
1✔
493
        }
494

495

496
        public function getPolicy(bool $effective = false): ?Policy
1✔
497
        {
498
                return !$effective || $this->sandboxed
1✔
499
                        ? $this->policy
1✔
500
                        : null;
1✔
501
        }
502

503

504
        public function setExceptionHandler(callable $handler): static
1✔
505
        {
506
                $this->providers->coreExceptionHandler = $handler;
1✔
507
                return $this;
1✔
508
        }
509

510

511
        public function setSandboxMode(bool $state = true): static
1✔
512
        {
513
                $this->sandboxed = $state;
1✔
514
                return $this;
1✔
515
        }
516

517

518
        public function setContentType(string $type): static
1✔
519
        {
520
                $this->contentType = $type;
1✔
521
                return $this;
1✔
522
        }
523

524

525
        /**
526
         * Sets path to temporary directory.
527
         */
528
        public function setTempDirectory(?string $path): static
1✔
529
        {
530
                $this->tempDirectory = $path;
1✔
531
                return $this;
1✔
532
        }
533

534

535
        /**
536
         * Sets auto-refresh mode.
537
         */
538
        public function setAutoRefresh(bool $state = true): static
539
        {
UNCOV
540
                $this->autoRefresh = $state;
×
UNCOV
541
                return $this;
×
542
        }
543

544

545
        /**
546
         * Enables declare(strict_types=1) in templates.
547
         */
548
        public function setStrictTypes(bool $state = true): static
1✔
549
        {
550
                $this->strictTypes = $state;
1✔
551
                return $this;
1✔
552
        }
553

554

555
        public function setStrictParsing(bool $state = true): static
1✔
556
        {
557
                $this->strictParsing = $state;
1✔
558
                return $this;
1✔
559
        }
560

561

562
        public function isStrictParsing(): bool
563
        {
564
                return $this->strictParsing;
1✔
565
        }
566

567

568
        public function setLoader(Loader $loader): static
1✔
569
        {
570
                $this->loader = $loader;
1✔
571
                return $this;
1✔
572
        }
573

574

575
        public function getLoader(): Loader
576
        {
577
                return $this->loader ??= new Loaders\FileLoader;
1✔
578
        }
579

580

581
        public function enablePhpLinter(?string $phpBinary): static
582
        {
UNCOV
583
                $this->phpBinary = $phpBinary;
×
UNCOV
584
                return $this;
×
585
        }
586

587

588
        /**
589
         * @param  object|mixed[]  $params
590
         * @return mixed[]
591
         */
592
        private function processParams(object|array $params): array
1✔
593
        {
594
                if (is_array($params)) {
1✔
595
                        return $params;
1✔
596
                }
597

598
                $methods = (new \ReflectionClass($params))->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
599
                foreach ($methods as $method) {
1✔
600
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
601
                                $this->addFilter($method->name, [$params, $method->name]);
1✔
602
                        }
603

604
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
605
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
606
                        }
607

608
                        if (strpos((string) $method->getDocComment(), '@filter')) {
1✔
609
                                trigger_error('Annotation @filter is deprecated, use attribute #[Latte\Attributes\TemplateFilter]', E_USER_DEPRECATED);
×
610
                                $this->addFilter($method->name, [$params, $method->name]);
×
611
                        }
612

613
                        if (strpos((string) $method->getDocComment(), '@function')) {
1✔
UNCOV
614
                                trigger_error('Annotation @function is deprecated, use attribute #[Latte\Attributes\TemplateFunction]', E_USER_DEPRECATED);
×
UNCOV
615
                                $this->addFunction($method->name, [$params, $method->name]);
×
616
                        }
617
                }
618

619
                return array_filter((array) $params, fn($key) => $key[0] !== "\0", ARRAY_FILTER_USE_KEY);
1✔
620
        }
621

622

623
        public function __get(string $name)
624
        {
UNCOV
625
                if ($name === 'onCompile') {
×
UNCOV
626
                        $trace = debug_backtrace(0)[0];
×
UNCOV
627
                        $loc = isset($trace['file'], $trace['line'])
×
UNCOV
628
                                ? ' (in ' . $trace['file'] . ' on ' . $trace['line'] . ')'
×
UNCOV
629
                                : '';
×
UNCOV
630
                        throw new \LogicException('You use Latte 3 together with the code designed for Latte 2' . $loc);
×
631
                }
632
        }
633
}
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