Passed
Push — master ( 43bacd...21e282 )
by Alexander
02:43
created

WebView::renderBodyBeginHtml()   A

Complexity

Conditions 6
Paths 32

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 17
ccs 11
cts 11
cp 1
rs 9.2222
cc 6
nc 32
nop 0
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\View;
6
7
use InvalidArgumentException;
8
use Psr\EventDispatcher\StoppableEventInterface;
9
use Yiisoft\Html\Html;
10
use Yiisoft\Html\Tag\Script;
11
use Yiisoft\Html\Tag\Style;
12
use Yiisoft\Json\Json;
13
use Yiisoft\View\Event\AfterRenderEventInterface;
14
use Yiisoft\View\Event\WebView\AfterRender;
15
use Yiisoft\View\Event\WebView\BeforeRender;
16
use Yiisoft\View\Event\WebView\BodyBegin;
17
use Yiisoft\View\Event\WebView\BodyEnd;
18
use Yiisoft\View\Event\WebView\Head;
19
use Yiisoft\View\Event\WebView\PageBegin;
20
use Yiisoft\View\Event\WebView\PageEnd;
21
22
use function array_key_exists;
23
use function get_class;
24
use function gettype;
25
use function in_array;
26
use function is_array;
27
use function is_object;
28
use function is_string;
29
30
/**
31
 * View represents a view object in the MVC pattern.
32
 *
33
 * View provides a set of methods (e.g. {@see render()} for rendering purpose.
34
 *
35
 * You can modify its configuration by adding an array to your application config under `components` as it is shown in
36
 * the following example:
37
 *
38
 * ```php
39
 * 'view' => [
40
 *     'theme' => 'app\themes\MyTheme',
41
 *     'renderers' => [
42
 *         // you may add Smarty or Twig renderer here
43
 *     ]
44
 *     // ...
45
 * ]
46
 * ```
47
 *
48
 * For more details and usage information on View, see the [guide article on views](guide:structure-views).
49
 */
50
final class WebView extends BaseView
51
{
52
    /**
53
     * The location of registered JavaScript code block or files.
54
     * This means the location is in the head section.
55
     */
56
    public const POSITION_HEAD = 1;
57
58
    /**
59
     * The location of registered JavaScript code block or files.
60
     * This means the location is at the beginning of the body section.
61
     */
62
    public const POSITION_BEGIN = 2;
63
64
    /**
65
     * The location of registered JavaScript code block or files.
66
     * This means the location is at the end of the body section.
67
     */
68
    public const POSITION_END = 3;
69
70
    /**
71
     * The location of registered JavaScript code block.
72
     * This means the JavaScript code block will be executed when HTML document composition is ready.
73
     */
74
    public const POSITION_READY = 4;
75
76
    /**
77
     * The location of registered JavaScript code block.
78
     * This means the JavaScript code block will be executed when HTML page is completely loaded.
79
     */
80
    public const POSITION_LOAD = 5;
81
82
    private const DEFAULT_POSITION_CSS_FILE = self::POSITION_HEAD;
83
    private const DEFAULT_POSITION_CSS_STRING = self::POSITION_HEAD;
84
    private const DEFAULT_POSITION_JS_FILE = self::POSITION_END;
85
    private const DEFAULT_POSITION_JS_VARIABLE = self::POSITION_HEAD;
86
    private const DEFAULT_POSITION_JS_STRING = self::POSITION_END;
87
88
    /**
89
     * This is internally used as the placeholder for receiving the content registered for the head section.
90
     */
91
    private const PLACEHOLDER_HEAD = '<![CDATA[YII-BLOCK-HEAD-%s]]>';
92
93
    /**
94
     * This is internally used as the placeholder for receiving the content registered for the beginning of the body
95
     * section.
96
     */
97
    private const PLACEHOLDER_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN-%s]]>';
98
99
    /**
100
     * This is internally used as the placeholder for receiving the content registered for the end of the body section.
101
     */
102
    private const PLACEHOLDER_BODY_END = '<![CDATA[YII-BLOCK-BODY-END-%s]]>';
103
104
    /**
105
     * @var string the page title
106
     */
107
    private string $title = '';
108
109
    /**
110
     * @var array the registered meta tags.
111
     *
112
     * {@see registerMetaTag()}
113
     */
