Passed
Push — master ( cc0617...4c2476 )
by Alexander
03:41 queued 12s
created

WebView   F

Complexity

Total Complexity 125

Size/Duplication

Total Lines 952
Duplicated Lines 0 %

Test Coverage

Coverage 82.57%

Importance

Changes 7
Bugs 0 Features 1
Metric Value
wmc 125
eloc 284
c 7
b 0
f 1
dl 0
loc 952
ccs 270
cts 327
cp 0.8257
rs 2

42 Methods

Rating   Name   Duplication   Size   Complexity  
A renderAjaxString() 0 14 2
A head() 0 4 1
A renderAjax() 0 16 2
A beginPage() 0 7 2
A beginBody() 0 4 1
A endBody() 0 4 1
A endPage() 0 13 1
A registerLinkTag() 0 5 2
A addJsVars() 0 7 3
A createBeforeRenderEvent() 0 3 1
A addCssFiles() 0 6 4
A addCssStrings() 0 7 4
A registerLink() 0 6 1
A registerMeta() 0 3 1
A clear() 0 9 1
B renderBodyBeginHtml() 0 20 7
A registerScriptTag() 0 4 2
A registerJs() 0 4 2
A registerMetaTag() 0 5 2
A setTitle() 0 3 1
A registerJsFile() 0 7 3
B registerCssStringByConfig() 0 29 8
A registerStyleTag() 0 4 2
A registerCssFile() 0 7 3
A generateCss() 0 21 5
A getType() 0 3 2
D renderBodyEndHtml() 0 45 11
A registerJsFileByConfig() 0 20 3
A registerJsVarByConfig() 0 28 5
B renderHeadHtml() 0 24 8
A getTitle() 0 3 1
A isValidCssPosition() 0 10 1
A generateJs() 0 21 5
A registerCssFileByConfig() 0 20 3
A isValidJsPosition() 0 12 1
A generateJsWithoutTag() 0 7 3
A addJsFiles() 0 6 4
A registerCss() 0 4 2
A addJsStrings() 0 7 4
A createAfterRenderEvent() 0 6 1
A registerJsVar() 0 4 1
B registerJsStringByConfig() 0 29 8

How to fix   Complexity   

Complex Class

Complex classes like WebView often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WebView, and based on these observations, apply Extract Interface, too.

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