Test Failed
Pull Request — master (#150)
by Alexander
12:20
created

WebView::renderAjax()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 15
rs 9.9332
ccs 7
cts 7
cp 1
cc 2
nc 2
nop 2
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 22
    private array $jsFiles = [];
151
152 22
    /**
153 22
     * Marks the position of an HTML head section.
154
     */
155
    public function head(): void
156
    {
157
        echo sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature());
158 22
        $this->eventDispatcher->dispatch(new Head($this));
159
    }
160 22
161 22
    /**
162 22
     * Marks the beginning of an HTML body section.
163
     */
164
    public function beginBody(): void
165
    {
166
        echo sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature());
167 22
        $this->eventDispatcher->dispatch(new BodyBegin($this));
168
    }
169 22
170 22
    /**
171 22
     * Marks the ending of an HTML body section.
172
     */
173
    public function endBody(): void
174
    {
175
        $this->eventDispatcher->dispatch(new BodyEnd($this));
176 22
        echo sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature());
177
    }
178 22
179 22
    /**
180
     * Marks the beginning of a HTML page.
181 22
     */
182 22
    public function beginPage(): void
183
    {
184
        ob_start();
185
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
186
187
        $this->eventDispatcher->dispatch(new PageBegin($this));
188
    }
189
190
    /**
191 22
     * Marks the ending of an HTML page.
192
     *
193 22
     * @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 22
     * normal scripts.
196
     */
197 22
    public function endPage(bool $ajaxMode = false): void
198 22
    {
199 22
        $this->eventDispatcher->dispatch(new PageEnd($this));
200 22
201
        $content = ob_get_clean();
202
203 22
        echo strtr($content, [
204 22
            sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature()) => $this->renderHeadHtml(),
205
            sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature()) => $this->renderBodyBeginHtml(),
206
            sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature()) => $this->renderBodyEndHtml($ajaxMode),
207
        ]);
208
209
        $this->clear();
210
    }
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 2
     * file.
223
     *
224 2
     * @return string the rendering result
225
     *
226 2
     * {@see render()}
227 2
     */
228
    public function renderAjax(string $view, array $params = []): string
229 2
    {
230 2
        $viewFile = $this->findTemplateFile($view);
231 2
232 2
        ob_start();
233 2
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
234 2
235
        $this->beginPage();
236 2
        $this->head();
237
        $this->beginBody();
238
        echo $this->renderFile($viewFile, $params);
239
        $this->endBody();
240
        $this->endPage(true);
241
242
        return ob_get_clean();
243
    }
244
245
    /**
246 1
     * Renders a string in response to an AJAX request.
247
     *
248 1
     * @param string $string The string.
249 1
     *
250
     * @return string The rendering result.
251 1
     */
252 1
    public function renderAjaxString(string $string): string
253 1
    {
254 1
        ob_start();
255 1
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
256 1
257
        $this->beginPage();
258 1
        $this->head();
259
        $this->beginBody();
260
        echo $string;
261
        $this->endBody();
262
        $this->endPage(true);
263
264 22
        return ob_get_clean();
265
    }
266 22
267 22
    /**
268 22
     * Clears up the registered meta tags, link tags, css/js scripts and files.
269 22
     */
270 22
    public function clear(): void
271 22
    {
272 22
        $this->metaTags = [];
273
        $this->linkTags = [];
274
        $this->css = [];
275
        $this->cssFiles = [];
276
        $this->js = [];
277
        $this->jsFiles = [];
278
    }
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 1
     *
294
     * @param array $options the HTML attributes for the meta tag.
295 1
     * @param string $key the key that identifies the meta tag. If two meta tags are registered with the same key, the
296 1
     * latter will overwrite the former. If this is null, the new meta tag will be appended to the
297
     * existing ones.
298
     */
299
    public function registerMetaTag(array $options, string $key = null): void
300 1
    {
301
        if ($key === null) {
302
            $this->metaTags[] = Html::meta()->attributes($options)->render();
303
        } else {
304
            $this->metaTags[$key] = Html::meta()->attributes($options)->render();
305
        }
306
    }
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 1
     *