114
    private array $metaTags = [];
115
116
    /**
117
     * @var array the registered link tags.
118
     *
119
     * {@see registerLinkTag()}
120
     */
121
    private array $linkTags = [];
122
123
    /**
124
     * @var array the registered CSS code blocks.
125
     *
126
     * {@see registerCss()}
127
     */
128
    private array $css = [];
129
130
    /**
131
     * @var array the registered CSS files.
132
     *
133
     * {@see registerCssFile()}
134
     */
135
    private array $cssFiles = [];
136
137
    /**
138
     * @var array the registered JS code blocks
139
     * @psalm-var array<int, string[]|Script[]>
140
     *
141
     * {@see registerJs()}
142
     */
143
    private array $js = [];
144
145
    /**
146
     * @var array the registered JS files.
147
     *
148
     * {@see registerJsFile()}
149
     */
150
    private array $jsFiles = [];
151
152
    /**
153
     * Marks the position of an HTML head section.
154
     */
155 22
    public function head(): void
156
    {
157 22
        echo sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature());
158 22
        $this->eventDispatcher->dispatch(new Head($this));
159 22
    }
160
161
    /**
162
     * Marks the beginning of an HTML body section.
163
     */
164 22
    public function beginBody(): void
165
    {
166 22
        echo sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature());
167 22
        $this->eventDispatcher->dispatch(new BodyBegin($this));
168 22
    }
169
170
    /**
171
     * Marks the ending of an HTML body section.
172
     */
173 22
    public function endBody(): void
174
    {
175 22
        $this->eventDispatcher->dispatch(new BodyEnd($this));
176 22
        echo sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature());
177 22
    }
178
179
    /**
180
     * Marks the beginning of a HTML page.
181
     */
182 22
    public function beginPage(): void
183
    {
184 22
        ob_start();
185 22
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
186
187 22
        $this->eventDispatcher->dispatch(new PageBegin($this));
188 22
    }
189
190
    /**
191
     * Marks the ending of an HTML page.
192
     *
193
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
194
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like
195
     * normal scripts.
196
     */
197 22
    public function endPage(bool $ajaxMode = false): void
198
    {
199 22
        $this->eventDispatcher->dispatch(new PageEnd($this));
200
201 22
        $content = ob_get_clean();
202
203 22
        echo strtr($content, [
204 22
            sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature()) => $this->renderHeadHtml(),
205 22
            sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature()) => $this->renderBodyBeginHtml(),
206 22
            sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature()) => $this->renderBodyEndHtml($ajaxMode),
207
        ]);
208
209 22
        $this->clear();
210 22
    }
211
212
    /**
213
     * Renders a view in response to an AJAX request.
214
     *
215
     * This method is similar to {@see render()} except that it will surround the view being rendered with the calls of
216
     * {@see beginPage()}, {@see head()}, {@see beginBody()}, {@see endBody()} and {@see endPage()}. By doing so, the
217
     * method is able to inject into the rendering result with JS/CSS scripts and files that are registered with the
218
     * view.
219
     *
220
     * @param string $view the view name. Please refer to {@see render()} on how to specify this parameter.
221
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view
222
     * file.
223
     *
224
     * @return string the rendering result
225
     *
226
     * {@see render()}
227
     */
228 2
    public function renderAjax(string $view, array $params = []): string
229
    {
230 2
        $viewFile = $this->findTemplateFile($view);
231
232 2
        ob_start();
233 2
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
234
235 2
        $this->beginPage();
236 2
        $this->head();
237 2
        $this->beginBody();
238 2
        echo $this->renderFile($viewFile, $params);
239 2
        $this->endBody();
240 2
        $this->endPage(true);
241
242 2
        return ob_get_clean();
243
    }
244
245
    /**
246
     * Renders a string in response to an AJAX request.
247
     *
248
     * @param string $string The string.
249
     *
250
     * @return string The rendering result.
251
     */
252 1
    public function renderAjaxString(string $string): string
253
    {
254 1
        ob_start();
255 1
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
256
257 1
        $this->beginPage();
258 1
        $this->head();
259 1
        $this->beginBody();
260 1
        echo $string;
261 1
        $this->endBody();
262 1
        $this->endPage(true);
263
264 1
        return ob_get_clean();
265
    }
