Passed
Push — master ( 242222...20f84a )
by Alexander
02:18
created

ViewTrait::withContext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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

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