Passed
Push — master ( d2c063...2d8d97 )
by Alexander
04:47 queued 02:06
created

WebView::renderAjaxString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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