266
267
    /**
268
     * Clears up the registered meta tags, link tags, css/js scripts and files.
269
     */
270 22
    public function clear(): void
271
    {
272 22
        $this->metaTags = [];
273 22
        $this->linkTags = [];
274 22
        $this->css = [];
275 22
        $this->cssFiles = [];
276 22
        $this->js = [];
277 22
        $this->jsFiles = [];
278 22
    }
279
280
    /**
281
     * Registers a meta tag.
282
     *
283
     * For example, a description meta tag can be added like the following:
284
     *
285
     * ```php
286
     * $view->registerMetaTag([
287
     *     'name' => 'description',
288
     *     'content' => 'This website is about funny raccoons.'
289
     * ]);
290
     * ```
291
     *
292
     * will result in the meta tag `<meta name="description" content="This website is about funny raccoons.">`.
293
     *
294
     * @param array $options the HTML attributes for the meta tag.
295
     * @param string $key the key that identifies the meta tag. If two meta tags are registered with the same key, the
296
     * latter will overwrite the former. If this is null, the new meta tag will be appended to the
297
     * existing ones.
298
     */
299 1
    public function registerMetaTag(array $options, string $key = null): void
300
    {
301 1
        if ($key === null) {
302 1
            $this->metaTags[] = Html::meta()->attributes($options)->render();
303
        } else {
304
            $this->metaTags[$key] = Html::meta()->attributes($options)->render();
305
        }
306 1
    }
307
308
    /**
309
     * Registers a link tag.
310
     *
311
     * For example, a link tag for a custom [favicon](http://www.w3.org/2005/10/howto-favicon) can be added like the
312
     * following:
313
     *
314
     * ```php
315
     * $view->registerLinkTag(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
316
     * ```
317
     *
318
     * which will result in the following HTML: `<link rel="icon" type="image/png" href="/myicon.png">`.
319
     *
320
     * **Note:** To register link tags for CSS stylesheets, use {@see registerCssFile()]} instead, which has more
321
     * options for this kind of link tag.
322
     *
323
     * @param array $options the HTML attributes for the link tag.
324
     * @param string|null $key the key that identifies the link tag. If two link tags are registered with the same
325
     * key, the latter will overwrite the former. If this is null, the new link tag will be appended
326
     * to the existing ones.
327
     */
328 1
    public function registerLinkTag(array $options, ?string $key = null): void
329
    {
330 1
        if ($key === null) {
331 1
            $this->linkTags[] = Html::link()->attributes($options)->render();
332
        } else {
333
            $this->linkTags[$key] = Html::link()->attributes($options)->render();
334
        }
335 1
    }
336
337
    /**
338
     * Registers a CSS code block.
339
     *
340
     * @param string $css the content of the CSS code block to be registered
341
     * @param string|null $key the key that identifies the CSS code block. If null, it will use $css as the key. If two CSS
342
     * code blocks are registered with the same key, the latter will overwrite the former.
343
     */
344 4
    public function registerCss(string $css, int $position = self::DEFAULT_POSITION_CSS_STRING, ?string $key = null): void
345
    {
346 4
        $key = $key ?: md5($css);
347 4
        $this->css[$position][$key] = $css;
348 4
    }
349
350
    /**
351
     * Register a `style` tag.
352
     *
353
     * @see registerJs()
354
     */
355 1
    public function registerStyleTag(Style $style, int $position = self::DEFAULT_POSITION_CSS_STRING, ?string $key = null): void
356
    {
357 1
        $key = $key ?: md5($style->render());
358 1
        $this->css[$position][$key] = $style;
359 1
    }
360
361
    /**
362
     * Registers a CSS file.
363
     *
364
     * This method should be used for simple registration of CSS files. If you want to use features of
365
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
366
     * {@see \Yiisoft\Assets\AssetBundle}.
367
     *
368
     * @param string $url the CSS file to be registered.
369
     * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
370
     * for the supported options.
371
     * @param string $key the key that identifies the CSS script file. If null, it will use $url as the key. If two CSS
372
     * files are registered with the same key, the latter will overwrite the former.
373
     */