323
     * @param array $options the HTML attributes for the link tag.
324 1
     * @param string|null $key the key that identifies the link tag. If two link tags are registered with the same
325 1
     * 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
    public function registerLinkTag(array $options, ?string $key = null): void
329 1
    {
330
        if ($key === null) {
331
            $this->linkTags[] = Html::link()->attributes($options)->render();
332
        } else {
333
            $this->linkTags[$key] = Html::link()->attributes($options)->render();
334
        }
335
    }
336
337
    /**
338 4
     * Registers a CSS code block.
339
     *
340 4
     * @param string $css the content of the CSS code block to be registered
341 4
     * @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 4
     * code blocks are registered with the same key, the latter will overwrite the former.
343
     */
344
    public function registerCss(string $css, int $position = self::DEFAULT_POSITION_CSS_STRING, ?string $key = null): void
345
    {
346
        $key = $key ?: md5($css);
347
        $this->css[$position][$key] = $css;
348
    }
349 1
350
    /**
351 1
     * Register a `style` tag.
352 1
     *
353 1
     * @see registerJs()
354
     */
355
    public function registerStyleTag(Style $style, int $position = self::DEFAULT_POSITION_CSS_STRING, ?string $key = null): void
356
    {
357
        $key = $key ?: md5($style->render());
358
        $this->css[$position][$key] = $style;
359
    }
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 3
     * @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 3
     * 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 3
    {
376
        if (!$this->isValidCssPosition($position)) {
377
            throw new InvalidArgumentException('Invalid position of CSS file.');
378
        }
379
380
        $this->cssFiles[$position][$key ?: $url] = Html::cssFile($url, $options)->render();
381
    }
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 6
     * - {@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 6
     * - {@see POSITION_READY}: executed when HTML document composition is ready.
396 6
     * @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 6
     * blocks are registered with the same key, the latter will overwrite the former.
398
     */
399
    public function registerJs(string $js, int $position = self::DEFAULT_POSITION_JS_FILE, ?string $key = null): void
400
    {
401
        $key = $key ?: md5($js);
402
        $this->js[$position][$key] = $js;
403
    }
404 4
405
    /**
406 4
     * Register a `script` tag
407 4
     *
408 4
     * @see registerJs()
409
     */
410
    public function registerScriptTag(Script $script, int $position = self::DEFAULT_POSITION_JS_STRING, ?string $key = null): void
411
    {
412
        $key = $key ?: md5($script->render());
413
        $this->js[$position][$key] = $script;
414
    }
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 3
     * 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 3
     * 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 3
    {
440
        if (!$this->isValidJsPosition($position)) {
441
            throw new InvalidArgumentException('Invalid position of JS file.');
442
        }
443
444
        $this->jsFiles[$position][$key ?: $url] = Html::javaScriptFile($url, $options)->render();
445
    }
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 3
     * - {@see POSITION_END}: at the end of the body section.
460
     * - {@see POSITION_LOAD}: enclosed within jQuery(window).load().
461 3
     *   Note that by using this position, the method will automatically register the jQuery js file.
462 3
     * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
463 3
     *   Note that by using this position, the method will automatically register the jQuery js file.
464
     */
465
    public function registerJsVar(string $name, $value, int $position = self::DEFAULT_POSITION_JS_VARIABLE): void
466
    {
467
        $js = sprintf('var %s = %s;', $name, Json::htmlEncode($value));
468
        $this->registerJs($js, $position, $name);
469
    }
470
471
    /**
472 22
     * Renders the content to be inserted in the head section.
473
     *
474 22
     * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
475 22
     *
476 1
     * @return string the rendered content
477
     */
478
    protected function renderHeadHtml(): string
479 22
    {
480 1
        $lines = [];
481
        if (!empty($this->metaTags)) {
482 22
            $lines[] = implode("\n", $this->metaTags);
483 1
        }
484
485 22
        if (!empty($this->linkTags)) {
486 2
            $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 4
            $lines[] = $this->generateCss($this->css[self::POSITION_HEAD]);
493
        }
494
        if (!empty($this->jsFiles[self::POSITION_HEAD])) {
495 22
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_HEAD]);
496
        }
