Passed
Push — master ( 008671...38e735 )
by Rustam
03:00
created

ViewTrait::getFallbackExtensions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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