Passed
Pull Request — master (#199)
by Sergei
04:29 queued 02:11
created

ViewTrait   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 621
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 3 Features 0
Metric Value
wmc 50
eloc 115
c 3
b 3
f 0
dl 0
loc 621
ccs 137
cts 137
cp 1
rs 8.4

34 Methods

Rating   Name   Duplication   Size   Complexity  
A getRequestedViewFile() 0 4 2
B findTemplateFile() 0 26 8
A localize() 0 23 5
A afterRender() 0 8 1
A beforeRender() 0 6 1
A setPlaceholderSalt() 0 3 1
A setParameter() 0 4 1
A withContext() 0 6 1
A hasBlock() 0 3 1
A getBlock() 0 3 1
A withDefaultExtension() 0 5 1
A withRenderers() 0 5 1
A withBasePath() 0 5 1
A getDefaultExtension() 0 3 1
A removeParameter() 0 4 1
A getViewFile() 0 4 2
A addToParameter() 0 4 1
A getTheme() 0 3 1
A getParameter() 0 3 1
A setBlock() 0 4 1
A withContextPath() 0 3 1
A getBasePath() 0 3 1
A hasParameter() 0 3 1
A render() 0 5 1
A getPlaceholderSignature() 0 3 1
A setParameters() 0 4 1
A withPlaceholderSalt() 0 5 1
A removeBlock() 0 4 1
A setTheme() 0 4 1
A withLocale() 0 6 1
A setLocale() 0 4 1
A clear() 0 5 1
A withSourceLocale() 0 5 1
A renderFile() 0 34 4

How to fix   Complexity   

Complex Class

Complex classes like ViewTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ViewTrait, and based on these observations, apply Extract Interface, too.

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

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

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