Passed
Pull Request — master (#160)
by Evgeniy
03:08
created

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