Passed
Pull Request — master (#195)
by Sergei
08:08 queued 05:57
created

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

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