497
        if (!empty($this->js[self::POSITION_HEAD])) {
498
            $lines[] = $this->generateJs($this->js[self::POSITION_HEAD]);
499
        }
500
501
        return empty($lines) ? '' : implode("\n", $lines);
502
    }
503
504
    /**
505 22
     * Renders the content to be inserted at the beginning of the body section.
506
     *
507 22
     * The content is rendered using the registered JS code blocks and files.
508 22
     *
509 1
     * @return string the rendered content
510
     */
511 22
    protected function renderBodyBeginHtml(): string
512 2
    {
513
        $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 1
            $lines[] = $this->generateCss($this->css[self::POSITION_BEGIN]);
519
        }
520
        if (!empty($this->jsFiles[self::POSITION_BEGIN])) {
521 22
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_BEGIN]);
522
        }
523
        if (!empty($this->js[self::POSITION_BEGIN])) {
524
            $lines[] = $this->generateJs($this->js[self::POSITION_BEGIN]);
525
        }
526
527
        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 22
     * @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 22
     * scripts.
538
     *
539 22
     * @return string the rendered content
540 1
     */
541
    protected function renderBodyEndHtml(bool $ajaxMode): string
542 22
    {
543 2
        $lines = [];
544
545 22
        if (!empty($this->cssFiles[self::POSITION_END])) {
546 1
            $lines[] = implode("\n", $this->cssFiles[self::POSITION_END]);
547
        }
548
        if (!empty($this->css[self::POSITION_END])) {
549 22
            $lines[] = $this->generateCss($this->css[self::POSITION_END]);
550 3
        }
551 3
        if (!empty($this->jsFiles[self::POSITION_END])) {
552 3
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_END]);
553 3
        }
554
555 3
        if ($ajaxMode) {
556 3
            $scripts = array_merge(
557
                $this->js[self::POSITION_END] ?? [],
558
                $this->js[self::POSITION_READY] ?? [],
559 19
                $this->js[self::POSITION_LOAD] ?? [],
560 4
            );
561
            if (!empty($scripts)) {
562 19
                $lines[] = $this->generateJs($scripts);
563
            }
564 1
        } else {
565 1
            if (!empty($this->js[self::POSITION_END])) {
566 1
                $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
                    $this->generateJsWithoutTag($this->js[self::POSITION_READY]) .
571
                    "\n});";
572
                $lines[] = Html::script($js)->render();
573
            }
574
            if (!empty($this->js[self::POSITION_LOAD])) {
575
                $js = "window.addEventListener('load', function (event) {\n" .
576 22
                    $this->generateJsWithoutTag($this->js[self::POSITION_LOAD]) .
577
                    "\n});";
578
                $lines[] = Html::script($js)->render();
579
            }
580
        }
581
582
        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 1
        }
620
    }
621
622 1
    /**
623 1
     * @param array $cssStrings
624 1
     */
625 1
    public function setCssStrings(array $cssStrings): void
626
    {
627
        /** @var mixed $value */
628 1
        foreach ($cssStrings as $key => $value) {
629
            $this->registerCssStringByConfig(
630
                is_string($key) ? $key : null,
631
                is_array($value) ? $value : [$value, self::DEFAULT_POSITION_CSS_STRING]
632
            );
633
        }
634
    }
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 7
     * It processes the JS strings generated by the asset manager.
653
     *
654
     * @param array $jsStrings
655 7
     *
656 7
     * @throws InvalidArgumentException
657 7
     */
658 7
    public function setJsStrings(array $jsStrings): void
659
    {
660
        /** @var mixed $value */
661 1
        foreach ($jsStrings as $key => $value) {
662
            $this->registerJsStringByConfig(
663
                is_string($key) ? $key : null,
664
                is_array($value) ? $value : [$value, self::DEFAULT_POSITION_JS_STRING]
665
            );
666
        }
667
    }
668
669
    /**
670 5
     * It processes the JS variables generated by the asset manager and converts it into JS code.
671
     *
672 5
     * @param array $jsVars
673 5
     *
674 1
     * @throws InvalidArgumentException
675
     */
