Passed
Push — master ( 66bc9c...3db065 )
by Alexander
03:13 queued 38s
created

ViewTrait   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 594
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 48
eloc 111
c 0
b 0
f 0
dl 0
loc 594
ccs 133
cts 133
cp 1
rs 8.5599

32 Methods

Rating   Name   Duplication   Size   Complexity  
A setParameter() 0 4 1
A withContext() 0 6 1
A localize() 0 23 5
A getBlock() 0 3 1
A hasBlock() 0 3 1
A withDefaultExtension() 0 5 1
A afterRender() 0 8 1
A withSourceLanguage() 0 5 1
A withRenderers() 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 withLanguage() 0 5 1
A setBlock() 0 4 1
A clear() 0 4 1
A withContextPath() 0 3 1
A getBasePath() 0 3 1
A hasParameter() 0 3 1
A beforeRender() 0 6 1
A render() 0 5 1
A getPlaceholderSignature() 0 3 1
A setParameters() 0 4 1
A getRequestedViewFile() 0 4 2
B findTemplateFile() 0 26 8
A withPlaceholderSalt() 0 5 1
A setPlaceholderSalt() 0 3 1
A renderFile() 0 33 4
A removeBlock() 0 4 1
A withTheme() 0 5 1

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

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