374 3
    public function registerCssFile(string $url, int $position = self::DEFAULT_POSITION_CSS_FILE, array $options = [], string $key = null): void
375
    {
376 3
        if (!$this->isValidCssPosition($position)) {
377
            throw new InvalidArgumentException('Invalid position of CSS file.');
378
        }
379
380 3
        $this->cssFiles[$position][$key ?: $url] = Html::cssFile($url, $options)->render();
381 3
    }
382
383
    /**
384
     * Registers a JS code block.
385
     *
386
     * @param string $js the JS code block to be registered
387
     * @param int $position the position at which the JS script tag should be inserted in a page.
388
     *
389
     * The possible values are:
390
     *
391
     * - {@see POSITION_HEAD}: in the head section
392
     * - {@see POSITION_BEGIN}: at the beginning of the body section
393
     * - {@see POSITION_END}: at the end of the body section. This is the default value.
394
     * - {@see POSITION_LOAD}: executed when HTML page is completely loaded.
395
     * - {@see POSITION_READY}: executed when HTML document composition is ready.
396
     * @param string $key the key that identifies the JS code block. If null, it will use $js as the key. If two JS code
397
     * blocks are registered with the same key, the latter will overwrite the former.
398
     */
399 6
    public function registerJs(string $js, int $position = self::DEFAULT_POSITION_JS_FILE, ?string $key = null): void
400
    {
401 6
        $key = $key ?: md5($js);
402 6
        $this->js[$position][$key] = $js;
403 6
    }
404
405
    /**
406
     * Register a `script` tag
407
     *
408
     * @see registerJs()
409
     */
410 4
    public function registerScriptTag(Script $script, int $position = self::DEFAULT_POSITION_JS_STRING, ?string $key = null): void
411
    {
412 4
        $key = $key ?: md5($script->render());
413 4
        $this->js[$position][$key] = $script;
414 4
    }
415
416
    /**
417
     * Registers a JS file.
418
     *
419
     * This method should be used for simple registration of JS files. If you want to use features of
420
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
421
     * {@see \Yiisoft\Assets\AssetBundle}.
422
     *
423
     * @param string $url the JS file to be registered.
424
     * @param array $options the HTML attributes for the script tag. The following options are specially handled and
425
     * are not treated as HTML attributes:
426
     *
427
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
428
     *     * {@see POSITION_HEAD}: in the head section
429
     *     * {@see POSITION_BEGIN}: at the beginning of the body section
430
     *     * {@see POSITION_END}: at the end of the body section. This is the default value.
431
     *
432
     * Please refer to {@see \Yiisoft\Html\Html::javaScriptFile()} for other supported options.
433
     * @param string $key the key that identifies the JS script file. If null, it will use $url as the key. If two JS
434
     * files are registered with the same key at the same position, the latter will overwrite the former.
435
     * Note that position option takes precedence, thus files registered with the same key, but different
436
     * position option will not override each other.
437
     */
438 3
    public function registerJsFile(string $url, int $position = self::DEFAULT_POSITION_JS_FILE, array $options = [], string $key = null): void
439
    {
440 3
        if (!$this->isValidJsPosition($position)) {
441
            throw new InvalidArgumentException('Invalid position of JS file.');
442
        }
443
444 3
        $this->jsFiles[$position][$key ?: $url] = Html::javaScriptFile($url, $options)->render();
445 3
    }
446
447
    /**
448
     * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
449
     * variable names.
450
     *
451
     * @param string $name Name of the variable
452
     * @param array|string $value Value of the variable
453
     * @param int $position the position in a page at which the JavaScript variable should be inserted.
454
     *
455
     * The possible values are:
456
     *
457
     * - {@see POSITION_HEAD}: in the head section. This is the default value.
458
     * - {@see POSITION_BEGIN}: at the beginning of the body section.
459
     * - {@see POSITION_END}: at the end of the body section.
460
     * - {@see POSITION_LOAD}: enclosed within jQuery(window).load().
461
     *   Note that by using this position, the method will automatically register the jQuery js file.
462
     * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
463
     *   Note that by using this position, the method will automatically register the jQuery js file.
464
     */
465 3
    public function registerJsVar(string $name, $value, int $position = self::DEFAULT_POSITION_JS_VARIABLE): void
