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

nette / latte / 9254369764

27 May 2024 11:56AM UTC coverage: 93.63% (+0.003%) from 93.627%
9254369764

push

github

dg
added support for locale, affects |date, |number, |bytes and |sort filters

47 of 52 new or added lines in 3 files covered. (90.38%)

37 existing lines in 2 files now uncovered.

5100 of 5447 relevant lines covered (93.63%)

0.94 hits per line

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

84.87
/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.16';
21
        public const VersionId = 30016;
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
        private ?string $locale = null;
54

55

56
        public function __construct()
57
        {
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->cacheKey = $clearCache ? null : $this->cacheKey;
1✔
97
                $class = $this->getTemplateClass($name);
1✔
98
                if (!class_exists($class, false)) {
1✔
99
                        $this->loadTemplate($name);
1✔
100
                }
101

102
                $this->providers->fn = $this->functions;
1✔
103
                return new $class(
1✔
104
                        $this,
1✔
105
                        $params,
106
                        $this->filters,
1✔
107
                        $this->providers,
1✔
108
                        $name,
109
                );
110
        }
111

112

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

122
                $template = $this->getLoader()->getContent($name);
1✔
123

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

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

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

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

141
                return $compiled;
1✔
142
        }
143

144

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

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

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

164

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

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

182

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

197

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

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

214

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

224
                        return;
1✔
225
                }
226

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

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

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

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

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

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

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

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

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

280

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

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

298
                return $handle;
1✔
299
        }
300

301

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

310

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

316

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

324

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

344

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

360

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

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

374

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

384

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

394

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

404

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

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

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

425

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

432

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

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

446

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

456

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

465

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

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

479

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

489

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

496

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

504

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

511

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

518

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

525

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

535

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

545

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

555

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

562

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

568

569
        /**
570
         * Sets locale for date and number formatting. See PHP intl extension.
571
         */
572
        public function setLocale(?string $locale): static
573
        {
NEW
574
                if ($locale && !extension_loaded('intl')) {
×
NEW
575
                        throw new RuntimeException("Locate requires the 'intl' extension to be installed.");
×
576
                }
NEW
577
                $this->locale = $locale;
×
NEW
578
                return $this;
×
579
        }
580

581

582
        public function getLocale(): ?string
583
        {
584
                return $this->locale;
1✔
585
        }
586

587

588
        public function setLoader(Loader $loader): static
1✔
589
        {
590
                $this->loader = $loader;
1✔
591
                return $this;
1✔
592
        }
593

594

595
        public function getLoader(): Loader
596
        {
597
                return $this->loader ??= new Loaders\FileLoader;
1✔
598
        }
599

600

601
        public function enablePhpLinter(?string $phpBinary): static
602
        {
UNCOV
603
                $this->phpBinary = $phpBinary;
×
UNCOV
604
                return $this;
×
605
        }
606

607

608
        /**
609
         * @param  object|mixed[]  $params
610
         * @return mixed[]
611
         */
612
        private function processParams(object|array $params): array
1✔
613
        {
614
                if (is_array($params)) {
1✔
615
                        return $params;
1✔
616
                }
617

618
                $methods = (new \ReflectionClass($params))->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
619
                foreach ($methods as $method) {
1✔
620
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
621
                                $this->addFilter($method->name, [$params, $method->name]);
1✔
622
                        }
623

624
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
625
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
626
                        }
627

628
                        if (strpos((string) $method->getDocComment(), '@filter')) {
1✔
UNCOV
629
                                trigger_error('Annotation @filter is deprecated, use attribute #[Latte\Attributes\TemplateFilter]', E_USER_DEPRECATED);
×
UNCOV
630
                                $this->addFilter($method->name, [$params, $method->name]);
×
631
                        }
632

633
                        if (strpos((string) $method->getDocComment(), '@function')) {
1✔
UNCOV
634
                                trigger_error('Annotation @function is deprecated, use attribute #[Latte\Attributes\TemplateFunction]', E_USER_DEPRECATED);
×
UNCOV
635
                                $this->addFunction($method->name, [$params, $method->name]);
×
636
                        }
637
                }
638

639
                return array_filter((array) $params, fn($key) => $key[0] !== "\0", ARRAY_FILTER_USE_KEY);
1✔
640
        }
641

642

643
        public function __get(string $name)
644
        {
UNCOV
645
                if ($name === 'onCompile') {
×
UNCOV
646
                        $trace = debug_backtrace(0)[0];
×
UNCOV
647
                        $loc = isset($trace['file'], $trace['line'])
×
UNCOV
648
                                ? ' (in ' . $trace['file'] . ' on ' . $trace['line'] . ')'
×
649
                                : '';
×
650
                        throw new \LogicException('You use Latte 3 together with the code designed for Latte 2' . $loc);
×
651
                }
652
        }
653
}
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