676 5
    public function setJsVars(array $jsVars): void
677
    {
678
        foreach ($jsVars as $key => $value) {
679 1
            if (is_string($key)) {
680
                $this->registerJsVar($key, $value, self::DEFAULT_POSITION_JS_VARIABLE);
681
            } else {
682
                $this->registerJsVarByConfig($value);
683
            }
684
        }
685
    }
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
    protected function createBeforeRenderEvent(string $viewFile, array $parameters): StoppableEventInterface
700
    {
701
        return new BeforeRender($this, $viewFile, $parameters);
702
    }
703
704
    protected function createAfterRenderEvent(
705
        string $viewFile,
706
        array $parameters,
707
        string $result
708
    ): AfterRenderEventInterface {
709
        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 1
722
        if (!is_string($file)) {
723 1
            throw new InvalidArgumentException(
724
                sprintf(
725
                    'CSS file should be string. Got %s.',
726 1
                    $this->getType($file),
727
                )
728 1
            );
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 1
    /**
738 1
     * @throws InvalidArgumentException
739
     */
740
    private function registerCssStringByConfig(?string $key, array $config): void
741
    {
742 1
        if (!array_key_exists(0, $config)) {
743 1
            throw new InvalidArgumentException('Do not set CSS string.');
744 1
        }
745
        $css = $config[0];
746
747 1
        if (!is_string($css) && !($css instanceof Style)) {
748 1
            throw new InvalidArgumentException(
749 1
                sprintf(
750 1
                    'CSS string should be string or instance of \\' . Style::class . '. Got %s.',
751
                    $this->getType($css),
752
                )
753
            );
754
        }
755
756
        $position = $config[1] ?? self::DEFAULT_POSITION_CSS_STRING;
757
        if (!$this->isValidCssPosition($position)) {
758
            throw new InvalidArgumentException('Invalid position of CSS strings.');
759
        }
760
761
        unset($config[0], $config[1]);
762
        if ($config !== []) {
763
            $css = ($css instanceof Style ? $css : Html::style($css))->attributes($config);
764
        }
765
766
        is_string($css)
767
            ? $this->registerCss($css, $position, $key)
768
            : $this->registerStyleTag($css, $position, $key);
769
    }
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 7
781
        if (!is_string($file)) {
782 7
            throw new InvalidArgumentException(
783 2
                sprintf(
784
                    'JS file should be string. Got %s.',
785 5
                    $this->getType($file),
786
                )
787 5
            );
788 2
        }
789 2
790 2
        $position = $config[1] ?? self::DEFAULT_POSITION_JS_FILE;
791 2
792
        unset($config[0], $config[1]);
793
        $this->registerJsFile($file, $position, $config, $key);
794
    }
795
796 3
    /**
797 3
     * @throws InvalidArgumentException
798 2
     */
799
    private function registerJsStringByConfig(?string $key, array $config): void
800
    {
801 1
        if (!array_key_exists(0, $config)) {
802 1
            throw new InvalidArgumentException('Do not set JS string.');
803
        }
804
        $js = $config[0];
805
806 1
        if (!is_string($js) && !($js instanceof Script)) {
807 1
            throw new InvalidArgumentException(
808 1
                sprintf(
809 1
                    'JS string should be string or instance of \\' . Script::class . '. Got %s.',
810
                    $this->getType($js),
811
                )
812
            );
813
        }
814 5
815
        $position = $config[1] ?? self::DEFAULT_POSITION_JS_STRING;
816 5
        if (!$this->isValidJsPosition($position)) {
817 1
            throw new InvalidArgumentException('Invalid position of JS strings.');
818
        }
819 4
820
        unset($config[0], $config[1]);
821 4
        if ($config !== []) {
822 1
            $js = ($js instanceof Script ? $js : Html::script($js))->attributes($config);
823 1
        }
824 1
825 1
        is_string($js)
826
            ? $this->registerJs($js, $position, $key)
827
            : $this->registerScriptTag($js, $position, $key);
828
    }
829
830 3
    /**
831 1
     * @throws InvalidArgumentException
832
     */
833
    private function registerJsVarByConfig(array $config): void
834 2
    {
835
        if (!array_key_exists(0, $config)) {
836 2
            throw new InvalidArgumentException('Do not set JS variable name.');
837 2
        }
838 1
        $key = $config[0];
839
840
        if (!is_string($key)) {
841 1
            throw new InvalidArgumentException(
842 1
                sprintf(
843
                    'JS variable name should be string. Got %s.',
844
                    $this->getType($key),
845
                )
846
            );
847 4
        }
848
849 4
        if (!array_key_exists(1, $config)) {
850
            throw new InvalidArgumentException('Do not set JS variable value.');
851 4
        }
852 4
        /** @var mixed */
853 4
        $value = $config[1];
854 1
855 1
        $position = $config[2] ?? self::DEFAULT_POSITION_JS_VARIABLE;
856 1
        if (!$this->isValidJsPosition($position)) {
857
            throw new InvalidArgumentException('Invalid position of JS variable.');
858 1
        }
859
860 4
        $this->registerJsVar($key, $value, $position);
861
    }
862
863 4
    /**
864 4
     * @param string[]|Style[] $items
865
     */
866
    private function generateCss(array $items): string
867 4
    {
868
        $lines = [];
869
870
        $css = [];
871
        foreach ($items as $item) {
872
            if ($item instanceof Style) {
873 7
                if ($css !== []) {
874
                    $lines[] = Html::style(implode("\n", $css))->render();
875 7
                    $css = [];
876
                }
877 7
                $lines[] = $item->render();
878 7
            } else {
879 7
                $css[] = $item;
880 4
            }
881 3
        }
882 3
        if ($css !== []) {
883
            $lines[] = Html::style(implode("\n", $css))->render();
884 4
        }
885
886 6
        return implode("\n", $lines);
887
    }
888
889 7
    /**
890 5
     * @param Script[]|string[] $items
891
     */
892
    private function generateJs(array $items): string
893 7
    {
894
        $lines = [];
895
896
        $js = [];
897
        foreach ($items as $item) {
898
            if ($item instanceof Script) {
899 1
                if ($js !== []) {
900
                    $lines[] = Html::script(implode("\n", $js))->render();
901 1
                    $js = [];
902 1
                }
903 1
                $lines[] = $item->render();
904
            } else {
905 1
                $js[] = $item;
906
            }
907
        }
908
        if ($js !== []) {
909
            $lines[] = Html::script(implode("\n", $js))->render();
910
        }
911
912
        return implode("\n", $lines);
913 4
    }
914
915 4
    /**
916 4
     * @param Script[]|string[] $items
917
     */
918 4
    private function generateJsWithoutTag(array $items): string
919 4
    {
920 4
        $js = [];
921
        foreach ($items as $item) {
922 4
            $js[] = $item instanceof Script ? $item->getContent() : $item;
923
        }
924
        return implode("\n", $js);
925
    }
926
927
    /**
928
     * @param mixed $position
929
     *
930
     * @psalm-assert =int $position
931 8
     */
932
    private function isValidCssPosition($position): bool
933 8
    {
934 8
        return in_array(
935
            $position,
936 8
            [
937 8
                self::POSITION_HEAD,
938 8
                self::POSITION_BEGIN,
939 8
                self::POSITION_END,
940 8
            ],
941
            true,
942 8
        );
943
    }
944
945
    /**
946
     * @param mixed $position
947
     *
948
     * @psalm-assert =int $position
949 3
     */
950
    private function isValidJsPosition($position): bool
951 3
    {
952
        return in_array(
953
            $position,
954
            [
955
                self::POSITION_HEAD,
956
                self::POSITION_BEGIN,
957
                self::POSITION_END,
958
                self::POSITION_READY,
959
                self::POSITION_LOAD,
960
            ],
961
            true,
962
        );
963
    }
964
965
    /**
966
     * @param mixed $value
967
     */
968
    private function getType($value): string
969
    {
970
        return is_object($value) ? get_class($value) : gettype($value);
971
    }
972
}
973