466
    {
467 3
        $js = sprintf('var %s = %s;', $name, Json::htmlEncode($value));
468 3
        $this->registerJs($js, $position, $name);
469 3
    }
470
471
    /**
472
     * Renders the content to be inserted in the head section.
473
     *
474
     * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
475
     *
476
     * @return string the rendered content
477
     */
478 22
    protected function renderHeadHtml(): string
479
    {
480 22
        $lines = [];
481 22
        if (!empty($this->metaTags)) {
482 1
            $lines[] = implode("\n", $this->metaTags);
483
        }
484
485 22
        if (!empty($this->linkTags)) {
486 1
            $lines[] = implode("\n", $this->linkTags);
487
        }
488 22
        if (!empty($this->cssFiles[self::POSITION_HEAD])) {
489 1
            $lines[] = implode("\n", $this->cssFiles[self::POSITION_HEAD]);
490
        }
491 22
        if (!empty($this->css[self::POSITION_HEAD])) {
492 2
            $lines[] = $this->generateCss($this->css[self::POSITION_HEAD]);
493
        }
494 22
        if (!empty($this->jsFiles[self::POSITION_HEAD])) {
495 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_HEAD]);
496
        }
497 22
        if (!empty($this->js[self::POSITION_HEAD])) {
498 4
            $lines[] = $this->generateJs($this->js[self::POSITION_HEAD]);
499
        }
500
501 22
        return empty($lines) ? '' : implode("\n", $lines);
502
    }
503
504
    /**
505
     * Renders the content to be inserted at the beginning of the body section.
506
     *
507
     * The content is rendered using the registered JS code blocks and files.
508
     *
509
     * @return string the rendered content
510
     */
511 22
    protected function renderBodyBeginHtml(): string
512
    {
513 22
        $lines = [];
514 22
        if (!empty($this->cssFiles[self::POSITION_BEGIN])) {
515 1
            $lines[] = implode("\n", $this->cssFiles[self::POSITION_BEGIN]);
516
        }
517 22
        if (!empty($this->css[self::POSITION_BEGIN])) {
518 2
            $lines[] = $this->generateCss($this->css[self::POSITION_BEGIN]);
519
        }
520 22
        if (!empty($this->jsFiles[self::POSITION_BEGIN])) {
521 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_BEGIN]);
522
        }
523 22
        if (!empty($this->js[self::POSITION_BEGIN])) {
524 1
            $lines[] = $this->generateJs($this->js[self::POSITION_BEGIN]);
525
        }
526
527 22
        return empty($lines) ? '' : implode("\n", $lines);
528
    }
529
530
    /**
531
     * Renders the content to be inserted at the end of the body section.
532
     *
533
     * The content is rendered using the registered JS code blocks and files.
534
     *
535
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
536
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like normal
537
     * scripts.
538
     *
539
     * @return string the rendered content
540
     */
541 22
    protected function renderBodyEndHtml(bool $ajaxMode): string
542
    {
543 22
        $lines = [];
544
545 22
        if (!empty($this->cssFiles[self::POSITION_END])) {
546 1
            $lines[] = implode("\n", $this->cssFiles[self::POSITION_END]);
547
        }
548 22
        if (!empty($this->css[self::POSITION_END])) {
549 2
            $lines[] = $this->generateCss($this->css[self::POSITION_END]);
550
        }
551 22
        if (!empty($this->jsFiles[self::POSITION_END])) {
552 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_END]);
553
        }
554
555 22
        if ($ajaxMode) {
556 3
            $scripts = array_merge(
557 3
                $this->js[self::POSITION_END] ?? [],
558 3
                $this->js[self::POSITION_READY] ?? [],
559 3
                $this->js[self::POSITION_LOAD] ?? [],
560
            );
561 3
            if (!empty($scripts)) {
562 3
                $lines[] = $this->generateJs($scripts);
563
            }
564
        } else {
565 19
            if (!empty($this->js[self::POSITION_END])) {
566 4
                $lines[] = $this->generateJs($this->js[self::POSITION_END]);
567
            }
568 19
            if (!empty($this->js[self::POSITION_READY])) {
569
                $js = "document.addEventListener('DOMContentLoaded', function(event) {\n" .
570 1
                    $this->generateJsWithoutTag($this->js[self::POSITION_READY]) .
571 1
                    "\n});";
572 1
                $lines[] = Html::script($js)->render();
573
            }
574 19
            if (!empty($this->js[self::POSITION_LOAD])) {
575
                $js = "window.addEventListener('load', function (event) {\n" .
576
                    $this->generateJsWithoutTag($this->js[self::POSITION_LOAD]) .
577
                    "\n});";
578
                $lines[] = Html::script($js)->render();
579
            }
580
        }
