Passed
Push — master ( 954765...3431ea )
by Alexander
37:12 queued 23:51
created

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

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