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

nette / latte / 8336142063

19 Mar 2024 01:30AM UTC coverage: 94.097% (+0.03%) from 94.071%
8336142063

push

github

dg
hasBlock() fixed template retrieval [Closes #357]

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

37 existing lines in 8 files now uncovered.

5037 of 5353 relevant lines covered (94.1%)

0.94 hits per line

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

83.26
/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.13';
21
        public const VersionId = 30013;
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

53

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

63

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

75

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

87

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

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

109

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

119
                $source = $this->getLoader()->getContent($name);
1✔
120

121
                try {
122
                        $node = $this->parse($source);
1✔
123
                        $this->applyPasses($node);
1✔
124
                        $code = $this->generate($node, $name);
1✔
125

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

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

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

138
                return $code;
1✔
139
        }
140

141

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

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

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

161

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

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

179

180
        /**
181
         * Generates template PHP code.
182
         */
183
        public function generate(TemplateNode $node, string $name): string
1✔
184
        {
185
                $sourceName = preg_match('#\n|\?#', $name) ? null : $name;
1✔
186
                $generator = new Compiler\TemplateGenerator;
1✔
187
                return $generator->generate(
1✔
188
                        $node,
1✔
189
                        $this->getTemplateClass($name),
1✔
190
                        $sourceName,
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->tempDirectory) {
1✔
203
                        throw new \LogicException('Path to temporary directory is not set.');
×
204
                }
205

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

212

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

222
                        return;
1✔
223
                }
224

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

234
                if (!$this->isExpired($file, $name) && (@include $file) !== false) { // @ - file may not exist
1✔
235
                        return;
×
236
                }
237

238
                if ($lock) {
1✔
239
                        flock($lock, LOCK_UN); // release shared lock so we can get exclusive
×
240
                }
241

242
                $lock = $this->acquireLock("$file.lock", LOCK_EX);
1✔
243

244
                // while waiting for exclusive lock, someone might have already created the cache
245
                if (!is_file($file) || $this->isExpired($file, $name)) {
1✔
246
                        $code = $this->compile($name);
1✔
247
                        if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
1✔
248
                                @unlink("$file.tmp"); // @ - file may not exist
×
249
                                throw new RuntimeException("Unable to create '$file'.");
×
250
                        }
251

252
                        if (function_exists('opcache_invalidate')) {
1✔
253
                                @opcache_invalidate($file, true); // @ can be restricted
1✔
254
                        }
255
                }
256

257
                if ((include $file) === false) {
1✔
258
                        throw new RuntimeException("Unable to load '$file'.");
×
259
                }
260

261
                flock($lock, LOCK_UN);
1✔
262
        }
1✔
263

264

265
        /**
266
         * @return resource
267
         */
268
        private function acquireLock(string $file, int $mode)
1✔
269
        {
270
                $dir = dirname($file);
1✔
271
                if (!is_dir($dir) && !@mkdir($dir) && !is_dir($dir)) { // @ - dir may already exist
1✔
272
                        throw new RuntimeException("Unable to create directory '$dir'. " . error_get_last()['message']);
×
273
                }
274

275
                $handle = @fopen($file, 'w'); // @ is escalated to exception
1✔
276
                if (!$handle) {
1✔
277
                        throw new RuntimeException("Unable to create file '$file'. " . error_get_last()['message']);
×
278
                } elseif (!@flock($handle, $mode)) { // @ is escalated to exception
1✔
279
                        throw new RuntimeException('Unable to acquire ' . ($mode & LOCK_EX ? 'exclusive' : 'shared') . " lock on file '$file'. " . error_get_last()['message']);
×
280
                }
281

282
                return $handle;
1✔
283
        }
284

285

286
        private function isExpired(string $file, string $name): bool
1✔
287
        {
288
                if (!$this->autoRefresh) {
1✔
289
                        return false;
×
290
                }
291

292
                $time = @filemtime($file); // @ - file may not exist
1✔
293
                if ($time === false) {
1✔
294
                        return true;
1✔
295
                }
296

297
                foreach ($this->extensions as $extension) {
×
298
                        $r = new \ReflectionObject($extension);
×
299
                        if (is_file($r->getFileName()) && filemtime($r->getFileName()) > $time) {
×
300
                                return true;
×
301
                        }
302
                }
303

304
                return $this->getLoader()->isExpired($name, $time);
×
305
        }
306

307

308
        public function getCacheFile(string $name): string
