Passed
Pull Request — master (#143)
by Alexander
02:07
created

BaseView::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 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
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\Log\LoggerInterface;
10
use RuntimeException;
11
use Throwable;
12
use Yiisoft\View\Event\AfterRender;
13
use Yiisoft\View\Event\BeforeRender;
14
use Yiisoft\View\Exception\ViewNotFoundException;
15
16
use function count;
17
use function dirname;
18
19
/**
20
 * @internal Base class for {@see View} and {@see WebView}.
21
 */
22
abstract class BaseView implements DynamicContentAwareInterface
23
{
24
    protected EventDispatcherInterface $eventDispatcher;
25
26
    private LoggerInterface $logger;
27
28
    /**
29
     * @var string Base view path.
30
     */
31
    private string $basePath;
32
33
    /**
34
     * @var Theme|null The theme object.
35
     */
36
    private ?Theme $theme = null;
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
    private string $placeholderSignature;
44
45
    /**
46
     * @var array A list of available renderers indexed by their corresponding supported file extensions. Each renderer
47
     * may be a view renderer object or the configuration for creating the renderer object. For example, the
48
     * following configuration enables the Twig view renderer:
49
     *
50
     * ```php
51
     * [
52
     *     'twig' => ['class' => \Yiisoft\Yii\Twig\ViewRenderer::class],
53
     * ]
54
     * ```
55
     *
56
     * If no renderer is available for the given view file, the view file will be treated as a normal PHP and rendered
57
     * via PhpTemplateRenderer.
58
     */
59
    private array $renderers = [];
60
61
    private string $language = 'en';
62
    private string $sourceLanguage = 'en';
63
64
    /**
65
     * @var string The default view file extension. This will be appended to view file names if they don't have file
66
     * extensions.
67
     */
68
    private string $defaultExtension = 'php';
69
70
    /**
71
     * @var array Custom parameters that are shared among view templates.
72
     */
73
    private array $defaultParameters = [];
74
75
    /**
76
     * @var array A list of named output blocks. The keys are the block names and the values are the corresponding block
77
     * content. You can call {@see beginBlock()} and {@see endBlock()} to capture small fragments of a view.
78
     * They can be later accessed somewhere else through this property.
79
     */
80
    private array $blocks;
81
82
    /**
83
     * @var array The view files currently being rendered. There may be multiple view files being
84
     * rendered at a moment because one view may be rendered within another.
85
     */
86
    private array $viewFiles = [];
87
88
    /**
89
     * @var DynamicContentAwareInterface[] A list of currently active dynamic content class instances.
90
     */
91
    private array $cacheStack = [];
92
93
    /**
94
     * @var array A list of placeholders for embedding dynamic contents.
95
     */
96
    private array $dynamicPlaceholders = [];
97
98 41
    public function __construct(string $basePath, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger)
99
    {
100 41
        $this->basePath = $basePath;
101 41
        $this->eventDispatcher = $eventDispatcher;
102 41
        $this->logger = $logger;
103 41
        $this->setPlaceholderSalt(__DIR__);
104 41
    }
105
106
    /**
107
     * @return static
108
     */
109 1
    public function withTheme(Theme $theme): self
110
    {
111 1
        $new = clone $this;
112 1
        $new->theme = $theme;
113 1
        return $new;
114
    }
115
116
    public function withRenderers(array $renderers): self
117
    {
118
        $new = clone $this;
119
        $new->renderers = $renderers;
120
        return $new;
121
    }
122
123
    public function withLanguage(string $language): self
124
    {
125
        $new = clone $this;
126
        $new->language = $language;
127
        return $new;
128
    }
129
130
    public function withSourceLanguage(string $language): self
131
    {
132
        $new = clone $this;
133
        $new->sourceLanguage = $language;
134
        return $new;
135
    }
136
137 1
    public function withContext(ViewContextInterface $context): self
138
    {
139 1
        $new = clone $this;
140 1
        $new->context = $context;
141 1
        return $new;
142
    }
143
144
    public function getBasePath(): string
145
    {
146
        return $this->basePath;
147
    }
148
149 17
    public function getPlaceholderSignature(): string
150
    {
151 17
        return $this->placeholderSignature;
152
    }
153
154 41
    public function setPlaceholderSalt(string $salt): void
155
    {
156 41
        $this->placeholderSignature = dechex(crc32($salt));
157 41
    }
158
159
    public function getDefaultExtension(): string
160
    {
161
        return $this->defaultExtension;
162
    }
163
164
    public function withDefaultExtension(string $defaultExtension): self
165
    {
166
        $new = clone $this;
167
        $new->defaultExtension = $defaultExtension;
168
        return $new;
169
    }
170
171
    public function getDefaultParameters(): array
172
    {
173
        return $this->defaultParameters;
174
    }
175
176 2
    public function withDefaultParameters(array $defaultParameters): self
177
    {
178 2
        $new = clone $this;
179 2
        $new->defaultParameters = $defaultParameters;
180 2
        return $new;
181
    }
182
183
    /**
184
     * {@see blocks}
185
     */
186
    public function setBlock(string $id, string $value): void
187
    {
188
        $this->blocks[$id] = $value;
189
    }
190
191
    /**
192
     * {@see blocks}
193
     */
194
    public function removeBlock(string $id): void
195
    {
196
        unset($this->blocks[$id]);
197
    }
198
199
    /**
200
     * {@see blocks}
201
     */
202
    public function getBlock(string $id): string
203
    {
204
        if (isset($this->blocks[$id])) {
205
            return $this->blocks[$id];
206
        }
207
208
        throw new InvalidArgumentException('Block: "' . $id . '" not found.');
209
    }
210
211
    /**
212
     * {@see blocks}
213
     */
214
    public function hasBlock(string $id): bool
215
    {
216
        return isset($this->blocks[$id]);
217
    }
218
219
    /**
220
     * @return string|null The view file currently being rendered. `null` if no view file is being rendered.
221
     */
222 16
    public function getViewFile(): ?string
223
    {
224 16
        return empty($this->viewFiles) ? null : end($this->viewFiles)['resolved'];
225
    }
226
227
    /**
228
     * Renders a view.
229
     *
230
     * The view to be rendered can be specified in one of the following formats:
231
     *
232
     * - [path alias](guide:concept-aliases) (e.g. "@app/views/site/index");
233
     * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. The actual
234
     *   view file will be looked for under the [[Application::viewPath|view path]] of the application.
235
     * - absolute path within current module (e.g. "/site/index"): the view name starts with a single slash. The actual
236
     *   view file will be looked for under the [[Module::viewPath|view path]]
237
     *   of the [[Controller::module|current module]].
238
     * - relative view (e.g. "index"): the view name does not start with `@` or `/`. The corresponding view file will be
239
     *   looked for under the {@see ViewContextInterface::getViewPath()} of the {@see View::$context}.
240
     *   If {@see View::$context} is not set, it will be looked for under the directory containing the view currently
241
     *   being rendered (i.e., this happens when rendering a view within another view).
242
     *
243
     * @param string $view the view name.
244
     * @param array $parameters the parameters (name-value pairs) that will be extracted and made available in the view
245
     * file.
246
     *
247
     * @throws RuntimeException If the view cannot be resolved.
248
     * @throws ViewNotFoundException If the view file does not exist.
249
     * @throws Throwable
250
     *
251
     * {@see renderFile()}
252
     *
253
     * @return string The rendering result.
254
     */
255 11
    public function render(string $view, array $parameters = []): string
256
    {
257 11
        $viewFile = $this->findTemplateFile($view);
258
259 11
        return $this->renderFile($viewFile, $parameters);
260
    }
261
262
    /**
263
     * Renders a view file.
264
     *
265
     * If {@see theme} is enabled (not null), it will try to render the themed version of the view file as long as it
266
     * is available.
267
     *
268
     * If {@see renderers} is enabled (not null), the method will use it to render the view file. Otherwise,
269
     * it will simply include the view file as a normal PHP file, capture its output and
270
     * return it as a string.
271
     *
272
     * @param string $viewFile The view file. This can be either an absolute file path or an alias of it.
273
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
274
     * file.
275
     *
276
     * @throws Throwable
277
     * @throws ViewNotFoundException If the view file does not exist
278
     *
279
     * @return string The rendering result.
280
     */
281 21
    public function renderFile(string $viewFile, array $parameters = []): string
282
    {
283 21
        $parameters = array_merge($this->defaultParameters, $parameters);
284
285
        // TODO: these two match now
286 21
        $requestedFile = $viewFile;
287
288 21
        if ($this->theme !== null) {
289 1
            $viewFile = $this->theme->applyTo($viewFile);
290
        }
291
292 21
        if (is_file($viewFile)) {
293 21
            $viewFile = $this->localize($viewFile);
294
        } else {
295
            throw new ViewNotFoundException("The view file does not exist: $viewFile");
296
        }
297
298 21
        $output = '';
299 21
        $this->viewFiles[] = [
300 21
            'resolved' => $viewFile,
301 21
            'requested' => $requestedFile,
302
        ];
303
304 21
        if ($this->beforeRender($viewFile, $parameters)) {
305 21
            $this->logger->debug("Rendering view file: $viewFile");
306 21
            $ext = pathinfo($viewFile, PATHINFO_EXTENSION);
307 21
            $renderer = $this->renderers[$ext] ?? new PhpTemplateRenderer();
308 21
            $output = $renderer->render($this, $viewFile, $parameters);
309
310 21
            $output = $this->afterRender($viewFile, $parameters, $output);
311
        }
312
313 21
        array_pop($this->viewFiles);
314
315 21
        return $output;
316
    }
317
318
    /**
319
     * Returns the localized version of a specified file.
320
     *
321
     * The searching is based on the specified language code. In particular, a file with the same name will be looked
322
     * for under the subdirectory whose name is the same as the language code. For example, given the file
323
     * "path/to/view.php" and language code "zh-CN", the localized file will be looked for as path/to/zh-CN/view.php".
324
     * If the file is not found, it will try a fallback with just a language code that is "zh"
325
     * i.e. "path/to/zh/view.php".
326
     * If it is not found as well the original file will be returned.
327
     *
328
     * If the target and the source language codes are the same, the original file will be returned.
329
     *
330
     * @param string $file the original file
331
     * @param string|null $language the target language that the file should be localized to.
332
     * @param string|null $sourceLanguage the language that the original file is in.
333
     *
334
     * @return string the matching localized file, or the original file if the localized version is not found.
335
     * If the target and the source language codes are the same, the original file will be returned.
336
     */
337 22
    public function localize(string $file, ?string $language = null, ?string $sourceLanguage = null): string
338
    {
339 22
        $language = $language ?? $this->language;
340 22
        $sourceLanguage = $sourceLanguage ?? $this->sourceLanguage;
341
342 22
        if ($language === $sourceLanguage) {
343 22
            return $file;
344
        }
345 1
        $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
346 1
        if (is_file($desiredFile)) {
347 1
            return $desiredFile;
348
        }
349
350
        $language = substr($language, 0, 2);
351
        if ($language === $sourceLanguage) {
352
            return $file;
353
        }
354
        $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file);
355
356
        return is_file($desiredFile) ? $desiredFile : $file;
357
    }
358
359
    /**
360
     * Renders dynamic content returned by the given PHP statements.
361
     *
362
     * This method is mainly used together with content caching (fragment caching and page caching) when some portions
363
     * of the content (called *dynamic content*) should not be cached. The dynamic content must be returned by some PHP
364
     * statements. You can optionally pass additional parameters that will be available as variables in the PHP
365
     * statement:.
366
     *
367
     * ```php
368
     * <?= $this->renderDynamic('return foo($myVar);', [
369
     *     'myVar' => $model->getMyComplexVar(),
370
     * ]) ?>
371
     * ```
372
     *
373
     * @param string $statements the PHP statements for generating the dynamic content.
374
     * @param array $parameters the parameters (name-value pairs) that will be extracted and made
375
     * available in the $statement context. The parameters will be stored in the cache and be reused
376
     * each time $statement is executed. You should make sure, that these are safely serializable.
377
     *
378
     * @return string the placeholder of the dynamic content, or the dynamic content if there is no active content
379
     *                cache currently.
380
     */
381
    public function renderDynamic(string $statements, array $parameters = []): string
382
    {
383
        if (!empty($parameters)) {
384
            $statements = 'extract(unserialize(\'' .
385
                str_replace(['\\', '\''], ['\\\\', '\\\''], serialize($parameters)) .
386
                '\'));' . $statements;
387
        }
388
389
        if (!empty($this->cacheStack)) {
390
            $n = count($this->dynamicPlaceholders);
391
            $placeholder = "<![CDATA[YII-DYNAMIC-$n-{$this->getPlaceholderSignature()}]]>";
392
            $this->addDynamicPlaceholder($placeholder, $statements);
393
394
            return $placeholder;
395
        }
396
397
        return $this->evaluateDynamicContent($statements);
398
    }
399
400
    /**
401
     * Evaluates the given PHP statements.
402
     *
403
     * This method is mainly used internally to implement dynamic content feature.
404
     *
405
     * @param string $statements The PHP statements to be evaluated.
406
     *
407
     * @return mixed The return value of the PHP statements.
408
     */
409
    public function evaluateDynamicContent(string $statements)
410
    {
411
        return eval($statements);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
412
    }
413
414
    /**
415
     * Returns a list of currently active dynamic content class instances.
416
     *
417
     * @return DynamicContentAwareInterface[] Class instances supporting dynamic contents.
418
     */
419
    public function getDynamicContents(): array
420
    {
421
        return $this->cacheStack;
422
    }
423
424
    /**
425
     * Adds a class instance supporting dynamic contents to the end of a list of currently active dynamic content class
426
     * instances.
427
     *
428
     * @param DynamicContentAwareInterface $instance Class instance supporting dynamic contents.
429
     */
430
    public function pushDynamicContent(DynamicContentAwareInterface $instance): void
431
    {
432
        $this->cacheStack[] = $instance;
433
    }
434
435
    /**
436
     * Removes a last class instance supporting dynamic contents from a list of currently active dynamic content class
437
     * instances.
438
     */
439
    public function popDynamicContent(): void
440
    {
441
        array_pop($this->cacheStack);
442
    }
443
444
    public function getDynamicPlaceholders(): array
445
    {
446
        return $this->dynamicPlaceholders;
447
    }
448
449
    public function setDynamicPlaceholders(array $placeholders): void
450
    {
451
        $this->dynamicPlaceholders = $placeholders;
452
    }
453
454
    public function addDynamicPlaceholder(string $name, string $statements): void
455
    {
456
        foreach ($this->cacheStack as $cache) {
457
            $cache->addDynamicPlaceholder($name, $statements);
458
        }
459
460
        $this->dynamicPlaceholders[$name] = $statements;
461
    }
462
463
    /**
464
     * This method is invoked right before {@see renderFile()} renders a view file.
465
     *
466
     * The default implementation will trigger the {@see BeforeRender()} event. If you override this method, make sure
467
     * you call the parent implementation first.
468
     *
469
     * @param string $viewFile the view file to be rendered.
470
     * @param array $parameters the parameter array passed to the {@see render()} method.
471
     *
472
     * @return bool whether to continue rendering the view file.
473
     */
474 21
    public function beforeRender(string $viewFile, array $parameters): bool
475
    {
476 21
        $event = new BeforeRender($viewFile, $parameters);
477 21
        $event = $this->eventDispatcher->dispatch($event);
478
479 21
        return !$event->isPropagationStopped();
480
    }
481
482
    /**
483
     * This method is invoked right after {@see renderFile()} renders a view file.
484
     *
485
     * The default implementation will trigger the {@see AfterRender} event. If you override this method, make sure you
486
     * call the parent implementation first.
487
     *
488
     * @param string $viewFile the view file being rendered.
489
     * @param array $parameters the parameter array passed to the {@see render()} method.
490
     * @param string $output the rendering result of the view file.
491
     *
492
     * @return string Updated output. It will be passed to {@see renderFile()} and returned.
493
     */
494 21
    public function afterRender(string $viewFile, array $parameters, string $output): string
495
    {
496 21
        $event = new AfterRender($viewFile, $parameters, $output);
497 21
        $event = $this->eventDispatcher->dispatch($event);
498
499 21
        return $event->getResult();
500
    }
501
502
    /**
503
     * Finds the view file based on the given view name.
504
     *
505
     * @param string $view The view name or the [path alias](guide:concept-aliases) of the view file. Please refer to
506
     * {@see render()} on how to specify this parameter.
507
     *
508
     * @throws RuntimeException If a relative view name is given while there is no active context to determine the
509
     * corresponding view file.
510
     *
511
     * @return string The view file path. Note that the file may not exist.
512
     */
513 13
    protected function findTemplateFile(string $view): string
514
    {
515 13
        if (strncmp($view, '//', 2) === 0) {
516
            // path relative to basePath e.g. "//layouts/main"
517 12
            $file = $this->basePath . '/' . ltrim($view, '/');
518 2
        } elseif (($currentViewFile = $this->getRequestedViewFile()) !== null) {
519
            // path relative to currently rendered view
520 2
            $file = dirname($currentViewFile) . '/' . $view;
521 1
        } elseif ($this->context instanceof ViewContextInterface) {
522
            // path provided by context
523 1
            $file = $this->context->getViewPath() . '/' . $view;
524
        } else {
525
            throw new RuntimeException("Unable to resolve view file for view '$view': no active view context.");
526
        }
527
528 13
        if (pathinfo($file, PATHINFO_EXTENSION) !== '') {
529 9
            return $file;
530
        }
531
532 4
        $path = $file . '.' . $this->defaultExtension;
533
534 4
        if ($this->defaultExtension !== 'php' && !is_file($path)) {
535
            $path = $file . '.php';
536
        }
537
538 4
        return $path;
539
    }
540
541
    /**
542
     * @return string|null The requested view currently being rendered. `null` if no view file is being rendered.
543
     */
544 2
    private function getRequestedViewFile(): ?string
545
    {
546 2
        return empty($this->viewFiles) ? null : end($this->viewFiles)['requested'];
547
    }
548
}
549