Passed
Pull Request — master (#194)
by Sergei
06:55
created

ViewTrait::getViewFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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

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

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