Passed
Push — master ( 20f84a...66bc9c )
by Alexander
02:30
created

ViewTrait::withTheme()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\View;
6
7
use InvalidArgumentException;
8
use Psr\EventDispatcher\EventDispatcherInterface;
9
use Psr\EventDispatcher\StoppableEventInterface;
10
use RuntimeException;
11
use Throwable;
12
use Yiisoft\View\Event\AfterRenderEventInterface;
13
use Yiisoft\View\Exception\ViewNotFoundException;
14
15
use function array_key_exists;
16
use function array_merge;
17
use function array_pop;
18
use function basename;
19
use function crc32;
20
use function dechex;
21
use function dirname;
22
use function end;
23
use function func_get_args;
24
use function is_array;
25
use function is_file;
26
use function pathinfo;
27
use function substr;
28
29
/**
30
 * ViewTrait could be used as a base implementation of {@see ViewInterface}.
31
 *
32
 * @internal
33
 */
34
trait ViewTrait
35
{
36
    private EventDispatcherInterface $eventDispatcher;
37
38
    private string $basePath;
39
    private ?Theme $theme = null;
40
    private ?ViewContextInterface $context = null;
41
    private string $placeholderSignature;
42
    private string $language = 'en';
43
    private string $sourceLanguage = 'en';
44
    private string $defaultExtension = 'php';
45
46
    /**
47
     * @var array A list of available renderers indexed by their corresponding
48
     * supported file extensions.
49
     * @psalm-var array<string, TemplateRendererInterface>
50
     */
51
    private array $renderers = [];
52
53
    /**
54
     * @var array Parameters that are common for all view templates.
55
     * @psalm-var array<string, mixed>
56
     */
57
    private array $parameters = [];
58
59
    /**
60
     * @var array Named content blocks that are common for all view templates.
61
     * @psalm-var array<string, string>
62
     */
63
    private array $blocks = [];
64
65
    /**
66
     * @var array The view files currently being rendered. There may be multiple view files being
67
     * rendered at a moment because one view may be rendered within another.
68
     *
69
     * @psalm-var array<array-key, array<string, string>>
70
     */
71
    private array $viewFiles = [];
72
73
    /**
74
     * @param string $basePath The full path to the base directory of views.
75
     * @param EventDispatcherInterface $eventDispatcher The event dispatcher instance.
76
     */
77 111
    public function __construct(string $basePath, EventDispatcherInterface $eventDispatcher)
78
    {
79 111
        $this->basePath = $basePath;
80 111
        $this->eventDispatcher = $eventDispatcher;
81 111
        $this->setPlaceholderSalt(__DIR__);
82 111
    }
83
84
    /**
85
     * Returns a new instance with the specified theme instance.
86
     *
87
     * @param Theme $theme The theme instance.
88
     *
89
     * @return static
90
     */
91 2
    public function withTheme(Theme $theme): self
92
    {
93 2
        $new = clone $this;
94 2
        $new->theme = $theme;
95 2
        return $new;
96
    }
97
98
    /**
99
     * Returns a new instance with the specified renderers.
100
     *
101
     * @param array $renderers A list of available renderers indexed by their
102
     * corresponding supported file extensions.
103
     *
104
     * ```php
105
     * $view = $view->withRenderers(['twig' => new \Yiisoft\Yii\Twig\ViewRenderer($environment)]);
106
     * ```
107
     *
108
     * If no renderer is available for the given view file, the view file will be treated as a normal PHP
109
     * and rendered via {@see PhpTemplateRenderer}.
110
     *
111
     * @psalm-param array<string, TemplateRendererInterface> $renderers
112
     *
113
     * @return static
114
     */
115 1
    public function withRenderers(array $renderers): self
116
    {
117 1
        $new = clone $this;
118 1
        $new->renderers = $renderers;
119 1
        return $new;
120
    }
121
122
    /**
123
     * Returns a new instance with the specified language.
124
     *
125
     * @param string $language The language.
126
     *
127
     * @return static
128
     */
129 1
    public function withLanguage(string $language): self
130
    {
131 1
        $new = clone $this;
132 1
        $new->language = $language;
133 1
        return $new;
134
    }
135
136
    /**
137
     * Returns a new instance with the specified source language.
138
     *
139
     * @param string $language The source language.
140
     *
141
     * @return static
142
     */
143 1
    public function withSourceLanguage(string $language): self
144
    {
145 1
        $new = clone $this;
146 1
        $new->sourceLanguage = $language;
147 1
        return $new;
148
    }
149
150
    /**
151
     * Returns a new instance with the specified default view file extension.
152
     *
153
     * @param string $defaultExtension The default view file extension. Default is "php".
154
     * This will be appended to view file names if they don't have file extensions.
155
     *
156
     * @return static
157
     */
158 2
    public function withDefaultExtension(string $defaultExtension): self
159
    {
160 2
        $new = clone $this;
161 2
        $new->defaultExtension = $defaultExtension;
162 2
        return $new;
163
    }
164
165
    /**
166
     * Returns a new instance with the specified view context instance.
167
     *
168
     * @param ViewContextInterface $context The context under which the {@see renderFile()} method is being invoked.
169
     *
170
     * @return static
171
     */
172 5
    public function withContext(ViewContextInterface $context): self
173
    {
174 5
        $new = clone $this;
175 5
        $new->context = $context;
176 5
        $new->viewFiles = [];
177 5
        return $new;
178
    }
179
180
    /**
181
     * Returns a new instance with the specified view context path.
182
     *
183
     * @param string $path The context path under which the {@see renderFile()} method is being invoked.
184
     *
185
     * @return static
186
     */
187 2
    public function withContextPath(string $path): self
188
    {
189 2
        return $this->withContext(new ViewContext($path));
190
    }
191
192
    /**
193
     * Gets the base path to the view directory.
194
     *
195
     * @return string The base view path.
196
     */
197 1
    public function getBasePath(): string
198
    {
199 1
        return $this->basePath;
200
    }
201
202
    /**
203
     * Gets the default view file extension.
204
     *
205
     * @return string The default view file extension.
206
     */
207 1
    public function getDefaultExtension(): string
208
    {
209 1
        return $this->defaultExtension;
210
    }
211
212
    /**
213
     * Gets the theme instance, or null if no theme has been set.
214
     *
215
     * @return Theme The theme instance, or null if no theme has been set.
216
     */
217 2
    public function getTheme(): ?Theme
218
    {
219 2
        return $this->theme;
220
    }
221
222
    /**
223
     * Sets a common parameters that is accessible in all view templates.
224
     *
225
     * @param array $parameters Parameters that are common for all view templates.
226
     *
227
     * @psalm-param array<string, mixed> $parameters
228
     *
229
     * @return static
230
     *
231
     * @see setParameter()
232
     */
233 3
    public function setParameters(array $parameters): self
234
    {
235
        /** @var mixed $value */
236 3
        foreach ($parameters as $id => $value) {
237 1
            $this->setParameter($id, $value);
238
        }
239 3
        return $this;
240
    }
241
242
    /**
243
     * Sets a common parameter that is accessible in all view templates.
244
     *
245
     * @param string $id The unique identifier of the parameter.
246
     * @param mixed $value The value of the parameter.
247
     *
248
     * @return static
249
     */
250 9
    public function setParameter(string $id, $value): self
251
    {
252 9
        $this->parameters[$id] = $value;
253 9
        return $this;
254
    }
255
256
    /**
257
     * Add values to end of common array parameter. If specified parameter does not exist or him is not array,
258
     * then parameter will be added as empty array.
259
     *
260
     * @param string $id The unique identifier of the parameter.
261
     * @param mixed ...$value Value(s) for add to end of array parameter.
262
     *
263
     * @throws InvalidArgumentException When specified parameter already exists and is not an array.
264
     *
265
     * @return static
266
     */
267 5
    public function addToParameter(string $id, ...$value): self
268
    {
269
        /** @var mixed $array */
270 5
        $array = $this->parameters[$id] ?? [];
271 5
        if (!is_array($array)) {
272 1
            throw new InvalidArgumentException(
273 1
                sprintf('The "%s" parameter already exists and is not an array.', $id)
274
            );
275
        }
276
277 4
        $this->setParameter($id, array_merge($array, $value));
278
279 4
        return $this;
280
    }
281
282
    /**
283
     * Removes a common parameter.
284
     *
285
     * @param string $id The unique identifier of the parameter.
286
     */
287 1
    public function removeParameter(string $id): void
288
    {
289 1
        unset($this->parameters[$id]);
290 1
    }
291
292
    /**
293
     * Gets a common parameter value by ID.
294
     *
295
     * @param string $id The unique identifier of the parameter.
296
     * @param mixed $default The default value to be returned if the specified parameter does not exist.
297
     *
298
     * @throws InvalidArgumentException If specified parameter does not exist and not passed default value.
299
     *
300
     * @return mixed The value of the parameter.
301
     */
302 6
    public function getParameter(string $id)
303
    {
304 6
        if (isset($this->parameters[$id])) {
305 4
            return $this->parameters[$id];
306
        }
307
308 3
        $args = func_get_args();
309 3
        if (array_key_exists(1, $args)) {
310 1
            return $args[1];
311
        }
312
313 2
        throw new InvalidArgumentException('Parameter "' . $id . '" not found.');
314
    }
315
316
    /**
317
     * Checks the existence of a common parameter by ID.
318
     *
319
     * @param string $id The unique identifier of the parameter.
320
     *
321
     * @return bool Whether a custom parameter that is common for all view templates exists.
322
     */
323 1
    public function hasParameter(string $id): bool
324
    {
325 1
        return isset($this->parameters[$id]);
326
    }
327
328
    /**
329
     * Sets a content block.
330
     *
331
     * @param string $id The unique identifier of the block.
332
     * @param string $content The content of the block.
333
     *
334
     * @return static
335
     */
336 3
    public function setBlock(string $id, string $content): self
337
    {
338 3
        $this->blocks[$id] = $content;
339 3
        return $this;
340
    }
341
342
    /**
343
     * Removes a content block.
344
     *
345
     * @param string $id The unique identifier of the block.
346
     */
347 1
    public function removeBlock(string $id): void
348
    {
349 1
        unset($this->blocks[$id]);
350 1
    }
351
352
    /**
353
     * Gets content of the block by ID.
354
     *
355
     * @param string $id The unique identifier of the block.
356
     *
357
     * @return string The content of the block.
358
     */
359 2
    public function getBlock(string $id): string
360
    {
361 2
        if (isset($this->blocks[$id])) {
362 1
            return $this->blocks[$id];
363
        }
364
365 2
        throw new InvalidArgumentException('Block "' . $id . '" not found.');
366
    }
367
368
    /**
369
     * Checks the existence of a content block by ID.
370
     *
371
     * @param string $id The unique identifier of the block.
372
     *
373
     * @return bool Whether a content block exists.
374
     */
375 1
    public function hasBlock(string $id): bool
376
    {
377 1
        return isset($this->blocks[$id]);
378
    }
379
380
    /**
381
     * Gets the view file currently being rendered.
382
     *
383
     * @return string|null The view file currently being rendered. `null` if no view file is being rendered.
384
     */
385 2
    public function getViewFile(): ?string
386
    {
387
        /** @psalm-suppress InvalidArrayOffset */
388 2
        return empty($this->viewFiles) ? null : end($this->viewFiles)['resolved'];
389
    }
390
391
    /**
392
     * Gets the placeholder signature.
393
     *
394
     * @return string The placeholder signature.
395
     */
396 47
    public function getPlaceholderSignature(): string
397
    {
398 47
        return $this->placeholderSignature;
399
    }
400
401
    /**
402
     * Sets a salt for the placeholder signature {@see getPlaceholderSignature()}.
403
     *
404
     * @param string $salt The placeholder salt.
405
     *
406
     * @return static
407
     */
408 111
    public function setPlaceholderSalt(string $salt): self
409
    {
410 111
        $this->placeholderSignature = dechex(crc32($salt));
411 111
        return $this;
412
    }
413
414
    /**
415
     * Renders a view.
416
     *
417
     * The view to be rendered can be specified in one of the following formats:
418
     *
419
     * - The name of the view starting with a slash to join the base path {@see getBasePath()} (e.g. "/site/index").
420
     * - The name of the view without the starting slash (e.g. "site/index"). The corresponding view file will be
421
     *   looked for under the {@see ViewContextInterface::getViewPath()} of the context set via {@see withContext()}.
422
     *   If the context instance was not set {@see withContext()}, it will be looked for under the directory containing
423
     *   the view currently being rendered (i.e., this happens when rendering a view within another view).
424
     *
425
     * @param string $view The view name.
426
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
427
     * file.
428
     *
429
     * @throws RuntimeException If the view cannot be resolved.
430
     * @throws ViewNotFoundException If the view file does not exist.
431
     * @throws Throwable
432
     *
433
     * {@see renderFile()}
434
     *
435
     * @return string The rendering result.
436
     */
437 51
    public function render(string $view, array $parameters = []): string
438
    {
439 51
        $viewFile = $this->findTemplateFile($view);
440
441 50
        return $this->renderFile($viewFile, $parameters);
442
    }
443
444
    /**
445
     * Renders a view file.
446
     *
447
     * If the theme was set {@see withTheme()}, it will try to render the themed version of the view file
448
     * as long as it is available.
449
     *
450
     * If the renderer was set {@see withRenderers()}, the method will use it to render the view file. Otherwise,
451
     * it will simply include the view file as a normal PHP file, capture its output and return it as a string.
452
     *
453
     * @param string $viewFile The full absolute path of the view file.
454
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
455
     * file.
456
     *
457
     * @throws Throwable
458
     * @throws ViewNotFoundException If the view file does not exist
459
     *
460
     * @return string The rendering result.
461
     */
462 55
    public function renderFile(string $viewFile, array $parameters = []): string
463
    {
464 55
        $parameters = array_merge($this->parameters, $parameters);
465
466
        // TODO: these two match now
467 55
        $requestedFile = $viewFile;
468
469 55
        if ($this->theme !== null) {
470 1
            $viewFile = $this->theme->applyTo($viewFile);
471
        }
472
473 55
        if (is_file($viewFile)) {
474 54
            $viewFile = $this->localize($viewFile);
475
        } else {
476 1
            throw new ViewNotFoundException("The view file \"$viewFile\" does not exist.");
477
        }
478
479 54
        $output = '';
480 54
        $this->viewFiles[] = [
481 54
            'resolved' => $viewFile,
482 54
            'requested' => $requestedFile,
483
        ];
484
485 54
        if ($this->beforeRender($viewFile, $parameters)) {
486 54
            $ext = pathinfo($viewFile, PATHINFO_EXTENSION);
487 54
            $renderer = $this->renderers[$ext] ?? new PhpTemplateRenderer();
488 54
            $output = $renderer->render($this, $viewFile, $parameters);
0 ignored issues
show
Bug introduced by
$this of type Yiisoft\View\ViewTrait is incompatible with the type Yiisoft\View\ViewInterface expected by parameter $view of Yiisoft\View\PhpTemplateRenderer::render(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

488
            $output = $renderer->render(/** @scrutinizer ignore-type */ $this, $viewFile, $parameters);
Loading history...
489 53
            $output = $this->afterRender($viewFile, $parameters, $output);
490
        }
491
492 53
        array_pop($this->viewFiles);
493
494 53
        return $output;
495
    }
496
497
    /**
498
     * Returns the localized version of a specified file.
499
     *
500
     * The searching is based on the specified language code. In particular, a file with the same name will be looked
501
     * for under the subdirectory whose name is the same as the language code. For example, given the file
502
     * "path/to/view.php" and language code "zh-CN", the localized file will be looked for as "path/to/zh-CN/view.php".
503
     * If the file is not found, it will try a fallback with just a language code that is "zh"
504
     * i.e. "path/to/zh/view.php".
505
     * If it is not found as well the original file will be returned.
506
     *
507
     * If the target and the source language codes are the same, the original file will be returned.
508
     *
509
     * @param string $file The original file
510
     * @param string|null $language The target language that the file should be localized to.
511
     * @param string|null $sourceLanguage The language that the original file is in.
512
     *
513
     * @return string The matching localized file, or the original file if the localized version is not found.
514
     * If the target and the source language codes are the same, the original file will be returned.
515
     */
516 56
    public function localize(string $file, ?string $language = null, ?string $sourceLanguage = null): string
517
    {
518 56
        $language = $language ?? $this->language;
519 56
        $sourceLanguage = $sourceLanguage ?? $this->sourceLanguage;
520
521 56
        if ($language === $sourceLanguage) {
522 56
            return $file;
523
        }
524
525 2
        $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
526
527 2
        if (is_file($desiredFile)) {
528 2
            return $desiredFile;
529
        }
530
531 1
        $language = substr($language, 0, 2);
532
533 1
        if ($language === $sourceLanguage) {
534 1
            return $file;
535
        }
536
537 1
        $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
538 1
        return is_file($desiredFile) ? $desiredFile : $file;
539
    }
540
541
    /**
542
     * Creates an event that occurs before rendering.
543
     *
544
     * @param string $viewFile The view file to be rendered.
545
     * @param array $parameters The parameter array passed to the {@see renderFile()} method.
546
     *
547
     * @return StoppableEventInterface The stoppable event instance.
548
     */
549
    abstract protected function createBeforeRenderEvent(string $viewFile, array $parameters): StoppableEventInterface;
550
551
    /**
552
     * Creates an event that occurs after rendering.
553
     *
554
     * @param string $viewFile The view file being rendered.
555
     * @param array $parameters The parameter array passed to the {@see renderFile()} method.
556
     * @param string $result The rendering result of the view file.
557
     *
558
     * @return AfterRenderEventInterface The event instance.
559
     */
560
    abstract protected function createAfterRenderEvent(
561
        string $viewFile,
562
        array $parameters,
563
        string $result
564
    ): AfterRenderEventInterface;
565
566
    /**
567
     * This method is invoked right before {@see renderFile()} renders a view file.
568
     *
569
     * The default implementations will trigger the {@see \Yiisoft\View\Event\View\BeforeRender}
570
     * or {@see \Yiisoft\View\Event\WebView\BeforeRender} event. If you override this method,
571
     * make sure you call the parent implementation first.
572
     *
573
     * @param string $viewFile The view file to be rendered.
574
     * @param array $parameters The parameter array passed to the {@see renderFile()} method.
575
     *
576
     * @return bool Whether to continue rendering the view file.
577
     */
578 54
    private function beforeRender(string $viewFile, array $parameters): bool
579
    {
580 54
        $event = $this->createBeforeRenderEvent($viewFile, $parameters);
581 54
        $event = $this->eventDispatcher->dispatch($event);
582
        /** @var StoppableEventInterface $event */
583 54
        return !$event->isPropagationStopped();
584
    }
585
586
    /**
587
     * This method is invoked right after {@see renderFile()} renders a view file.
588
     *
589
     * The default implementations will trigger the {@see \Yiisoft\View\Event\View\AfterRender}
590
     * or {@see \Yiisoft\View\Event\WebView\AfterRender} event. If you override this method,
591
     * make sure you call the parent implementation first.
592
     *
593
     * @param string $viewFile The view file being rendered.
594
     * @param array $parameters The parameter array passed to the {@see renderFile()} method.
595
     * @param string $result The rendering result of the view file.
596
     *
597
     * @return string Updated output. It will be passed to {@see renderFile()} and returned.
598
     */
599 53
    private function afterRender(string $viewFile, array $parameters, string $result): string
600
    {
601 53
        $event = $this->createAfterRenderEvent($viewFile, $parameters, $result);
602
603
        /** @var AfterRenderEventInterface $event */
604 53
        $event = $this->eventDispatcher->dispatch($event);
605
606 53
        return $event->getResult();
607
    }
608
609
    /**
610
     * Finds the view file based on the given view name.
611
     *
612
     * @param string $view The view name of the view file. Please refer to
613
     * {@see render()} on how to specify this parameter.
614
     *
615
     * @throws RuntimeException If a relative view name is given while there is no active context to determine the
616
     * corresponding view file.
617
     *
618
     * @return string The view file path. Note that the file may not exist.
619
     */
620 53
    private function findTemplateFile(string $view): string
621
    {
622 53
        if ($view !== '' && $view[0] === '/') {
623
            // path relative to basePath e.g. "/layouts/main"
624 49
            $file = $this->basePath . '/' . ltrim($view, '/');
625 6
        } elseif (($currentViewFile = $this->getRequestedViewFile()) !== null) {
626
            // path relative to currently rendered view
627 2
            $file = dirname($currentViewFile) . '/' . $view;
628 5
        } elseif ($this->context instanceof ViewContextInterface) {
629
            // path provided by context
630 4
            $file = $this->context->getViewPath() . '/' . $view;
631
        } else {
632 1
            throw new RuntimeException("Unable to resolve view file for view \"$view\": no active view context.");
633
        }
634
635 52
        if (pathinfo($file, PATHINFO_EXTENSION) !== '') {
636 45
            return $file;
637
        }
638
639 7
        $path = $file . '.' . $this->defaultExtension;
640
641 7
        if ($this->defaultExtension !== 'php' && !is_file($path)) {
642 1
            $path = $file . '.php';
643
        }
644
645 7
        return $path;
646
    }
647
648
    /**
649
     * @return string|null The requested view currently being rendered. `null` if no view file is being rendered.
650
     */
651 6
    private function getRequestedViewFile(): ?string
652
    {
653
        /** @psalm-suppress InvalidArrayOffset */
654 6
        return empty($this->viewFiles) ? null : end($this->viewFiles)['requested'];
655
    }
656
}
657