Passed
Pull Request — master (#148)
by Alexander
02:36
created

WebView::endPage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

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