Passed
Pull Request — master (#199)
by
unknown
08:12 queued 05:43
created

ViewTrait::getDefaultExtension()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
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 the specified renderers.
60
     *
61
     * @param array $renderers A list of available renderers indexed by their
62
     * corresponding supported file extensions.
63
     *
64
     * ```php
65
     * $view = $view->withRenderers(['twig' => new \Yiisoft\Yii\Twig\ViewRenderer($environment)]);
66
     * ```
67
     *
68
     * If no renderer is available for the given view file, the view file will be treated as a normal PHP
69
     * and rendered via {@see PhpTemplateRenderer}.
70
     *
71
     * @psalm-param array<string, TemplateRendererInterface> $renderers
72
     *
73
     * @return static
74
     */
75 1
    public function withRenderers(array $renderers): self
76
    {
77 1
        $new = clone $this;
78 1
        $new->renderers = $renderers;
79 1
        return $new;
80
    }
81
82
    /**
83
     * Returns a new instance with the specified source language.
84
     *
85
     * @param string $language The source language.
86
     *
87
     * @return static
88
     */
89 3
    public function withSourceLanguage(string $language): self
90
    {
91 3
        $new = clone $this;
92 3
        $new->sourceLanguage = $language;
93 3
        return $new;
94
    }
95
96
    /**
97
     * Returns a new instance with the specified default view file extension.
98
     *
99
     * @param string $defaultExtension The default view file extension. Default is "php".
100
     * This will be appended to view file names if they don't have file extensions.
101
     *
102
     * @return static
103
     */
104 2
    public function withDefaultExtension(string $defaultExtension): self
105
    {
106 2
        $new = clone $this;
107 2
        $new->defaultExtension = $defaultExtension;
108 2
        return $new;
109
    }
110
111
    /**
112
     * Returns a new instance with the specified view context instance.
113
     *
114
     * @param ViewContextInterface $context The context under which the {@see renderFile()} method is being invoked.
115
     *
116
     * @return static
117
     */
118 6
    public function withContext(ViewContextInterface $context): self
119
    {
120 6
        $new = clone $this;
121 6
        $new->context = $context;
122 6
        $new->viewFiles = [];
123 6
        return $new;
124
    }
125
126
    /**
127
     * Returns a new instance with the specified view context path.
128
     *
129
     * @param string $path The context path under which the {@see renderFile()} method is being invoked.
130
     *
131
     * @return static
132
     */
133 2
    public function withContextPath(string $path): self
134
    {
135 2
        return $this->withContext(new ViewContext($path));
136
    }
137
138
    /**
139
     * Returns a new instance with specified salt for the placeholder signature {@see getPlaceholderSignature()}.
140
     *
141
     * @param string $salt The placeholder salt.
142
     *
143
     * @return static
144
     */
145 2
    public function withPlaceholderSalt(string $salt): self
146
    {
147 2
        $new = clone $this;
148 2
        $new->setPlaceholderSalt($salt);
149 2
        return $new;
150
    }
151
152
    /**
153
     * Returns a new instance with specified language code.
154
     *
155
     * @param string $language The language code.
156
     *
157
     * @return static
158
     */
159 1
    public function withLanguage(string $language): self
160
    {
161 1
        $new = clone $this;
162 1
        $new->setLanguage($language);
163 1
        return $new;
164
    }
165
166
    /**
167
     * Returns a new instance with specified base path to the view directory.
168
     *
169
     * @param string $basePath The base path to the view directory.
170
     *
171
     * @return static
172
     */
173 1
    public function withBasePath(string $basePath): self
174
    {
175 1
        $new = clone $this;
176 1
        $new->basePath = $basePath;
177 1
        return $new;
178
    }
179
180
    /**
181
     * Set the specified language code.
182
     *
183
     * @param string $language The language code.
184
     *
185
     * @return static
186
     */
187 2
    public function setLanguage(string $language): self
188
    {
189 2
        $this->state->setLanguage($language);
190 2
        return $this;
191
    }
192
193
    /**
194
     * Gets the base path to the view directory.
195
     *
196
     * @return string The base view path.
197
     */
198 1
    public function getBasePath(): string
199
    {
200 1
        return $this->basePath;
201
    }
202
203
    /**
204
     * Gets the default view file extension.
205
     *
206
     * @return string The default view file extension.
207
     */
208 1
    public function getDefaultExtension(): string
209
    {
210 1
        return $this->defaultExtension;
211
    }
212
213
    /**
214
     * Gets the theme instance, or `null` if no theme has been set.
215
     *
216
     * @return Theme|null The theme instance, or `null` if no theme has been set.
217
     */
218 59
    public function getTheme(): ?Theme
219
    {
220 59
        return $this->state->getTheme();
221
    }
222
223
    /**
224
     * Set the specified theme instance.
225
     *
226
     * @param Theme|null $theme The theme instance or `null` for reset theme.
227
     *
228
     * @return static
229
     */
230 1
    public function setTheme(?Theme $theme): self
231
    {
232 1
        $this->state->setTheme($theme);
233 1
        return $this;
234
    }
235
236
    /**
237
     * Sets a common parameters that is accessible in all view templates.
238
     *
239
     * @param array $parameters Parameters that are common for all view templates.
240
     *
241
     * @psalm-param array<string, mixed> $parameters
242
     *
243
     * @return static
244
     *
245
     * @see setParameter()
246
     */
247 3
    public function setParameters(array $parameters): self
248
    {
249 3
        $this->state->setParameters($parameters);
250 3
        return $this;
251
    }
252
253
    /**
254
     * Sets a common parameter that is accessible in all view templates.
255
     *
256
     * @param string $id The unique identifier of the parameter.
257
     * @param mixed $value The value of the parameter.
258
     *
259
     * @return static
260
     */
261 12
    public function setParameter(string $id, $value): self
262
    {
263 12
        $this->state->setParameter($id, $value);
264 12
        return $this;
265
    }
266
267
    /**
268
     * Add values to end of common array parameter. If specified parameter does not exist or him is not array,
269
     * then parameter will be added as empty array.
270
     *
271
     * @param string $id The unique identifier of the parameter.
272
     * @param mixed ...$value Value(s) for add to end of array parameter.
273
     *
274
     * @throws InvalidArgumentException When specified parameter already exists and is not an array.
275
     *
276
     * @return static
277
     */
278 6
    public function addToParameter(string $id, ...$value): self
279
    {
280 6
        $this->state->addToParameter($id, ...$value);
281 5
        return $this;
282
    }
283
284
    /**
285
     * Removes a common parameter.
286
     *
287
     * @param string $id The unique identifier of the parameter.
288
     *
289
     * @return static
290
     */
291 3
    public function removeParameter(string $id): self
292
    {
293 3
        $this->state->removeParameter($id);
294 3
        return $this;
295
    }
296
297
    /**
298
     * Gets a common parameter value by ID.
299
     *
300
     * @param string $id The unique identifier of the parameter.
301
     * @param mixed $default The default value to be returned if the specified parameter does not exist.
302
     *
303
     * @throws InvalidArgumentException If specified parameter does not exist and not passed default value.
304
     *
305
     * @return mixed The value of the parameter.
306
     */
307 8
    public function getParameter(string $id)
308
    {
309 8
        return call_user_func_array([$this->state, 'getParameter'], func_get_args());
310
    }
311
312
    /**
313
     * Checks the existence of a common parameter by ID.
314
     *
315
     * @param string $id The unique identifier of the parameter.
316
     *
317
     * @return bool Whether a custom parameter that is common for all view templates exists.
318
     */
319 5
    public function hasParameter(string $id): bool
320
    {
321 5
        return $this->state->hasParameter($id);
322
    }
323
324
    /**
325
     * Sets a content block.
326
     *
327
     * @param string $id The unique identifier of the block.
328
     * @param string $content The content of the block.
329
     *
330
     * @return static
331
     */
332 7
    public function setBlock(string $id, string $content): self
333
    {
334 7
        $this->state->setBlock($id, $content);
335 7
        return $this;
336
    }
337
338
    /**
339
     * Removes a content block.
340
     *
341
     * @param string $id The unique identifier of the block.
342
     *
343
     * @return static
344
     */
345 3
    public function removeBlock(string $id): self
346
    {
347 3
        $this->state->removeBlock($id);
348 3
        return $this;
349
    }
350
351
    /**
352
     * Gets content of the block by ID.
353
     *
354
     * @param string $id The unique identifier of the block.
355
     *
356
     * @return string The content of the block.
357
     */
358 2
    public function getBlock(string $id): string
359
    {
360 2
        return $this->state->getBlock($id);
361
    }
362
363
    /**
364
     * Checks the existence of a content block by ID.
365
     *
366
     * @param string $id The unique identifier of the block.
367
     *
368
     * @return bool Whether a content block exists.
369
     */
370 5
    public function hasBlock(string $id): bool
371
    {
372 5
        return $this->state->hasBlock($id);
373
    }
374
375
    /**
376
     * Gets the view file currently being rendered.
377
     *
378
     * @return string|null The view file currently being rendered. `null` if no view file is being rendered.
379
     */
380 5
    public function getViewFile(): ?string
381
    {
382
        /** @psalm-suppress InvalidArrayOffset */
383 5
        return empty($this->viewFiles) ? null : end($this->viewFiles)['resolved'];
384
    }
385
386
    /**
387
     * Gets the placeholder signature.
388
     *
389
     * @return string The placeholder signature.
390
     */
391 49
    public function getPlaceholderSignature(): string
392
    {
393 49
        return $this->placeholderSignature;
394
    }
395
396
    /**
397
     * Renders a view.
398
     *
399
     * The view to be rendered can be specified in one of the following formats:
400
     *
401
     * - The name of the view starting with a slash to join the base path {@see getBasePath()} (e.g. "/site/index").
402
     * - The name of the view without the starting slash (e.g. "site/index"). The corresponding view file will be
403
     *   looked for under the {@see ViewContextInterface::getViewPath()} of the context set via {@see withContext()}.
404
     *   If the context instance was not set {@see withContext()}, it will be looked for under the directory containing
405
     *   the view currently being rendered (i.e., this happens when rendering a view within another view).
406
     *
407
     * @param string $view The view name.
408
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
409
     * file.
410
     * @param string|null The language to which will be used to localize the view.
411
     *
412
     * @throws RuntimeException If the view cannot be resolved.
413
     * @throws ViewNotFoundException If the view file does not exist.
414
     * @throws Throwable
415
     *
416
     * {@see renderFile()}
417
     *
418
     * @return string The rendering result.
419
     */
420 54
    public function render(string $view, array $parameters = [], ?string $language = null): string
421
    {
422 54
        $viewFile = $this->findTemplateFile($view);
423
424 53
        return $this->renderFile($viewFile, $parameters, $language);
425
    }
426
427
    /**
428
     * Renders a view file.
429
     *
430
     * If the theme was set {@see setTheme()}, it will try to render the themed version of the view file
431
     * as long as it is available.
432
     *
433
     * If the renderer was set {@see withRenderers()}, the method will use it to render the view file. Otherwise,
434
     * it will simply include the view file as a normal PHP file, capture its output and return it as a string.
435
     *
436
     * @param string $viewFile The full absolute path of the view file.
437
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
438
     * file.
439
     * @param string|null The language to which will be used to localize the view.
440
     *
441
     * @throws Throwable
442
     * @throws ViewNotFoundException If the view file does not exist
443
     *
444
     * @return string The rendering result.
445
     */
446 59
    public function renderFile(string $viewFile, array $parameters = [], ?string $language = null): string
447
    {
448 59
        $parameters = array_merge($this->state->getParameters(), $parameters);
449
450
        // TODO: these two match now
451 59
        $requestedFile = $viewFile;
452
453 59
        $theme = $this->getTheme();
454 59
        if ($theme !== null) {
455 1
            $viewFile = $theme->applyTo($viewFile);
456
        }
457
458 59
        if (is_file($viewFile)) {
459 58
            $viewFile = $this->localize($viewFile, $language);
460
        } else {
461 1
            throw new ViewNotFoundException("The view file \"$viewFile\" does not exist.");
462
        }
463
464 58
        $output = '';
465 58
        $this->viewFiles[] = [
466
            'resolved' => $viewFile,
467
            'requested' => $requestedFile,
468
        ];
469
470 58
        if ($this->beforeRender($viewFile, $parameters)) {
471 58
            $ext = pathinfo($viewFile, PATHINFO_EXTENSION);
472 58
            $renderer = $this->renderers[$ext] ?? new PhpTemplateRenderer();
473 58
            $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 57
            $output = $this->afterRender($viewFile, $parameters, $output);
475
        }
476
477 57
        array_pop($this->viewFiles);
478
479 57
        return $output;
480
    }
481
482
    /**
483
     * Returns the localized version of a specified file.
484
     *
485
     * The searching is based on the specified language code. In particular, a file with the same name will be looked
486
     * for under the subdirectory whose name is the same as the language code. For example, given the file
487
     * "path/to/view.php" and language 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 language 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 language codes are the same, the original file will be returned.
493
     *
494
     * @param string $file The original file
495
     * @param string|null $language The target language that the file should be localized to.
496
     * @param string|null $sourceLanguage The language 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 language codes are the same, the original file will be returned.
500
     */
501 61
    public function localize(string $file, ?string $language = null, ?string $sourceLanguage = null): string
502
    {
503 61
        $language = $language ?? $this->state->getLanguage();
504 61
        $sourceLanguage = $sourceLanguage ?? $this->sourceLanguage;
505
506 61
        if ($language === $sourceLanguage) {
507 58
            return $file;
508
        }
509
510 5
        $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
511
512 5
        if (is_file($desiredFile)) {
513 5
            return $desiredFile;
514
        }
515
516 1
        $language = substr($language, 0, 2);
517
518 1
        if ($language === $sourceLanguage) {
519 1
            return $file;
520
        }
521
522 1
        $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . 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 49
    public function clear(): void
530
    {
531 49
        $this->viewFiles = [];
532 49
        $this->state->clear();
533
    }
534
535
    /**
536
     * Creates an event that occurs before rendering.
537
     *
538
     * @param string $viewFile The view file to be rendered.
539
     * @param array $parameters The parameter array passed to the {@see renderFile()} method.
540
     *
541
     * @return StoppableEventInterface The stoppable event instance.
542
     */
543
    abstract protected function createBeforeRenderEvent(string $viewFile, array $parameters): StoppableEventInterface;
544
545
    /**
546
     * Creates an event that occurs after rendering.
547
     *
548
     * @param string $viewFile The view file being rendered.
549
     * @param array $parameters The parameter array passed to the {@see renderFile()} method.
550
     * @param string $result The rendering result of the view file.
551
     *
552
     * @return AfterRenderEventInterface The event instance.
553
     */
554
    abstract protected function createAfterRenderEvent(
555
        string $viewFile,
556
        array $parameters,
557
        string $result
558
    ): AfterRenderEventInterface;
559
560
    /**
561
     * This method is invoked right before {@see renderFile()} renders a view file.
562
     *
563
     * The default implementations will trigger the {@see \Yiisoft\View\Event\View\BeforeRender}
564
     * or {@see \Yiisoft\View\Event\WebView\BeforeRender} event. If you override this method,
565
     * make sure you call the parent implementation first.
566
     *
567
     * @param string $viewFile The view file to be rendered.
568
     * @param array $parameters The parameter array passed to the {@see renderFile()} method.
569
     *
570
     * @return bool Whether to continue rendering the view file.
571
     */
572 58
    private function beforeRender(string $viewFile, array $parameters): bool
573
    {
574 58
        $event = $this->createBeforeRenderEvent($viewFile, $parameters);
575 58
        $event = $this->eventDispatcher->dispatch($event);
576
        /** @var StoppableEventInterface $event */
577 58
        return !$event->isPropagationStopped();
578
    }
579
580
    /**
581
     * This method is invoked right after {@see renderFile()} renders a view file.
582
     *
583
     * The default implementations will trigger the {@see \Yiisoft\View\Event\View\AfterRender}
584
     * or {@see \Yiisoft\View\Event\WebView\AfterRender} event. If you override this method,
585
     * make sure you call the parent implementation first.
586
     *
587
     * @param string $viewFile The view file being rendered.
588
     * @param array $parameters The parameter array passed to the {@see renderFile()} method.
589
     * @param string $result The rendering result of the view file.
590
     *
591
     * @return string Updated output. It will be passed to {@see renderFile()} and returned.
592
     */
593 57
    private function afterRender(string $viewFile, array $parameters, string $result): string
594
    {
595 57
        $event = $this->createAfterRenderEvent($viewFile, $parameters, $result);
596
597
        /** @var AfterRenderEventInterface $event */
598 57
        $event = $this->eventDispatcher->dispatch($event);
599
600 57
        return $event->getResult();
601
    }
602
603 120
    private function setPlaceholderSalt(string $salt): void
604
    {
605 120
        $this->placeholderSignature = dechex(crc32($salt));
606
    }
607
608
    /**
609
     * Finds the view file based on the given view name.
610
     *
611
     * @param string $view The view name of the view file. Please refer to
612
     * {@see render()} on how to specify this parameter.
613
     *
614
     * @throws RuntimeException If a relative view name is given while there is no active context to determine the
615
     * corresponding view file.
616
     *
617
     * @return string The view file path. Note that the file may not exist.
618
     */
619 56
    private function findTemplateFile(string $view): string
620
    {
621 56
        if ($view !== '' && $view[0] === '/') {
622
            // path relative to basePath e.g. "/layouts/main"
623 51
            $file = $this->basePath . '/' . ltrim($view, '/');
624 7
        } elseif (($currentViewFile = $this->getRequestedViewFile()) !== null) {
625
            // path relative to currently rendered view
626 2
            $file = dirname($currentViewFile) . '/' . $view;
627 6
        } elseif ($this->context instanceof ViewContextInterface) {
628
            // path provided by context
629 5
            $file = $this->context->getViewPath() . '/' . $view;
630
        } else {
631 1
            throw new RuntimeException("Unable to resolve view file for view \"$view\": no active view context.");
632
        }
633
634 55
        if (pathinfo($file, PATHINFO_EXTENSION) !== '') {
635 47
            return $file;
636
        }
637
638 8
        $path = $file . '.' . $this->defaultExtension;
639
640 8
        if ($this->defaultExtension !== 'php' && !is_file($path)) {
641 1
            $path = $file . '.php';
642
        }
643
644 8
        return $path;
645
    }
646
647
    /**
648
     * @return string|null The requested view currently being rendered. `null` if no view file is being rendered.
649
     */
650 7
    private function getRequestedViewFile(): ?string
651
    {
652
        /** @psalm-suppress InvalidArrayOffset */
653 7
        return empty($this->viewFiles) ? null : end($this->viewFiles)['requested'];
654
    }
655
}
656