Passed
Pull Request — master (#91)
by
unknown
11:35
created

View::getBlock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
ccs 0
cts 0
cp 0
cc 2
nc 2
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\View;
6
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use Psr\Log\LoggerInterface;
9
use RuntimeException;
10
use Yiisoft\I18n\Locale;
11
use Yiisoft\View\Event\AfterRender;
12
use Yiisoft\View\Event\BeforeRender;
13
use Yiisoft\View\Event\PageBegin;
14
use Yiisoft\View\Event\PageEnd;
15
use Yiisoft\View\Exception\ViewNotFoundException;
16
17
/**
18
 * View represents a view object in the MVC pattern.
19
 *
20
 * View provides a set of methods (e.g. {@see View::render()}) for rendering purpose.
21
 *
22
 * For more details and usage information on View, see the [guide article on views](guide:structure-views).
23
 */
24
class View implements DynamicContentAwareInterface
25
{
26
    /**
27
     * @var string view path
28
     */
29
    private string $basePath;
30
31
    /**
32
     * @var array a list of named output blocks. The keys are the block names and the values are the corresponding block
33
     * content. You can call {@see beginBlock()} and {@see endBlock()} to capture small fragments of a view.
34
     * They can be later accessed somewhere else through this property.
35
     */
36
    private array $blocks;
37
38
    /**
39
     * @var ViewContextInterface|null the context under which the {@see renderFile()} method is being invoked.
40
     */
41
    private ?ViewContextInterface $context = null;
42
43
    /**
44
     * @var string the default view file extension. This will be appended to view file names if they don't have file
45
     * extensions.
46
     */
47
    private string $defaultExtension = 'php';
48
49
    /**
50
     * @var array custom parameters that are shared among view templates.
51
     */
52
    private array $defaultParameters = [];
53
54
    protected EventDispatcherInterface $eventDispatcher;
55
56
    /**
57
     * @var array a list of available renderers indexed by their corresponding supported file extensions. Each renderer
58
     * may be a view renderer object or the configuration for creating the renderer object. For example, the
59
     * following configuration enables the Twig view renderer:
60
     *
61
     * ```php
62
     * [
63
     *     'twig' => ['__class' => \Yiisoft\Yii\Twig\ViewRenderer::class],
64
     * ]
65
     * ```
66
     *
67
     * If no renderer is available for the given view file, the view file will be treated as a normal PHP and rendered
68
     * via PhpTemplateRenderer.
69
     */
70
    protected array $renderers = [];
71
72
    /**
73
     * @var Theme the theme object.
74
     */
75
    protected Theme $theme;
76
77
    private FragmentCacheInterface $fragmentCache;
78
79
    /**
80
     * @var DynamicContentAwareInterface[] a list of currently active dynamic content class instances.
81
     */
82
    private array $cacheStack = [];
83
84
    /**
85
     * @var string[] a list of placeholders for embedding dynamic contents.
86
     */
87
    private array $dynamicPlaceholders = [];
88
89
    private string $language = 'en';
90
91
    private LoggerInterface $logger;
92
93
    private string $sourceLanguage = 'en';
94
95
    /**
96
     * @var Locale|null source locale used to find localized view file.
97
     */
98
    private ?Locale $sourceLocale = null;
99
100
    private string $placeholderDynamicSignature;
101
102
    private string $placeholderSignature;
103
104
    /**
105
     * @var array the view files currently being rendered. There may be multiple view files being
106
     * rendered at a moment because one view may be rendered within another.
107
     */
108
    private array $viewFiles = [];
109
110
    public function __construct(string $basePath, Theme $theme, EventDispatcherInterface $eventDispatcher, FragmentCacheInterface $fragmentCache, LoggerInterface $logger)
111
    {
112
        $this->basePath = $basePath;
113
        $this->theme = $theme;
114
        $this->eventDispatcher = $eventDispatcher;
115
        $this->fragmentCache = $fragmentCache;
116
        $this->logger = $logger;
117 18
        $this->generatePlaceholderSignatures();
118
    }
119 18
120 18
    public function generatePlaceholderSignatures(): void
121 18
    {
122 18
        $this->placeholderDynamicSignature = \strtr(\base64_encode(\substr(\pack('Q', \rand(0, PHP_INT_MAX)), 0, 6)), '+/=', '012');
123 18
        $this->placeholderSignature = \dechex(\crc32(__DIR__));
124 18
    }
125
126 18
    public function setPlaceholderDynamicSignature(string $sign): void
127
    {
128 18
        $this->placeholderDynamicSignature = $sign;
129 18
    }
130
131 5
    public function setPlaceholderSignature(string $sign): void
132
    {
133 5
        $this->placeholderSignature = $sign;
134
    }
135
136
    public function getPlaceholderDynamicSignature(): string
137
    {
138
        return $this->placeholderDynamicSignature;
139
    }
140
141
    public function getPlaceholderSignature(): string
142
    {
143
        return $this->placeholderSignature;
144
    }
145
146
    public function getBasePath(): string
147
    {
148
        return $this->basePath;
149
    }
150
151
    public function setRenderers(array $renderers): void
152
    {
153
        $this->renderers = $renderers;
154
    }
155
156
    public function setSourceLanguage(string $language): void
157
    {
158
        $this->sourceLanguage = $language;
159
    }
160
161
    public function setLanguage(string $language): void
162
    {
163
        $this->language = $language;
164
    }
165
166
    public function setContext(ViewContextInterface $context): void
167
    {
168
        $this->context = $context;
169
    }
170
171
    public function getDefaultExtension(): string
172
    {
173
        return $this->defaultExtension;
174
    }
175
176 2
    public function setDefaultExtension(string $defaultExtension): void
177
    {
178 2
        $this->defaultExtension = $defaultExtension;
179 2
    }
180
181
    public function getDefaultParameters(): array
182
    {
183
        return $this->defaultParameters;
184
    }
185
186
    public function setDefaultParameters(array $defaultParameters): void
187
    {
188
        $this->defaultParameters = $defaultParameters;
189
    }
190
191
    /**
192
     * {@see blocks}
193
     *
194
     * @param string $id
195
     * @param string $value
196
     *
197
     * @return void
198
     */
199
    public function setBlocks(string $id, string $value): void
200
    {
201
        $this->blocks[$id] = $value;
202
    }
203
204
    /**
205
     * {@see blocks}
206
     *
207
     * @param string $value
208
     *
209
     * @return string
210
     */
211
    public function getBlock(string $value): string
212
    {
213
        if (isset($this->blocks[$value])) {
214
            return $this->blocks[$value];
215
        }
216
217
        throw new \InvalidArgumentException('Block: ' . $value . ' not found.');
218
    }
219
220
    /**
221
     * Renders a view.
222
     *
223
     * The view to be rendered can be specified in one of the following formats:
224
     *
225
     * - [path alias](guide:concept-aliases) (e.g. "@app/views/site/index");
226
     * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. The actual
227
     *   view file will be looked for under the [[Application::viewPath|view path]] of the application.
228
     * - absolute path within current module (e.g. "/site/index"): the view name starts with a single slash. The actual
229
     *   view file will be looked for under the [[Module::viewPath|view path]] of the [[Controller::module|current module]].
230
     * - relative view (e.g. "index"): the view name does not start with `@` or `/`. The corresponding view file will be
231
     *   looked for under the {@see ViewContextInterface::getViewPath()} of the view `$context`.
232
     *   If `$context` is not given, it will be looked for under the directory containing the view currently
233
     *   being rendered (i.e., this happens when rendering a view within another view).
234
     *
235
     * @param string $view the view name.
236
     * @param array $parameters the parameters (name-value pairs) that will be extracted and made available in the view
237
     * file.
238
     * @param ViewContextInterface|null $context the context to be assigned to the view and can later be accessed via
239
     * {@see context} in the view. If the context implements {@see ViewContextInterface}, it may also be used to locate
240 4
     * the view file corresponding to a relative view name.
241
     *
242 4
     * @return string the rendering result
243
     *
244 4
     * @throws \RuntimeException if the view cannot be resolved.
245
     * @throws ViewNotFoundException if the view file does not exist.
246
     * @throws \Throwable
247
     *
248
     * {@see renderFile()}
249
     */
250
    public function render(string $view, array $parameters = [], ?ViewContextInterface $context = null): string
251
    {
252
        $viewFile = $this->findTemplateFile($view, $context);
253
254
        return $this->renderFile($viewFile, $parameters, $context);
255
    }
256
257
    /**
258
     * Finds the view file based on the given view name.
259
     *
260
     * @param string $view the view name or the [path alias](guide:concept-aliases) of the view file. Please refer to
261 4
     * {@see render()} on how to specify this parameter.
262
     * @param ViewContextInterface|null $context the context to be assigned to the view and can later be accessed via
263 4
     * {@see context} in the view. If the context implements {@see ViewContextInterface}, it may also be used to locate the
264
     * view file corresponding to a relative view name.
265 4
     *
266 1
     * @throws \RuntimeException if a relative view name is given while there is no active context to determine the
267
     * corresponding view file.
268
     *
269 1
     * @return string the view file path. Note that the file may not exist.
270
     */
271 1
    protected function findTemplateFile(string $view, ?ViewContextInterface $context = null): string
272
    {
273
        if (\strncmp($view, '//', 2) === 0) {
274
            // path relative to basePath e.g. "//layouts/main"
275
            $file = $this->basePath . '/' . \ltrim($view, '/');
276 4
        } elseif ($context instanceof ViewContextInterface) {
277 1
            // path provided by context
278
            $file = $context->getViewPath() . '/' . $view;
279
        } elseif (($currentViewFile = $this->getRequestedViewFile()) !== false) {
280 3
            // path relative to currently rendered view
281
            $file = \dirname($currentViewFile) . '/' . $view;
0 ignored issues
show
Bug introduced by
It seems like $currentViewFile can also be of type true; however, parameter $path of dirname() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

281
            $file = \dirname(/** @scrutinizer ignore-type */ $currentViewFile) . '/' . $view;
Loading history...
282 3
        } else {
283
            throw new \RuntimeException("Unable to resolve view file for view '$view': no active view context.");
284
        }
285
286 3
        if (\pathinfo($file, PATHINFO_EXTENSION) !== '') {
287
            return $file;
288
        }
289
290
        $path = $file . '.' . $this->defaultExtension;
291
292
        if ($this->defaultExtension !== 'php' && !\is_file($path)) {
293
            $path = $file . '.php';
294
        }
295
296
        return $path;
297
    }
298
299
    /**
300
     * Renders a view file.
301
     *
302
     * If {@see theme} is enabled (not null), it will try to render the themed version of the view file as long as it
303
     * is available.
304
     *
305
     * If {@see renderers} is enabled (not null), the method will use it to render the view file. Otherwise,
306
     * it will simply include the view file as a normal PHP file, capture its output and
307
     * return it as a string.
308
     *
309
     * @param string $viewFile the view file. This can be either an absolute file path or an alias of it.
310 8
     * @param array $parameters the parameters (name-value pairs) that will be extracted and made available in the view
311
     * file.
312 8
     * @param ViewContextInterface|null $context the context that the view should use for rendering the view. If null,
313
     * existing {@see context} will be used.
314
     *
315 8
     * @return string the rendering result
316
     * @throws \Throwable
317 8
     *
318 8
     * @throws ViewNotFoundException if the view file does not exist
319
     */
320
    public function renderFile(string $viewFile, array $parameters = [], ?ViewContextInterface $context = null): string
321 8
    {
322 8
        $parameters = \array_merge($this->defaultParameters, $parameters);
323
324
        // TODO: these two match now
325
        $requestedFile = $viewFile;
326
327 8
        if (!empty($this->theme)) {
328 8
            $viewFile = $this->theme->applyTo($viewFile);
329
        }
330
331 8
        if (\is_file($viewFile)) {
332 8
            $viewFile = $this->localize($viewFile);
333 8
        } else {
334 8
            throw new ViewNotFoundException("The view file does not exist: $viewFile");
335
        }
336
337 8
        $oldContext = $this->context;
338 8
        if ($context !== null) {
339 8
            $this->context = $context;
340 8
        }
341 8
        $output = '';
342
        $this->viewFiles[] = [
343 8
            'resolved' => $viewFile,
344
            'requested' => $requestedFile,
345
        ];
346 8
347 8
        if ($this->beforeRender($viewFile, $parameters)) {
348
            $this->logger->debug("Rendering view file: $viewFile");
349 8
            $ext = \pathinfo($viewFile, PATHINFO_EXTENSION);
350
            $renderer = $this->renderers[$ext] ?? new PhpTemplateRenderer();
351
            $output = $renderer->render($this, $viewFile, $parameters);
352
353
            $output = $this->afterRender($viewFile, $parameters, $output);
354
        }
355
356
        \array_pop($this->viewFiles);
357
        $this->context = $oldContext;
358
359
        return $output;
360
    }
361
362
    /**
363
     * Returns the localized version of a specified file.
364
     *
365
     * The searching is based on the specified language code. In particular, a file with the same name will be looked
366
     * for under the subdirectory whose name is the same as the language code. For example, given the file
367
     * "path/to/view.php" and language code "zh-CN", the localized file will be looked for as path/to/zh-CN/view.php".
368
     * If the file is not found, it will try a fallback with just a language code that is "zh" i.e. "path/to/zh/view.php".
369
     * If it is not found as well the original file will be returned.
370 9
     *
371
     * If the target and the source language codes are the same, the original file will be returned.
372 9
     *
373 9
     * @param string $file the original file
374
     * @param string|null $language the target language that the file should be localized to.
375 9
     * @param string|null $sourceLanguage the language that the original file is in.
376 9
     *
377
     * @return string the matching localized file, or the original file if the localized version is not found.
378 1
     * If the target and the source language codes are the same, the original file will be returned.
379 1
     */
380 1
    public function localize(string $file, ?string $language = null, ?string $sourceLanguage = null): string
381
    {
382
        $language = $language ?? $this->language;
383
        $sourceLanguage = $sourceLanguage ?? $this->sourceLanguage;
384
385
        if ($language === $sourceLanguage) {
386
            return $file;
387
        }
388
        $desiredFile = \dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . \basename($file);
389
        if (\is_file($desiredFile)) {
390
            return $desiredFile;
391
        }
392
393
        $language = \substr($language, 0, 2);
394
        if ($language === $sourceLanguage) {
395 4
            return $file;
396
        }
397 4
        $desiredFile = \dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . \basename($file);
398
399
        return \is_file($desiredFile) ? $desiredFile : $file;
400
    }
401
402
    /**
403 1
     * @return string|bool the view file currently being rendered. False if no view file is being rendered.
404
     */
405 1
    public function getViewFile()
406
    {
407
        return empty($this->viewFiles) ? false : \end($this->viewFiles)['resolved'];
408
    }
409
410
    /**
411
     * @return string|bool the requested view currently being rendered. False if no view file is being rendered.
412
     */
413
    protected function getRequestedViewFile()
414
    {
415
        return empty($this->viewFiles) ? false : \end($this->viewFiles)['requested'];
416
    }
417
418
    /**
419 8
     * This method is invoked right before {@see renderFile()} renders a view file.
420
     *
421 8
     * The default implementation will trigger the {@see BeforeRender()} event. If you override this method, make sure
422 8
     * you call the parent implementation first.
423
     *
424 8
     * @param string $viewFile the view file to be rendered.
425
     * @param array $parameters the parameter array passed to the {@see render()} method.
426
     *
427
     * @return bool whether to continue rendering the view file.
428
     */
429
    public function beforeRender(string $viewFile, array $parameters): bool
430
    {
431
        $event = new BeforeRender($viewFile, $parameters);
432
        $event = $this->eventDispatcher->dispatch($event);
433
434
        return !$event->isPropagationStopped();
435
    }
436
437
    /**
438 8
     * This method is invoked right after {@see renderFile()} renders a view file.
439
     *
440 8
     * The default implementation will trigger the {@see AfterRender} event. If you override this method, make sure you
441 8
     * call the parent implementation first.
442
     *
443 8
     * @param string $viewFile the view file being rendered.
444
     * @param array $parameters the parameter array passed to the {@see render()} method.
445
     * @param string $output the rendering result of the view file. Updates to this parameter
446
     * will be passed back and returned by {@see renderFile()}.
447
     */
448
    public function afterRender(string $viewFile, array $parameters, &$output): string
449
    {
450
        $event = new AfterRender($viewFile, $parameters, $output);
451
        $event = $this->eventDispatcher->dispatch($event);
452
453
        return $event->getResult();
454
    }
455
456
    /**
457
     * Renders dynamic content returned by the given PHP statements.
458
     *
459
     * This method is mainly used together with content caching (fragment caching and page caching) when some portions
460
     * of the content (called *dynamic content*) should not be cached. The dynamic content must be returned by some PHP
461
     * statements. You can optionally pass additional parameters that will be available as variables in the PHP
462
     * statement:.
463
     *
464
     * ```php
465
     * <?= $this->renderDynamic('return foo($myVar);', [
466
     *     'myVar' => $model->getMyComplexVar(),
467
     * ]) ?>
468
     * ```
469
     *
470
     * @param string $statements the PHP statements for generating the dynamic content.
471
     * @param array $parameters the parameters (name-value pairs) that will be extracted and made
472
     * available in the $statement context. The parameters will be stored in the cache and be reused
473
     * each time $statement is executed. You should make sure, that these are safely serializable.
474
     *
475
     * @return string the placeholder of the dynamic content, or the dynamic content if there is no active content
476
     *                cache currently.
477
     */
478
    public function renderDynamic(string $statements, array $parameters = []): string
479
    {
480
        if (!empty($parameters)) {
481
            $statements = 'extract(unserialize(\'' . \str_replace(['\\', '\''], ['\\\\', '\\\''], \serialize($parameters)) . '\'));' . $statements;
482
        }
483
484
        if (!empty($this->cacheStack)) {
485
            $n = \count($this->dynamicPlaceholders);
486
            $placeholder = "<![CDATA[YII-DYNAMIC-$n-{$this->getPlaceholderDynamicSignature()}]]>";
487
            $this->addDynamicPlaceholder($placeholder, $statements);
488
489
            return $placeholder;
490
        }
491
492
        return $this->evaluateDynamicContent($statements);
493
    }
494
495
    /**
496
     * Get source locale.
497
     *
498
     * @return Locale
499
     */
500
    public function getSourceLocale(): Locale
501
    {
502
        if ($this->sourceLocale === null) {
503
            $this->sourceLocale = new Locale('en-US');
504
        }
505
506
        return $this->sourceLocale;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->sourceLocale could return the type null which is incompatible with the type-hinted return Yiisoft\I18n\Locale. Consider adding an additional type-check to rule them out.
Loading history...
507
    }
508
509
    /**
510
     * Set source locale.
511
     *
512
     * @param string $locale
513
     * @return self
514
     */
515
    public function setSourceLocale(string $locale): self
516
    {
517
        $this->sourceLocale = new Locale($locale);
518
519
        return $this;
520
    }
521
522
    /**
523
     * @return string[]
524
     */
525
    public function getDynamicPlaceholders(): array
526
    {
527
        return $this->dynamicPlaceholders;
528
    }
529
530
    /**
531
     * @param string[] $placeholders
532
     */
533
    public function setDynamicPlaceholders(array $placeholders): void
534
    {
535
        $this->dynamicPlaceholders = $placeholders;
536
    }
537
538
    public function addDynamicPlaceholder(string $name, string $statements): void
539
    {
540
        foreach ($this->cacheStack as $cache) {
541
            $cache->addDynamicPlaceholder($name, $statements);
542
        }
543
544
        $this->dynamicPlaceholders[$name] = $statements;
545
    }
546
547
    /**
548
     * Evaluates the given PHP statements.
549
     *
550
     * This method is mainly used internally to implement dynamic content feature.
551
     *
552
     * @param string $statements the PHP statements to be evaluated.
553
     *
554
     * @return mixed the return value of the PHP statements.
555
     */
556
    public function evaluateDynamicContent(string $statements)
557
    {
558
        return eval($statements);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
559
    }
560
561
    /**
562
     * Returns a list of currently active dynamic content class instances.
563
     *
564
     * @return DynamicContentAwareInterface[] class instances supporting dynamic contents.
565
     */
566
    public function getDynamicContents(): array
567
    {
568
        return $this->cacheStack;
569
    }
570
571
    /**
572
     * Adds a class instance supporting dynamic contents to the end of a list of currently active dynamic content class
573
     * instances.
574
     *
575
     * @param DynamicContentAwareInterface $instance class instance supporting dynamic contents.
576
     *
577
     * @return void
578
     */
579
    public function pushDynamicContent(DynamicContentAwareInterface $instance): void
580
    {
581
        $this->cacheStack[] = $instance;
582
    }
583
584 4
    /**
585
     * Removes a last class instance supporting dynamic contents from a list of currently active dynamic content class
586 4
     * instances.
587 4
     *
588
     * @return void
589 4
     */
590 4
    public function popDynamicContent(DynamicContentAwareInterface $instance = null): void
591
    {
592
        $popInstance = \array_pop($this->cacheStack);
593
        if ($instance !== null && $instance !== $popInstance) {
594
            throw new RuntimeException('Poped the element is not an expected element.');
595
        }
596
    }
597
598
    /**
599
     * Marks the beginning of a page.
600
     *
601
     * @return void
602
     */
603 1
    public function beginPage(): void
604
    {
605
        \ob_start();
606
        \ob_implicit_flush(0);
607
        $this->eventDispatcher->dispatch(new PageBegin($this->getViewFile()));
0 ignored issues
show
Bug introduced by
It seems like $this->getViewFile() can also be of type boolean; however, parameter $file of Yiisoft\View\Event\PageBegin::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

607
        $this->eventDispatcher->dispatch(new PageBegin(/** @scrutinizer ignore-type */ $this->getViewFile()));
Loading history...
608
    }
609
610
    /**
611
     * Marks the ending of a page.
612
     *
613
     * @return void
614
     */
615
    public function endPage(): void
616
    {
617
        $this->eventDispatcher->dispatch(new PageEnd($this->getViewFile()));
0 ignored issues
show
Bug introduced by
It seems like $this->getViewFile() can also be of type boolean; however, parameter $file of Yiisoft\View\Event\PageEnd::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

617
        $this->eventDispatcher->dispatch(new PageEnd(/** @scrutinizer ignore-type */ $this->getViewFile()));
Loading history...
618
        \ob_end_flush();
619
    }
620
621
    /**
622
     * @param string $id
623
     * @param array $params
624
     * @param string[] $vars
625
     * @return FragmentCache|null
626
     */
627
    public function beginCache(string $id, array $params = [], array $vars = []): ?FragmentCache
628
    {
629
        $fc = $this->fragmentCache->beginCache($this, $id, $params, $vars);
630
        if ($fc->getStatus() === FragmentCacheInterface::STATUS_IN_CACHE) {
631
            $fc->endCache();
632
            return null;
633
        }
634
        return $fc;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $fc returns the type Yiisoft\View\FragmentCacheInterface which includes types incompatible with the type-hinted return Yiisoft\View\FragmentCache|null.
Loading history...
635
    }
636
}
637