581
582 22
        return empty($lines) ? '' : implode("\n", $lines);
583
    }
584
585
    /**
586
     * Get title in views.
587
     *
588
     * in Layout:
589
     *
590
     * ```php
591
     * <title><?= Html::encode($this->getTitle()) ?></title>
592
     * ```
593
     *
594
     * in Views:
595
     *
596
     * ```php
597
     * $this->setTitle('Web Application - Yii 3.0.');
598
     * ```
599
     *
600
     * @return string
601
     */
602
    public function getTitle(): string
603
    {
604
        return $this->title;
605
    }
606
607
    /**
608
     * It processes the CSS configuration generated by the asset manager and converts it into HTML code.
609
     *
610
     * @param array $cssFiles
611
     */
612
    public function setCssFiles(array $cssFiles): void
613
    {
614
        foreach ($cssFiles as $key => $value) {
615
            $this->registerCssFileByConfig(
616
                is_string($key) ? $key : null,
617
                is_array($value) ? $value : [$value],
618
            );
619
        }
620
    }
621
622
    /**
623
     * @param array $cssStrings
624
     */
625 1
    public function setCssStrings(array $cssStrings): void
626
    {
627
        /** @var mixed $value */
628 1
        foreach ($cssStrings as $key => $value) {
629 1
            $this->registerCssStringByConfig(
630 1
                is_string($key) ? $key : null,
631 1
                is_array($value) ? $value : [$value, self::DEFAULT_POSITION_CSS_STRING]
632
            );
633
        }
634 1
    }
635
636
    /**
637
     * It processes the JS configuration generated by the asset manager and converts it into HTML code.
638
     *
639
     * @param array $jsFiles
640
     */
641
    public function setJsFiles(array $jsFiles): void
642
    {
643
        foreach ($jsFiles as $key => $value) {
644
            $this->registerJsFileByConfig(
645
                is_string($key) ? $key : null,
646
                is_array($value) ? $value : [$value],
647
            );
648
        }
649
    }
650
651
    /**
652
     * It processes the JS strings generated by the asset manager.
653
     *
654
     * @param array $jsStrings
655
     *
656
     * @throws InvalidArgumentException
657
     */
658 7
    public function setJsStrings(array $jsStrings): void
659
    {
660
        /** @var mixed $value */
661 7
        foreach ($jsStrings as $key => $value) {
662 7
            $this->registerJsStringByConfig(
663 7
                is_string($key) ? $key : null,
664 7
                is_array($value) ? $value : [$value, self::DEFAULT_POSITION_JS_STRING]
665
            );
666
        }
667 1
    }
668
669
    /**
670
     * It processes the JS variables generated by the asset manager and converts it into JS code.
671
     *
672
     * @param array $jsVars
673
     *
674
     * @throws InvalidArgumentException
675
     */
676 5
    public function setJsVars(array $jsVars): void
677
    {
678 5
        foreach ($jsVars as $key => $value) {
679 5
            if (is_string($key)) {
680 1
                $this->registerJsVar($key, $value, self::DEFAULT_POSITION_JS_VARIABLE);
681
            } else {
682 5
                $this->registerJsVarByConfig($value);
683
            }
684
        }
685 1
    }
686
687
    /**
688
     * Set title in views.
689
     *
690
     * {@see getTitle()}
691
     *
692
     * @param string $value
693
     */
694
    public function setTitle(string $value): void
695
    {
696
        $this->title = $value;
697
    }
698
699 21
    protected function createBeforeRenderEvent(string $viewFile, array $parameters): StoppableEventInterface
700
    {
701 21
        return new BeforeRender($this, $viewFile, $parameters);
702
    }
