Passed
Push — master ( ca3f40...fae10a )
by Alexander
02:15
created

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

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