Passed
Pull Request — master (#243)
by
unknown
13:06
created

ViewTrait   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 634
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 55
eloc 122
c 1
b 0
f 0
dl 0
loc 634
ccs 151
cts 151
cp 1
rs 6

38 Methods

Rating   Name   Duplication   Size   Complexity  
A render() 0 5 1
A withRenderers() 0 5 1
A withContext() 0 6 1
A withDefaultExtension() 0 3 1
A withFallbackExtension() 0 5 1
A withBasePath() 0 5 1
A withLocale() 0 6 1
A setLocale() 0 4 1
A withContextPath() 0 3 1
A withSourceLocale() 0 5 1
A withPlaceholderSalt() 0 5 1
A getFallbackExtensions() 0 3 1
A getDefaultExtension() 0 3 1
A getTheme() 0 3 1
A getBasePath() 0 3 1
A getLocale() 0 3 1
A setTheme() 0 4 1
A setParameter() 0 4 1
A localize() 0 23 5
A hasBlock() 0 3 1
A getBlock() 0 3 1
A afterRender() 0 8 1
A removeParameter() 0 4 1
A getViewFile() 0 4 2
A addToParameter() 0 4 1
A getParameter() 0 3 1
A setBlock() 0 4 1
A clear() 0 5 1
A hasParameter() 0 3 1
A beforeRender() 0 6 1
A getPlaceholderSignature() 0 3 1
A setParameters() 0 4 1
A getRequestedViewFile() 0 4 2
B findTemplateFile() 0 27 9
A setPlaceholderSalt() 0 3 1
A renderFile() 0 34 4
A removeBlock() 0 4 1
A withTheme() 0 6 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
use Yiisoft\View\State\LocaleState;
15
use Yiisoft\View\State\ThemeState;
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
    /**
43
     * @var string[]
44
     */
45
    private array $fallbackExtensions = [self::PHP_EXTENSION];
46
47
    /**
48
     * @var array A list of available renderers indexed by their corresponding
49
     * supported file extensions.
50
     * @psalm-var array<string, TemplateRendererInterface>
51
     */
52
    private array $renderers = [];
53
54
    /**
55
     * @var array The view files currently being rendered. There may be multiple view files being
56
     * rendered at a moment because one view may be rendered within another.
57
     *
58
     * @psalm-var array<array-key, array<string, string>>
59
     */
60
    private array $viewFiles = [];
61
62
    /**
63
     * Returns a new instance with specified base path to the view directory.
64
     *
65
     * @param string $basePath The base path to the view directory.
66
     */
67 2
    public function withBasePath(string $basePath): static
68
    {
69 2
        $new = clone $this;
70 2
        $new->basePath = $basePath;
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\View\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 3
    public function withRenderers(array $renderers): static
90
    {
91 3
        $new = clone $this;
92 3
        $new->renderers = $renderers;
93 3
        return $new;
94
    }
95
96
    /**
97
     * Returns a new instance with the specified source locale.
98
     *
99
     * @param string $locale The source locale.
100
     */
101 3
    public function withSourceLocale(string $locale): static
102
    {
103 3
        $new = clone $this;
104 3
        $new->sourceLocale = $locale;
105 3
        return $new;
106
    }
107
108
    /**
109
     * Returns a new instance with the specified default view file extension.
110
     *
111
     * @param string $defaultExtension The default view file extension. Default is {@see ViewInterface::PHP_EXTENSION}.
112
     * This will be appended to view file names if they don't have file extensions.
113
     * @deprecated Since 8.0.1 and will be removed in the next major version. Use {@see withFallbackExtension()} instead.
114
     */
115 8
    public function withDefaultExtension(string $defaultExtension): static
116
    {
117 8
        return $this->withFallbackExtension($defaultExtension);
118
    }
119
120
    /**
121
     * Returns a new instance with the specified fallback view file extension.
122
     *
123
     * @param string $fallbackExtension The fallback view file extension. Default is {@see ViewInterface::PHP_EXTENSION}.
124
     * This will be appended to view file names if they don't exist.
125
     */
126 8
    public function withFallbackExtension(string $fallbackExtension, string ...$otherFallbacks): static
127
    {
128 8
        $new = clone $this;
129 8
        $new->fallbackExtensions = [$fallbackExtension, ...array_values($otherFallbacks)];
0 ignored issues
show
Documentation Bug introduced by
array($fallbackExtension...alues($otherFallbacks)) is of type array<integer,array|string>, but the property $fallbackExtensions was declared to be of type string[]. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
130 8
        return $new;
131
    }
132
133
    /**
134
     * Returns a new instance with the specified view context instance.
135
     *
136
     * @param ViewContextInterface $context The context under which the {@see renderFile()} method is being invoked.
137
     */
138 9
    public function withContext(ViewContextInterface $context): static
139
    {
140 9
        $new = clone $this;
141 9
        $new->context = $context;
142 9
        $new->viewFiles = [];
143 9
        return $new;
144
    }
145
146
    /**
147
     * Returns a new instance with the specified view context path.
148
     *
149
     * @param string $path The context path under which the {@see renderFile()} method is being invoked.
150
     */
151 2
    public function withContextPath(string $path): static
152
    {
153 2
        return $this->withContext(new ViewContext($path));
154
    }
155
156
    /**
157
     * Returns a new instance with specified salt for the placeholder signature {@see getPlaceholderSignature()}.
158
     *
159
     * @param string $salt The placeholder salt.
160
     */
161 2
    public function withPlaceholderSalt(string $salt): static
162
    {
163 2
        $new = clone $this;
164 2
        $new->setPlaceholderSalt($salt);
165 2
        return $new;
166
    }
167
168
    /**
169
     * Set the specified locale code.
170
     *
171
     * @param string $locale The locale code.
172
     */
173 3
    public function setLocale(string $locale): static
174
    {
175 3
        $this->localeState->setLocale($locale);
176 3
        return $this;
177
    }
178
179
    /**
180
     * Set the specified locale code.
181
     *
182
     * @param string $locale The locale code.
183
     */
184 3
    public function withLocale(string $locale): static
185
    {
186 3
        $new = clone $this;
187 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...
188
189 3
        return $new;
190
    }
191
192
    /**
193
     * Get the specified locale code.
194
     *
195
     * @return string The locale code.
196
     */
197 1
    public function getLocale(): string
198
    {
199 1
        return $this->localeState->getLocale();
200
    }
201
202
    /**
203
     * Gets the base path to the view directory.
204
     *
205
     * @return string The base view path.
206
     */
207 2
    public function getBasePath(): string
208
    {
209 2
        return $this->basePath;
210
    }
211
212
    /**
213
     * Gets the default view file extension.
214
     *
215
     * @return string The default view file extension.
216
     * @deprecated Since 8.0.1 and will be removed in the next major version. Use {@see getFallbackExtensions()} instead.
217
     */
218 1
    public function getDefaultExtension(): string
219
    {
220 1
        return $this->getFallbackExtensions()[0];
221
    }
222
223
    /**
224
     * Gets the fallback view file extension.
225
     *
226
     * @return string[] The fallback view file extension.
227
     */
228 1
    public function getFallbackExtensions(): array
229
    {
230 1
        return $this->fallbackExtensions;
231
    }
232
233
    /**
234
     * Gets the theme instance, or `null` if no theme has been set.
235
     *
236
     * @return Theme|null The theme instance, or `null` if no theme has been set.
237
     */
238 65
    public function getTheme(): ?Theme
239
    {
240 65
        return $this->themeState->getTheme();
241
    }
242
243
    /**
244
     * Set the specified theme instance.
245
     *
246
     * @param Theme|null $theme The theme instance or `null` for reset theme.
247
     */
248 1
    public function setTheme(?Theme $theme): static
249
    {
250 1
        $this->themeState->setTheme($theme);
251 1
        return $this;
252
    }
253
254 2
    public function withTheme(?Theme $theme): static
255
    {
256 2
        $new = clone $this;
257 2
        $new->themeState = new ThemeState($theme);
0 ignored issues
show
Bug Best Practice introduced by
The property themeState does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
258
259 2
        return $new;
260
    }
261
262
    /**
263
     * Sets a common parameters that is accessible in all view templates.
264
     *
265
     * @param array $parameters Parameters that are common for all view templates.
266
     *
267
     * @psalm-param array<string, mixed> $parameters
268
     *
269
     * @see setParameter()
270
     */
271 5
    public function setParameters(array $parameters): static
272
    {
273 5
        $this->state->setParameters($parameters);
274 5
        return $this;
275
    }
276
277
    /**
278
     * Sets a common parameter that is accessible in all view templates.
279
     *
280
     * @param string $id The unique identifier of the parameter.
281
     * @param mixed $value The value of the parameter.
282
     */
283 12
    public function setParameter(string $id, mixed $value): static
284
    {
285 12
        $this->state->setParameter($id, $value);
286 12
        return $this;
287
    }
288
289
    /**
290
     * Add values to end of common array parameter. If specified parameter does not exist or him is not array,
291
     * then parameter will be added as empty array.
292
     *
293
     * @param string $id The unique identifier of the parameter.
294
     * @param mixed ...$value Value(s) for add to end of array parameter.
295
     *
296
     * @throws InvalidArgumentException When specified parameter already exists and is not an array.
297
     */
298 6
    public function addToParameter(string $id, mixed ...$value): static
299
    {
300 6
        $this->state->addToParameter($id, ...$value);
301 5
        return $this;
302
    }
303
304
    /**
305
     * Removes a common parameter.
306
     *
307
     * @param string $id The unique identifier of the parameter.
308
     */
309 3
    public function removeParameter(string $id): static
310
    {
311 3
        $this->state->removeParameter($id);
312 3
        return $this;
313
    }
314
315
    /**
316
     * Gets a common parameter value by ID.
317
     *
318
     * @param string $id The unique identifier of the parameter.
319
     * @param mixed $default The default value to be returned if the specified parameter does not exist.
320
     *
321
     * @throws InvalidArgumentException If specified parameter does not exist and not passed default value.
322
     *
323
     * @return mixed The value of the parameter.
324
     */
325 8
    public function getParameter(string $id)
326
    {
327 8
        return call_user_func_array([$this->state, 'getParameter'], func_get_args());
328
    }
329
330
    /**
331
     * Checks the existence of a common parameter by ID.
332
     *
333
     * @param string $id The unique identifier of the parameter.
334
     *
335
     * @return bool Whether a custom parameter that is common for all view templates exists.
336
     */
337 5
    public function hasParameter(string $id): bool
338
    {
339 5
        return $this->state->hasParameter($id);
340
    }
341
342
    /**
343
     * Sets a content block.
344
     *
345
     * @param string $id The unique identifier of the block.
346
     * @param string $content The content of the block.
347
     */
348 7
    public function setBlock(string $id, string $content): static
349
    {
350 7
        $this->state->setBlock($id, $content);
351 7
        return $this;
352
    }
353
354
    /**
355
     * Removes a content block.
356
     *
357
     * @param string $id The unique identifier of the block.
358
     */
359 3
    public function removeBlock(string $id): static
360
    {
361 3
        $this->state->removeBlock($id);
362 3
        return $this;
363
    }
364
365
    /**
366
     * Gets content of the block by ID.
367
     *
368
     * @param string $id The unique identifier of the block.
369
     *
370
     * @return string The content of the block.
371
     */
372 2
    public function getBlock(string $id): string
373
    {
374 2
        return $this->state->getBlock($id);
375
    }
376
377
    /**
378
     * Checks the existence of a content block by ID.
379
     *
380
     * @param string $id The unique identifier of the block.
381
     *
382
     * @return bool Whether a content block exists.
383
     */
384 5
    public function hasBlock(string $id): bool
385
    {
386 5
        return $this->state->hasBlock($id);
387
    }
388
389
    /**
390
     * Gets the view file currently being rendered.
391
     *
392
     * @return string|null The view file currently being rendered. `null` if no view file is being rendered.
393
     */
394 5
    public function getViewFile(): ?string
395
    {
396
        /** @psalm-suppress InvalidArrayOffset */
397 5
        return empty($this->viewFiles) ? null : end($this->viewFiles)['resolved'];
398
    }
399
400
    /**
401
     * Gets the placeholder signature.
402
     *
403
     * @return string The placeholder signature.
404
     */
405 49
    public function getPlaceholderSignature(): string
406
    {
407 49
        return $this->placeholderSignature;
408
    }
409
410
    /**
411
     * Renders a view.
412
     *
413
     * The view to be rendered can be specified in one of the following formats:
414
     *
415
     * - The name of the view starting with a slash to join the base path {@see getBasePath()} (e.g. "/site/index").
416
     * - The name of the view without the starting slash (e.g. "site/index"). The corresponding view file will be
417
     *   looked for under the {@see ViewContextInterface::getViewPath()} of the context set via {@see withContext()}.
418
     *   If the context instance was not set {@see withContext()}, it will be looked for under the directory containing
419
     *   the view currently being rendered (i.e., this happens when rendering a view within another view).
420
     *
421
     * @param string $view The view name.
422
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
423
     * file.
424
     *
425
     * @throws RuntimeException If the view cannot be resolved.
426
     * @throws ViewNotFoundException If the view file does not exist.
427
     * @throws Throwable
428
     *
429
     * {@see renderFile()}
430
     *
431
     * @return string The rendering result.
432
     */
433 60
    public function render(string $view, array $parameters = []): string
434
    {
435 60
        $viewFile = $this->findTemplateFile($view);
436
437 59
        return $this->renderFile($viewFile, $parameters);
438
    }
439
440
    /**
441
     * Renders a view file.
442
     *
443
     * If the theme was set {@see setTheme()}, it will try to render the themed version of the view file
444
     * as long as it's available.
445
     *
446
     * If the renderer was set {@see withRenderers()}, the method will use it to render the view file. Otherwise,
447
     * it will simply include the view file as a normal PHP file, capture its output and return it as a string.
448
     *
449
     * @param string $viewFile The full absolute path of the view file.
450
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
451
     * file.
452
     *
453
     * @throws Throwable
454
     * @throws ViewNotFoundException If the view file doesn't exist
455
     *
456
     * @return string The rendering result.
457
     */
458 64
    public function renderFile(string $viewFile, array $parameters = []): string
459
    {
460 64
        $parameters = array_merge($this->state->getParameters(), $parameters);
461
462
        // TODO: these two match now
463 64
        $requestedFile = $viewFile;
464
465 64
        $theme = $this->getTheme();
466 64
        if ($theme !== null) {
467 1
            $viewFile = $theme->applyTo($viewFile);
468
        }
469
470 64
        if (is_file($viewFile)) {
471 63
            $viewFile = $this->localize($viewFile);
472
        } else {
473 1
            throw new ViewNotFoundException("The view file \"$viewFile\" does not exist.");
474
        }
475
476 63
        $output = '';
477 63
        $this->viewFiles[] = [
478 63
            'resolved' => $viewFile,
479 63
            'requested' => $requestedFile,
480 63
        ];
481
482 63
        if ($this->beforeRender($viewFile, $parameters)) {
483 63
            $ext = pathinfo($viewFile, PATHINFO_EXTENSION);
484 63
            $renderer = $this->renderers[$ext] ?? new PhpTemplateRenderer();
485 63
            $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

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