703
704 21
    protected function createAfterRenderEvent(
705
        string $viewFile,
706
        array $parameters,
707
        string $result
708
    ): AfterRenderEventInterface {
709 21
        return new AfterRender($this, $viewFile, $parameters, $result);
710
    }
711
712
    /**
713
     * @throws InvalidArgumentException
714
     */
715
    private function registerCssFileByConfig(?string $key, array $config): void
716
    {
717
        if (!array_key_exists(0, $config)) {
718
            throw new InvalidArgumentException('Do not set CSS file.');
719
        }
720
        $file = $config[0];
721
722
        if (!is_string($file)) {
723
            throw new InvalidArgumentException(
724
                sprintf(
725
                    'CSS file should be string. Got %s.',
726
                    $this->getType($file),
727
                )
728
            );
729
        }
730
731
        $position = $config[1] ?? self::DEFAULT_POSITION_CSS_FILE;
732
733
        unset($config[0], $config[1]);
734
        $this->registerCssFile($file, $position, $config, $key);
735
    }
736
737
    /**
738
     * @throws InvalidArgumentException
739
     */
740 1
    private function registerCssStringByConfig(?string $key, array $config): void
741
    {
742 1
        if (!array_key_exists(0, $config)) {
743
            throw new InvalidArgumentException('Do not set CSS string.');
744
        }
745 1
        $css = $config[0];
746
747 1
        if (!is_string($css) && !($css instanceof Style)) {
748
            throw new InvalidArgumentException(
749
                sprintf(
750
                    'CSS string should be string or instance of \\' . Style::class . '. Got %s.',
751
                    $this->getType($css),
752
                )
753
            );
754
        }
755
756 1
        $position = $config[1] ?? self::DEFAULT_POSITION_CSS_STRING;
757 1
        if (!$this->isValidCssPosition($position)) {
758
            throw new InvalidArgumentException('Invalid position of CSS strings.');
759
        }
760
761 1
        unset($config[0], $config[1]);
762 1
        if ($config !== []) {
763 1
            $css = ($css instanceof Style ? $css : Html::style($css))->attributes($config);
764
        }
765
766 1
        is_string($css)
767 1
            ? $this->registerCss($css, $position, $key)
768 1
            : $this->registerStyleTag($css, $position, $key);
769 1
    }
770
771
    /**
772
     * @throws InvalidArgumentException
773
     */
774
    private function registerJsFileByConfig(?string $key, array $config): void
775
    {
776
        if (!array_key_exists(0, $config)) {
777
            throw new InvalidArgumentException('Do not set JS file.');
778
        }
779
        $file = $config[0];
780
781
        if (!is_string($file)) {
782
            throw new InvalidArgumentException(
783
                sprintf(
784
                    'JS file should be string. Got %s.',
785
                    $this->getType($file),
786
                )
787
            );
788
        }
789
790
        $position = $config[1] ?? self::DEFAULT_POSITION_JS_FILE;
791
792
        unset($config[0], $config[1]);
793
        $this->registerJsFile($file, $position, $config, $key);
794
    }
795
796
    /**
797
     * @throws InvalidArgumentException
798
     */
799 7
    private function registerJsStringByConfig(?string $key, array $config): void
800
    {
801 7
        if (!array_key_exists(0, $config)) {
802 2
            throw new InvalidArgumentException('Do not set JS string.');
803
        }
804 5
        $js = $config[0];
805
806 5
        if (!is_string($js) && !($js instanceof Script)) {
807 2
            throw new InvalidArgumentException(
808 2
                sprintf(
809 2
                    'JS string should be string or instance of \\' . Script::class . '. Got %s.',
810 2
                    $this->getType($js),
811
                )
812
            );
813
        }
814
815 3
        $position = $config[1] ?? self::DEFAULT_POSITION_JS_STRING;
816 3
        if (!$this->isValidJsPosition($position)) {
817 2
            throw new InvalidArgumentException('Invalid position of JS strings.');
818
        }
819
820 1
        unset($config[0], $config[1]);
821 1
        if ($config !== []) {
822
            $js = ($js instanceof Script ? $js : Html::script($js))->attributes($config);
823
        }
824
825 1
        is_string($js)
826 1
            ? $this->registerJs($js, $position, $key)
827 1
            : $this->registerScriptTag($js, $position, $key);
828 1
    }
