Passed
Pull Request — master (#91)
by Alexander
10:25
created

View::setPlaceholderSalt()   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
rs 10
ccs 2
cts 2
cp 1
cc 1
nc 1
nop 1
crap 1
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 __clone()
121 18
    {
122 18
        $this->generatePlaceholderSignatures();
123 18
    }
124 18
125
    public function generatePlaceholderSignatures(): void
126 18
    {
127
        $this->placeholderDynamicSignature = \strtr(\base64_encode(\substr(\pack('Q', \rand(0, PHP_INT_MAX)), 0, 6)), '+/=', '012');
128 18
        $this->placeholderSignature = \dechex(\crc32(__DIR__));
129 18
    }
130
131 5
    public function setPlaceholderDynamicSignature(string $sign): void
132
    {
133 5
        $this->placeholderDynamicSignature = $sign;
134
    }
135
136
    public function setPlaceholderSignature(string $sign): void
137
    {
138
        $this->placeholderSignature = $sign;
139
    }
140
141
    public function getPlaceholderDynamicSignature(): string
142
    {
143
        return $this->placeholderDynamicSignature;
144
    }
145
146
    public function getPlaceholderSignature(): string
147
    {
148
        return $this->placeholderSignature;
149
    }
150
151
    public function getBasePath(): string
152
    {
153
        return $this->basePath;
154
    }
155
156
    public function setRenderers(array $renderers): void
157
    {
158
        $this->renderers = $renderers;
159
    }
160
161
    public function setSourceLanguage(string $language): void
162
    {
163
        $this->sourceLanguage = $language;
164
    }
165
166
    public function setLanguage(string $language): void
167
    {
168
        $this->language = $language;
169
    }
170
171
    public function setContext(ViewContextInterface $context): void
172
    {
173
        $this->context = $context;
174
    }
175
176 2
    public function getDefaultExtension(): string
177
    {
178 2
        return $this->defaultExtension;
179 2
    }
180
181
    public function setDefaultExtension(string $defaultExtension): void
182
    {
183
        $this->defaultExtension = $defaultExtension;
184
    }
185
186
    public function getDefaultParameters(): array
187
    {
188
        return $this->defaultParameters;
189
    }
190
191
    public function setDefaultParameters(array $defaultParameters): void
192
    {
193
        $this->defaultParameters = $defaultParameters;
194
    }
195
196
    /**
197
     * {@see blocks}
198
     *
199
     * @param string $id
200
     * @param string $value
201
     *
202
     * @return void
203
     */
204
    public function setBlocks(string $id, string $value): void
205
    {
206
        $this->blocks[$id] = $value;
207
    }
208
209
    /**
210
     * {@see blocks}
211
     *
212
     * @param string $value
213
     *
214
     * @return string
215
     */
216
    public function getBlock(string $value): string
217
    {
218
        if (isset($this->blocks[$value])) {
219
            return $this->blocks[$value];
220
        }
221
222
        throw new \InvalidArgumentException('Block: ' . $value . ' not found.');
223
    }
224
225
    /**
226
     * Renders a view.
227
     *
228
     * The view to be rendered can be specified in one of the following formats:
229
     *
230
     * - [path alias](guide:concept-aliases) (e.g. "@app/views/site/index");
231
     * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. The actual
232
     *   view file will be looked for under the [[Application::viewPath|view path]] of the application.
233
     * - absolute path within current module (e.g. "/site/index"): the view name starts with a single slash. The actual
234
     *   view file will be looked for under the [[Module::viewPath|view path]] of the [[Controller::module|current module]].
235
     * - relative view (e.g. "index"): the view name does not start with `@` or `/`. The corresponding view file will be
236
     *   looked for under the {@see ViewContextInterface::getViewPath()} of the view `$context`.
237
     *   If `$context` is not given, it will be looked for under the directory containing the view currently
238
     *   being rendered (i.e., this happens when rendering a view within another view).
239
     *
240 4
     * @param string $view the view name.
241
     * @param array $parameters the parameters (name-value pairs) that will be extracted and made available in the view
242 4
     * file.
243
     * @param ViewContextInterface|null $context the context to be assigned to the view and can later be accessed via
244 4
     * {@see context} in the view. If the context implements {@see ViewContextInterface}, it may also be used to locate
245
     * the view file corresponding to a relative view name.
246
     *
247
     * @return string the rendering result
248
     *
249
     * @throws \RuntimeException if the view cannot be resolved.
250
     * @throws ViewNotFoundException if the view file does not exist.
251
     * @throws \Throwable
252
     *
253
     * {@see renderFile()}
254
     */
255
    public function render(string $view, array $parameters = [], ?ViewContextInterface $context = null): string
256
    {
257
        $viewFile = $this->findTemplateFile($view, $context);
258
259
        return $this->renderFile($viewFile, $parameters, $context);
260
    }
261 4
262
    /**
263 4
     * Finds the view file based on the given view name.
264
     *
265 4
     * @param string $view the view name or the [path alias](guide:concept-aliases) of the view file. Please refer to
266 1
     * {@see render()} on how to specify this parameter.
267
     * @param ViewContextInterface|null $context the context to be assigned to the view and can later be accessed via
268
     * {@see context} in the view. If the context implements {@see ViewContextInterface}, it may also be used to locate the
269 1
     * view file corresponding to a relative view name.
270
     *
271 1
     * @throws \RuntimeException if a relative view name is given while there is no active context to determine the
272
     * corresponding view file.
273
     *
274
     * @return string the view file path. Note that the file may not exist.
275
     */
276 4
    protected function findTemplateFile(string $view, ?ViewContextInterface $context = null): string
277 1
    {
278
        if (\strncmp($view, '//', 2) === 0) {
279
            // path relative to basePath e.g. "//layouts/main"
280 3
            $file = $this->basePath . '/' . \ltrim($view, '/');
281
        } elseif ($context instanceof ViewContextInterface) {
282 3
            // path provided by context
283
            $file = $context->getViewPath() . '/' . $view;
284
        } elseif (($currentViewFile = $this->getRequestedViewFile()) !== false) {
285
            // path relative to currently rendered view
286 3
            $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

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

612
        $this->eventDispatcher->dispatch(new PageBegin(/** @scrutinizer ignore-type */ $this->getViewFile()));
Loading history...
613
    }
614
615
    /**
616
     * Marks the ending of a page.
617
     *
618
     * @return void
619
     */
620
    public function endPage(): void
621
    {
622
        $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

622
        $this->eventDispatcher->dispatch(new PageEnd(/** @scrutinizer ignore-type */ $this->getViewFile()));
Loading history...
623
        \ob_end_flush();
624
    }
625
626
    /**
627
     * @param string $id
628
     * @param array $params
629
     * @param string[] $vars
630
     * @return FragmentCache|null
631
     */
632
    public function beginCache(string $id, array $params = [], array $vars = []): ?FragmentCache
633
    {
634
        $fc = $this->fragmentCache->beginCache($this, $id, $params, $vars);
635
        if ($fc->getStatus() === FragmentCacheInterface::STATUS_IN_CACHE) {
636
            $fc->endCache();
637
            return null;
638
        }
639
        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...
640
    }
641
}
642