ViewTrait::withContext()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
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
use Yiisoft\View\State\LocaleState;
15
use Yiisoft\View\State\ThemeState;
16
17
use function array_merge;
18
use function array_pop;
19
use function basename;
20
use function call_user_func_array;
21
use function crc32;
22
use function dechex;
23
use function dirname;
24
use function end;
25
use function func_get_args;
26
use function is_file;
27
use function pathinfo;
28
use function substr;
29
30
/**
31
 * `ViewTrait` could be used as a base implementation of {@see ViewInterface}.
32
 *
33
 * @internal
34
 */
35
trait ViewTrait
36
{
37
    private EventDispatcherInterface $eventDispatcher;
38
39
    private string $basePath;
40
    private ?ViewContextInterface $context = null;
41
    private string $placeholderSignature;
42
    private string $sourceLocale = 'en';
43
    /**
44
     * @var string[]
45
     */
46
    private array $fallbackExtensions = [self::PHP_EXTENSION];
47
48
    /**
49
     * @var array A list of available renderers indexed by their corresponding
50
     * supported file extensions.
51
     * @psalm-var array<string, TemplateRendererInterface>
52
     */
53
    private array $renderers = [];
54
55
    /**
56
     * @var array The view files currently being rendered. There may be multiple view files being
57
     * rendered at a moment because one view may be rendered within another.
58
     *
59
     * @psalm-var array<array-key, array<string, string>>
60
     */
61
    private array $viewFiles = [];
62
63
    /**
64
     * Returns a new instance with specified base path to the view directory.
65
     *
66
     * @param string $basePath The base path to the view directory.
67
     */
68 2
    public function withBasePath(string $basePath): static
69
    {
70 2
        $new = clone $this;
71 2
        $new->basePath = $basePath;
72 2
        return $new;
73
    }
74
75
    /**
76
     * Returns a new instance with the specified renderers.
77
     *
78
     * @param array $renderers A list of available renderers indexed by their
79
     * corresponding supported file extensions.
80
     *
81
     * ```php
82
     * $view = $view->withRenderers(['twig' => new \Yiisoft\View\Twig\ViewRenderer($environment)]);
83
     * ```
84
     *
85
     * If no renderer is available for the given view file, the view file will be treated as a normal PHP
86
     * and rendered via {@see PhpTemplateRenderer}.
87
     *
88
     * @psalm-param array<string, TemplateRendererInterface> $renderers
89
     */
90 3
    public function withRenderers(array $renderers): static
91
    {
92 3
        $new = clone $this;
93 3
        $new->renderers = $renderers;
94 3
        return $new;
95
    }
96
97
    /**
98
     * Returns a new instance with the specified source locale.
99
     *
100
     * @param string $locale The source locale.
101
     */
102 3
    public function withSourceLocale(string $locale): static
103
    {
104 3
        $new = clone $this;
105 3
        $new->sourceLocale = $locale;
106 3
        return $new;
107
    }
108
109
    /**
110
     * Returns a new instance with the specified default view file extension.
111
     *
112
     * @param string $defaultExtension The default view file extension. Default is {@see ViewInterface::PHP_EXTENSION}.
113
     * This will be appended to view file names if they don't have file extensions.
114
     * @deprecated Since 8.0.1 and will be removed in the next major version. Use {@see withFallbackExtension()} instead.
115
     */
116 8
    public function withDefaultExtension(string $defaultExtension): static
117
    {
118 8
        return $this->withFallbackExtension($defaultExtension);
119
    }
120
121
    /**
122
     * Returns a new instance with the specified fallback view file extension.
123
     *
124
     * @param string $fallbackExtension The fallback view file extension. Default is {@see ViewInterface::PHP_EXTENSION}.
125
     * This will be appended to view file names if they don't exist.
126
     */
127 8
    public function withFallbackExtension(string $fallbackExtension, string ...$otherFallbacks): static
128
    {
129 8
        $new = clone $this;
130 8
        $new->fallbackExtensions = [$fallbackExtension, ...array_values($otherFallbacks)];
0 ignored issues
show
Documentation Bug introduced by
array($fallbackExtension...alues($otherFallbacks)) is of type array<integer,array|string>, but the property $fallbackExtensions was declared to be of type string[]. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
131 8
        return $new;
132
    }
133
134
    /**
135
     * Returns a new instance with the specified view context instance.
136
     *
137
     * @param ViewContextInterface $context The context under which the {@see renderFile()} method is being invoked.
138
     */
139 9
    public function withContext(ViewContextInterface $context): static
140
    {
141 9
        $new = clone $this;
142 9
        $new->context = $context;
143 9
        $new->viewFiles = [];
144 9
        return $new;
145
    }
146
147
    /**
148
     * Returns a new instance with the specified view context path.
149
     *
150
     * @param string $path The context path under which the {@see renderFile()} method is being invoked.
151
     */
152 2
    public function withContextPath(string $path): static
153
    {
154 2
        return $this->withContext(new ViewContext($path));
155
    }
156
157
    /**
158
     * Returns a new instance with specified salt for the placeholder signature {@see getPlaceholderSignature()}.
159
     *
160
     * @param string $salt The placeholder salt.
161
     */
162 2
    public function withPlaceholderSalt(string $salt): static
163
    {
164 2
        $new = clone $this;
165 2
        $new->setPlaceholderSalt($salt);
166 2
        return $new;
167
    }
168
169
    /**
170
     * Set the specified locale code.
171
     *
172
     * @param string $locale The locale code.
173
     */
174 3
    public function setLocale(string $locale): static
175
    {
176 3
        $this->localeState->setLocale($locale);
177 3
        return $this;
178
    }
179
180
    /**
181
     * Set the specified locale code.
182
     *
183
     * @param string $locale The locale code.
184
     */
185 3
    public function withLocale(string $locale): static
186
    {
187 3
        $new = clone $this;
188 3
        $new->localeState = new LocaleState($locale);
0 ignored issues
show
Bug Best Practice introduced by
The property localeState does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
189
190 3
        return $new;
191
    }
192
193
    /**
194
     * Get the specified locale code.
195
     *
196
     * @return string The locale code.
197
     */
198 1
    public function getLocale(): string
199
    {
200 1
        return $this->localeState->getLocale();
201
    }
202
203
    /**
204
     * Gets the base path to the view directory.
205
     *
206
     * @return string The base view path.
207
     */
208 2
    public function getBasePath(): string
209
    {
210 2
        return $this->basePath;
211
    }
212
213
    /**
214
     * Gets the default view file extension.
215
     *
216
     * @return string The default view file extension.
217
     * @deprecated Since 8.0.1 and will be removed in the next major version. Use {@see getFallbackExtensions()} instead.
218
     */
219 1
    public function getDefaultExtension(): string
220
    {
221 1
        return $this->getFallbackExtensions()[0];
222
    }
223
224
    /**
225
     * Gets the fallback view file extension.
226
     *
227
     * @return string[] The fallback view file extension.
228
     */
229 1
    public function getFallbackExtensions(): array
230
    {
231 1
        return $this->fallbackExtensions;
232
    }
233
234
    /**
235
     * Gets the theme instance, or `null` if no theme has been set.
236
     *
237
     * @return Theme|null The theme instance, or `null` if no theme has been set.
238
     */
239 65
    public function getTheme(): ?Theme
240
    {
241 65
        return $this->themeState->getTheme();
242
    }
243
244
    /**
245
     * Set the specified theme instance.
246
     *
247
     * @param Theme|null $theme The theme instance or `null` for reset theme.
248
     */
249 1
    public function setTheme(?Theme $theme): static
250
    {
251 1
        $this->themeState->setTheme($theme);
252 1
        return $this;
253
    }
254
255 2
    public function withTheme(?Theme $theme): static
256
    {
257 2
        $new = clone $this;
258 2
        $new->themeState = new ThemeState($theme);
0 ignored issues
show
Bug Best Practice introduced by
The property themeState does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
259
260 2
        return $new;
261
    }
262
263
    /**
264
     * Sets a common parameters that is accessible in all view templates.
265
     *
266
     * @param array $parameters Parameters that are common for all view templates.
267
     *
268
     * @psalm-param array<string, mixed> $parameters
269
     *
270
     * @see setParameter()
271
     */
272 5
    public function setParameters(array $parameters): static
273
    {
274 5
        $this->state->setParameters($parameters);
275 5
        return $this;
276
    }
277
278
    /**
279
     * Sets a common parameter that is accessible in all view templates.
280
     *
281
     * @param string $id The unique identifier of the parameter.
282
     * @param mixed $value The value of the parameter.
283
     */
284 12
    public function setParameter(string $id, mixed $value): static
285
    {
286 12
        $this->state->setParameter($id, $value);
287 12
        return $this;
288
    }
289
290
    /**
291
     * Add values to end of common array parameter. If specified parameter does not exist or him is not array,
292
     * then parameter will be added as empty array.
293
     *
294
     * @param string $id The unique identifier of the parameter.
295
     * @param mixed ...$value Value(s) for add to end of array parameter.
296
     *
297
     * @throws InvalidArgumentException When specified parameter already exists and is not an array.
298
     */
299 6
    public function addToParameter(string $id, mixed ...$value): static
300
    {
301 6
        $this->state->addToParameter($id, ...$value);
302 5
        return $this;
303
    }
304
305
    /**
306
     * Removes a common parameter.
307
     *
308
     * @param string $id The unique identifier of the parameter.
309
     */
310 3
    public function removeParameter(string $id): static
311
    {
312 3
        $this->state->removeParameter($id);
313 3
        return $this;
314
    }
315
316
    /**
317
     * Gets a common parameter value by ID.
318
     *
319
     * @param string $id The unique identifier of the parameter.
320
     * @param mixed $default The default value to be returned if the specified parameter does not exist.
321
     *
322
     * @throws InvalidArgumentException If specified parameter does not exist and not passed default value.
323
     *
324
     * @return mixed The value of the parameter.
325
     */
326 8
    public function getParameter(string $id)
327
    {
328 8
        return call_user_func_array([$this->state, 'getParameter'], func_get_args());
329
    }
330
331
    /**
332
     * Checks the existence of a common parameter by ID.
333
     *
334
     * @param string $id The unique identifier of the parameter.
335
     *
336
     * @return bool Whether a custom parameter that is common for all view templates exists.
337
     */
338 5
    public function hasParameter(string $id): bool
339
    {
340 5
        return $this->state->hasParameter($id);
341
    }
342
343
    /**
344
     * Sets a content block.
345
     *
346
     * @param string $id The unique identifier of the block.
347
     * @param string $content The content of the block.
348
     */
349 7
    public function setBlock(string $id, string $content): static
350
    {
351 7
        $this->state->setBlock($id, $content);
352 7
        return $this;
353
    }
354
355
    /**
356
     * Removes a content block.
357
     *
358
     * @param string $id The unique identifier of the block.
359
     */
360 3
    public function removeBlock(string $id): static
361
    {
362 3
        $this->state->removeBlock($id);
363 3
        return $this;
364
    }
365
366
    /**
367
     * Gets content of the block by ID.
368
     *
369
     * @param string $id The unique identifier of the block.
370
     *
371
     * @return string The content of the block.
372
     */
373 2
    public function getBlock(string $id): string
374
    {
375 2
        return $this->state->getBlock($id);
376
    }
377
378
    /**
379
     * Checks the existence of a content block by ID.
380
     *
381
     * @param string $id The unique identifier of the block.
382
     *
383
     * @return bool Whether a content block exists.
384
     */
385 5
    public function hasBlock(string $id): bool
386
    {
387 5
        return $this->state->hasBlock($id);
388
    }
389
390
    /**
391
     * Gets the view file currently being rendered.
392
     *
393
     * @return string|null The view file currently being rendered. `null` if no view file is being rendered.
394
     */
395 5
    public function getViewFile(): ?string
396
    {
397
        /** @psalm-suppress InvalidArrayOffset */
398 5
        return empty($this->viewFiles) ? null : end($this->viewFiles)['resolved'];
399
    }
400
401
    /**
402
     * Gets the placeholder signature.
403
     *
404
     * @return string The placeholder signature.
405
     */
406 49
    public function getPlaceholderSignature(): string
407
    {
408 49
        return $this->placeholderSignature;
409
    }
410
411
    /**
412
     * Renders a view.
413
     *
414
     * The view to be rendered can be specified in one of the following formats:
415
     *
416
     * - The name of the view starting with a slash to join the base path {@see getBasePath()} (e.g. "/site/index").
417
     * - The name of the view without the starting slash (e.g. "site/index"). The corresponding view file will be
418
     *   looked for under the {@see ViewContextInterface::getViewPath()} of the context set via {@see withContext()}.
419
     *   If the context instance was not set {@see withContext()}, it will be looked for under the directory containing
420
     *   the view currently being rendered (i.e., this happens when rendering a view within another view).
421
     *
422
     * @param string $view The view name.
423
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
424
     * file.
425
     *
426
     * @throws RuntimeException If the view cannot be resolved.
427
     * @throws ViewNotFoundException If the view file does not exist.
428
     * @throws Throwable
429
     *
430
     * {@see renderFile()}
431
     *
432
     * @return string The rendering result.
433
     */
434 60
    public function render(string $view, array $parameters = []): string
435
    {
436 60
        $viewFile = $this->findTemplateFile($view);
437
438 59
        return $this->renderFile($viewFile, $parameters);
439
    }
440
441
    /**
442
     * Renders a view file.
443
     *
444
     * If the theme was set {@see setTheme()}, it will try to render the themed version of the view file
445
     * as long as it's available.
446
     *
447
     * If the renderer was set {@see withRenderers()}, the method will use it to render the view file. Otherwise,
448
     * it will simply include the view file as a normal PHP file, capture its output and return it as a string.
449
     *
450
     * @param string $viewFile The full absolute path of the view file.
451
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
452
     * file.
453
     *
454
     * @throws Throwable
455
     * @throws ViewNotFoundException If the view file doesn't exist
456
     *
457
     * @return string The rendering result.
458
     */
459 64
    public function renderFile(string $viewFile, array $parameters = []): string
460
    {
461 64
        $parameters = array_merge($this->state->getParameters(), $parameters);
462
463
        // TODO: these two match now
464 64
        $requestedFile = $viewFile;
465
466 64
        $theme = $this->getTheme();
467 64
        if ($theme !== null) {
468 1
            $viewFile = $theme->applyTo($viewFile);
469
        }
470
471 64
        if (is_file($viewFile)) {
472 63
            $viewFile = $this->localize($viewFile);
473
        } else {
474 1
            throw new ViewNotFoundException("The view file \"$viewFile\" does not exist.");
475
        }
476
477 63
        $output = '';
478 63
        $this->viewFiles[] = [
479 63
            'resolved' => $viewFile,
480 63
            'requested' => $requestedFile,
481 63
        ];
482
483 63
        if ($this->beforeRender($viewFile, $parameters)) {
484 63
            $ext = pathinfo($viewFile, PATHINFO_EXTENSION);
485 63
            $renderer = $this->renderers[$ext] ?? new PhpTemplateRenderer();
486 63
            $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

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