1✔
309
        {
310
                $hash = substr($this->getTemplateClass($name), 8);
1✔
311
                $base = preg_match('#([/\\\\][\w@.-]{3,35}){1,3}$#D', $name, $m)
1✔
312
                        ? preg_replace('#[^\w@.-]+#', '-', substr($m[0], 1)) . '--'
1✔
313
                        : '';
1✔
314
                return "$this->tempDirectory/$base$hash.php";
1✔
315
        }
316

317

318
        public function getTemplateClass(string $name): string
1✔
319
        {
320
                $key = [
1✔
321
                        $this->getLoader()->getUniqueId($name),
1✔
322
                        self::Version,
323
                        array_keys($this->getFunctions()),
1✔
324
                        $this->contentType,
1✔
325
                ];
326
                foreach ($this->extensions as $extension) {
1✔
327
                        $key[] = [
1✔
328
                                get_debug_type($extension),
1✔
329
                                $extension->getCacheKey($this),
1✔
330
                        ];
331
                }
332

333
                return 'Template' . substr(md5(serialize($key)), 0, 10);
1✔
334
        }
335

336

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

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

350

351
        /**
352
         * Registers filter loader.
353
         */
354
        public function addFilterLoader(callable $callback): static
1✔
355
        {
356
                $this->filters->add(null, $callback);
1✔
357
                return $this;
1✔
358
        }
359

360

361
        /**
362
         * Returns all run-time filters.
363
         * @return callable[]
364
         */
365
        public function getFilters(): array
366
        {
367
                return $this->filters->getAll();
1✔
368
        }
369

370

371
        /**
372
         * Call a run-time filter.
373
         * @param  mixed[]  $args
374
         */
375
        public function invokeFilter(string $name, array $args): mixed
1✔
376
        {
377
                return ($this->filters->$name)(...$args);
1✔
378
        }
379

380

381
        /**
382
         * Adds new extension.
383
         */
384
        public function addExtension(Extension $extension): static
1✔
385
        {
386
                $this->extensions[] = $extension;
1✔
387
                foreach ($extension->getFilters() as $name => $value) {
1✔
388
                        $this->filters->add($name, $value);
1✔
389
                }
390

391
                foreach ($extension->getFunctions() as $name => $value) {
1✔
392
                        $this->functions->add($name, $value);
1✔
393
                }
394

395
                foreach ($extension->getProviders() as $name => $value) {
1✔
396
                        $this->providers->$name = $value;
×
397
                }
398
                return $this;
1✔
399
        }
400

401

402
        /** @return Extension[] */
403
        public function getExtensions(): array
404
        {
405
                return $this->extensions;
1✔
406
        }
407

408

409
        /**
410
         * Registers run-time function.
411
         */
412
        public function addFunction(string $name, callable $callback): static
1✔
413
        {
414
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
415
                        throw new \LogicException("Invalid function name '$name'.");
×
416
                }
417

418
                $this->functions->add($name, $callback);
1✔
419
                return $this;
1✔
420
        }
421

422

423
        /**
424
         * Call a run-time function.
425
         * @param  mixed[]  $args
426
         */
427
        public function invokeFunction(string $name, array $args): mixed
1✔
428
        {
429
                return ($this->functions->$name)(null, ...$args);
1✔
430
        }
431

432

433
        /**
434
         * @return callable[]
435
         */
436
        public function getFunctions(): array
437
        {
438
                return $this->functions->getAll();
1✔
439
        }
440

441

442
        /**
443
         * Adds new provider.
444
         */
445
        public function addProvider(string $name, mixed $value): static
1✔
446
        {
447
                if (!preg_match('#^[a-z]\w*$#iD', $name)) {
1✔
UNCOV
448
                        throw new \LogicException("Invalid provider name '$name'.");
×
449
                }
450

451
                $this->providers->$name = $value;
1✔
452
                return $this;
1✔
453
        }
454

455

456
        /**
457
         * Returns all providers.
458
         * @return mixed[]
459
         */
460
        public function getProviders(): array
461
        {
462
                return (array) $this->providers;
1✔
463
        }
464

465

466
        public function setPolicy(?Policy $policy): static
1✔
467
        {
468
                $this->policy = $policy;
1✔
469
                return $this;
1✔
470
        }
471

472

473
        public function getPolicy(bool $effective = false): ?Policy
1✔
474
        {
475
                return !$effective || $this->sandboxed
1✔
476
                        ? $this->policy
1✔
477
                        : null;
1✔
478
        }
479

480

481
        public function setExceptionHandler(callable $callback): static
