Passed
Pull Request — master (#158)
by Evgeniy
02:21
created

BaseView::setCommonParameters()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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