829
830
    /**
831
     * @throws InvalidArgumentException
832
     */
833 5
    private function registerJsVarByConfig(array $config): void
834
    {
835 5
        if (!array_key_exists(0, $config)) {
836 1
            throw new InvalidArgumentException('Do not set JS variable name.');
837
        }
838 4
        $key = $config[0];
839
840 4
        if (!is_string($key)) {
841 1
            throw new InvalidArgumentException(
842 1
                sprintf(
843 1
                    'JS variable name should be string. Got %s.',
844 1
                    $this->getType($key),
845
                )
846
            );
847
        }
848
849 3
        if (!array_key_exists(1, $config)) {
850 1
            throw new InvalidArgumentException('Do not set JS variable value.');
851
        }
852
        /** @var mixed */
853 2
        $value = $config[1];
854
855 2
        $position = $config[2] ?? self::DEFAULT_POSITION_JS_VARIABLE;
856 2
        if (!$this->isValidJsPosition($position)) {
857 1
            throw new InvalidArgumentException('Invalid position of JS variable.');
858
        }
859
860 1
        $this->registerJsVar($key, $value, $position);
861 1
    }
862
863
    /**
864
     * @param string[]|Style[] $items
865
     */
866 4
    private function generateCss(array $items): string
867
    {
868 4
        $lines = [];
869
870 4
        $css = [];
871 4
        foreach ($items as $item) {
872 4
            if ($item instanceof Style) {
873 1
                if ($css !== []) {
874 1
                    $lines[] = Html::style(implode("\n", $css))->render();
875 1
                    $css = [];
876
                }
877 1
                $lines[] = $item->render();
878
            } else {
879 4
                $css[] = $item;
880
            }
881
        }
882 4
        if ($css !== []) {
883 4
            $lines[] = Html::style(implode("\n", $css))->render();
884
        }
885
886 4
        return implode("\n", $lines);
887
    }
888
889
    /**
890
     * @param Script[]|string[] $items
891
     */
892 7
    private function generateJs(array $items): string
893
    {
894 7
        $lines = [];
895
896 7
        $js = [];
897 7
        foreach ($items as $item) {
898 7
            if ($item instanceof Script) {
899 4
                if ($js !== []) {
900 3
                    $lines[] = Html::script(implode("\n", $js))->render();
901 3
                    $js = [];
902
                }
903 4
                $lines[] = $item->render();
904
            } else {
905 6
                $js[] = $item;
906
            }
907
        }
908 7
        if ($js !== []) {
909 5
            $lines[] = Html::script(implode("\n", $js))->render();
910
        }
911
912 7
        return implode("\n", $lines);
913
    }
914
915
    /**
916
     * @param Script[]|string[] $items
917
     */
918 1
    private function generateJsWithoutTag(array $items): string
919
    {
920 1
        $js = [];
921 1
        foreach ($items as $item) {
922 1
            $js[] = $item instanceof Script ? $item->getContent() : $item;
923
        }
924 1
        return implode("\n", $js);
925
    }
926
927
    /**
928
     * @param mixed $position
929
     *
930
     * @psalm-assert =int $position
931
     */
932 4
    private function isValidCssPosition($position): bool
933
    {
934 4
        return in_array(
935 4
            $position,
936
            [
937 4
                self::POSITION_HEAD,
938 4
                self::POSITION_BEGIN,
939 4
                self::POSITION_END,
940
            ],
941 4
            true,
942
        );
943
    }
944
945
    /**
946
     * @param mixed $position
947
     *
948
     * @psalm-assert =int $position
949
     */
950 8
    private function isValidJsPosition($position): bool
951
    {
952 8
        return in_array(
953 8
            $position,
954
            [
955 8
                self::POSITION_HEAD,
956 8
                self::POSITION_BEGIN,
957 8
                self::POSITION_END,
958 8
                self::POSITION_READY,
959 8
                self::POSITION_LOAD,
960
            ],
961 8
            true,
962
        );
963
    }
964
965
    /**
966
     * @param mixed $value
967
     */
968 3
    private function getType($value): string
969
    {
970 3
        return is_object($value) ? get_class($value) : gettype($value);
971
    }
972
}
973