Passed
Pull Request — master (#232)
by Rustam
02:48
created

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

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