1✔
482
        {
483
                $this->providers->coreExceptionHandler = $callback;
1✔
484
                return $this;
1✔
485
        }
486

487

488
        public function setSandboxMode(bool $on = true): static
1✔
489
        {
490
                $this->sandboxed = $on;
1✔
491
                return $this;
1✔
492
        }
493

494

495
        public function setContentType(string $type): static
1✔
496
        {
497
                $this->contentType = $type;
1✔
498
                return $this;
1✔
499
        }
500

501

502
        /**
503
         * Sets path to temporary directory.
504
         */
505
        public function setTempDirectory(?string $path): static
1✔
506
        {
507
                $this->tempDirectory = $path;
1✔
508
                return $this;
1✔
509
        }
510

511

512
        /**
513
         * Sets auto-refresh mode.
514
         */
515
        public function setAutoRefresh(bool $on = true): static
516
        {
UNCOV
517
                $this->autoRefresh = $on;
×
UNCOV
518
                return $this;
×
519
        }
520

521

522
        /**
523
         * Enables declare(strict_types=1) in templates.
524
         */
525
        public function setStrictTypes(bool $on = true): static
1✔
526
        {
527
                $this->strictTypes = $on;
1✔
528
                return $this;
1✔
529
        }
530

531

532
        public function setStrictParsing(bool $on = true): static
1✔
533
        {
534
                $this->strictParsing = $on;
1✔
535
                return $this;
1✔
536
        }
537

538

539
        public function isStrictParsing(): bool
540
        {
541
                return $this->strictParsing;
1✔
542
        }
543

544

545
        public function setLoader(Loader $loader): static
1✔
546
        {
547
                $this->loader = $loader;
1✔
548
                return $this;
1✔
549
        }
550

551

552
        public function getLoader(): Loader
553
        {
554
                if (!$this->loader) {
1✔
555
                        $this->loader = new Loaders\FileLoader;
1✔
556
                }
557

558
                return $this->loader;
1✔
559
        }
560

561

562
        public function enablePhpLinter(?string $phpBinary): static
563
        {
UNCOV
564
                $this->phpBinary = $phpBinary;
×
UNCOV
565
                return $this;
×
566
        }
567

568

569
        /**
570
         * @param  object|mixed[]  $params
571
         * @return mixed[]
572
         */
573
        private function processParams(object|array $params): array
1✔
574
        {
575
                if (is_array($params)) {
1✔
576
                        return $params;
1✔
577
                }
578

579
                $methods = (new \ReflectionClass($params))->getMethods(\ReflectionMethod::IS_PUBLIC);
1✔
580
                foreach ($methods as $method) {
1✔
581
                        if ($method->getAttributes(Attributes\TemplateFilter::class)) {
1✔
582
                                $this->addFilter($method->name, [$params, $method->name]);
1✔
583
                        }
584

585
                        if ($method->getAttributes(Attributes\TemplateFunction::class)) {
1✔
586
                                $this->addFunction($method->name, [$params, $method->name]);
1✔
587
                        }
588

589
                        if (strpos((string) $method->getDocComment(), '@filter')) {
1✔
UNCOV
590
                                trigger_error('Annotation @filter is deprecated, use attribute #[Latte\Attributes\TemplateFilter]', E_USER_DEPRECATED);
×
UNCOV
591
                                $this->addFilter($method->name, [$params, $method->name]);
×
592
                        }
593

594
                        if (strpos((string) $method->getDocComment(), '@function')) {
1✔
UNCOV
595
                                trigger_error('Annotation @function is deprecated, use attribute #[Latte\Attributes\TemplateFunction]', E_USER_DEPRECATED);
×
UNCOV
596
                                $this->addFunction($method->name, [$params, $method->name]);
×
597
                        }
598
                }
599

600
                return array_filter((array) $params, fn($key) => $key[0] !== "\0", ARRAY_FILTER_USE_KEY);
1✔
601
        }
602

603

604
        public function __get(string $name)
605
        {
UNCOV
606
                if ($name === 'onCompile') {
×
UNCOV
607
                        $trace = debug_backtrace(0)[0];
×
UNCOV
608
                        $loc = isset($trace['file'], $trace['line'])
×
UNCOV
609
                                ? ' (in ' . $trace['file'] . ' on ' . $trace['line'] . ')'
×
UNCOV
610
                                : '';
×
UNCOV
611
                        throw new \LogicException('You use Latte 3 together with the code designed for Latte 2' . $loc);
×
612
                }
613
        }
614
}
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