Passed
Pull Request — master (#141)
by Sergei
02:18
created

WebView::setCssFiles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\View;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Html\Html;
9
use Yiisoft\Html\Tag\Script;
10
use Yiisoft\View\Event\BodyBegin;
11
use Yiisoft\View\Event\BodyEnd;
12
use Yiisoft\View\Event\PageEnd;
13
14
/**
15
 * View represents a view object in the MVC pattern.
16
 *
17
 * View provides a set of methods (e.g. {@see render()} for rendering purpose.
18
 *
19
 * You can modify its configuration by adding an array to your application config under `components` as it is shown in
20
 * the following example:
21
 *
22
 * ```php
23
 * 'view' => [
24
 *     'theme' => 'app\themes\MyTheme',
25
 *     'renderers' => [
26
 *         // you may add Smarty or Twig renderer here
27
 *     ]
28
 *     // ...
29
 * ]
30
 * ```
31
 *
32
 * For more details and usage information on View, see the [guide article on views](guide:structure-views).
33
 */
34
class WebView extends View
35
{
36
    /**
37
     * The location of registered JavaScript code block or files.
38
     * This means the location is in the head section.
39
     */
40
    public const POSITION_HEAD = 1;
41
42
    /**
43
     * The location of registered JavaScript code block or files.
44
     * This means the location is at the beginning of the body section.
45
     */
46
    public const POSITION_BEGIN = 2;
47
48
    /**
49
     * The location of registered JavaScript code block or files.
50
     * This means the location is at the end of the body section.
51
     */
52
    public const POSITION_END = 3;
53
54
    /**
55
     * The location of registered JavaScript code block.
56
     * This means the JavaScript code block will be executed when HTML document composition is ready.
57
     */
58
    public const POSITION_READY = 4;
59
60
    /**
61
     * The location of registered JavaScript code block.
62
     * This means the JavaScript code block will be executed when HTML page is completely loaded.
63
     */
64
    public const POSITION_LOAD = 5;
65
66
    /**
67
     * This is internally used as the placeholder for receiving the content registered for the head section.
68
     */
69
    private const PLACEHOLDER_HEAD = '<![CDATA[YII-BLOCK-HEAD-%s]]>';
70
71
    /**
72
     * This is internally used as the placeholder for receiving the content registered for the beginning of the body
73
     * section.
74
     */
75
    private const PLACEHOLDER_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN-%s]]>';
76
77
    /**
78
     * This is internally used as the placeholder for receiving the content registered for the end of the body section.
79
     */
80
    private const PLACEHOLDER_BODY_END = '<![CDATA[YII-BLOCK-BODY-END-%s]]>';
81
82
    /**
83
     * @var string the page title
84
     */
85
    private string $title = '';
86
87
    /**
88
     * @var array the registered meta tags.
89
     *
90
     * {@see registerMetaTag()}
91
     */
92
    private array $metaTags = [];
93
94
    /**
95
     * @var array the registered link tags.
96
     *
97
     * {@see registerLinkTag()}
98
     */
99
    private array $linkTags = [];
100
101
    /**
102
     * @var array the registered CSS code blocks.
103
     *
104
     * {@see registerCss()}
105
     */
106
    private array $css = [];
107
108
    /**
109
     * @var array the registered CSS files.
110
     *
111
     * {@see registerCssFile()}
112
     */
113
    private array $cssFiles = [];
114
115
    /**
116
     * @var array the registered JS code blocks
117
     * @psalm-var array<int, string[]|Script[]>
118
     *
119
     * {@see registerJs()}
120
     */
121
    private array $js = [];
122
123
    /**
124
     * @var array the registered JS files.
125
     *
126
     * {@see registerJsFile()}
127
     */
128
    private array $jsFiles = [];
129
130
    /**
131
     * Marks the position of an HTML head section.
132
     */
133 12
    public function head(): void
134
    {
135 12
        echo sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature());
136 12
    }
137
138
    /**
139
     * Marks the beginning of an HTML body section.
140
     */
141 12
    public function beginBody(): void
142
    {
143 12
        echo sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature());
144 12
        $this->eventDispatcher->dispatch(new BodyBegin($this->getViewFile()));
145 12
    }
146
147
    /**
148
     * Marks the ending of an HTML body section.
149
     */
150 12
    public function endBody(): void
151
    {
152 12
        $this->eventDispatcher->dispatch(new BodyEnd($this->getViewFile()));
153 12
        echo sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature());
154 12
    }
155
156
    /**
157
     * Marks the ending of an HTML page.
158
     *
159
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
160
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like
161
     * normal scripts.
162
     */
163 12
    public function endPage($ajaxMode = false): void
164
    {
165 12
        $this->eventDispatcher->dispatch(new PageEnd($this->getViewFile()));
166
167 12
        $content = ob_get_clean();
168
169 12
        echo strtr($content, [
170 12
            sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature()) => $this->renderHeadHtml(),
171 12
            sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature()) => $this->renderBodyBeginHtml(),
172 12
            sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature()) => $this->renderBodyEndHtml($ajaxMode),
173
        ]);
174
175 12
        $this->clear();
176 12
    }
177
178
    /**
179
     * Renders a view in response to an AJAX request.
180
     *
181
     * This method is similar to {@see render()} except that it will surround the view being rendered with the calls of
182
     * {@see beginPage()}, {@see head()}, {@see beginBody()}, {@see endBody()} and {@see endPage()}. By doing so, the
183
     * method is able to inject into the rendering result with JS/CSS scripts and files that are registered with the
184
     * view.
185
     *
186
     * @param string $view the view name. Please refer to {@see render()} on how to specify this parameter.
187
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view
188
     * file.
189
     *
190
     * @return string the rendering result
191
     *
192
     * {@see render()}
193
     */
194 2
    public function renderAjax(string $view, array $params = []): string
195
    {
196 2
        $viewFile = $this->findTemplateFile($view);
197
198 2
        ob_start();
199 2
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
200
201 2
        $this->beginPage();
202 2
        $this->head();
203 2
        $this->beginBody();
204 2
        echo $this->renderFile($viewFile, $params);
205 2
        $this->endBody();
206 2
        $this->endPage(true);
207
208 2
        return ob_get_clean();
209
    }
210
211
    /**
212
     * Renders a string in response to an AJAX request.
213
     *
214
     * @param string $string The string.
215
     *
216
     * @return string The rendering result.
217
     */
218 1
    public function renderAjaxString(string $string): string
219
    {
220 1
        ob_start();
221 1
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
222
223 1
        $this->beginPage();
224 1
        $this->head();
225 1
        $this->beginBody();
226 1
        echo $string;
227 1
        $this->endBody();
228 1
        $this->endPage(true);
229
230 1
        return ob_get_clean();
231
    }
232
233
    /**
234
     * Clears up the registered meta tags, link tags, css/js scripts and files.
235
     */
236 12
    public function clear(): void
237
    {
238 12
        $this->metaTags = [];
239 12
        $this->linkTags = [];
240 12
        $this->css = [];
241 12
        $this->cssFiles = [];
242 12
        $this->js = [];
243 12
        $this->jsFiles = [];
244 12
    }
245
246
    /**
247
     * Registers a meta tag.
248
     *
249
     * For example, a description meta tag can be added like the following:
250
     *
251
     * ```php
252
     * $view->registerMetaTag([
253
     *     'name' => 'description',
254
     *     'content' => 'This website is about funny raccoons.'
255
     * ]);
256
     * ```
257
     *
258
     * will result in the meta tag `<meta name="description" content="This website is about funny raccoons.">`.
259
     *
260
     * @param array $options the HTML attributes for the meta tag.
261
     * @param string $key the key that identifies the meta tag. If two meta tags are registered with the same key, the
262
     * latter will overwrite the former. If this is null, the new meta tag will be appended to the
263
     * existing ones.
264
     */
265 1
    public function registerMetaTag(array $options, string $key = null): void
266
    {
267 1
        if ($key === null) {
268 1
            $this->metaTags[] = Html::meta()->attributes($options)->render();
269
        } else {
270
            $this->metaTags[$key] = Html::meta()->attributes($options)->render();
271
        }
272 1
    }
273
274
    /**
275
     * Registers a link tag.
276
     *
277
     * For example, a link tag for a custom [favicon](http://www.w3.org/2005/10/howto-favicon) can be added like the
278
     * following:
279
     *
280
     * ```php
281
     * $view->registerLinkTag(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
282
     * ```
283
     *
284
     * which will result in the following HTML: `<link rel="icon" type="image/png" href="/myicon.png">`.
285
     *
286
     * **Note:** To register link tags for CSS stylesheets, use {@see registerCssFile()]} instead, which has more
287
     * options for this kind of link tag.
288
     *
289
     * @param array $options the HTML attributes for the link tag.
290
     * @param string|null $key the key that identifies the link tag. If two link tags are registered with the same
291
     * key, the latter will overwrite the former. If this is null, the new link tag will be appended
292
     * to the existing ones.
293
     */
294 1
    public function registerLinkTag(array $options, ?string $key = null): void
295
    {
296 1
        if ($key === null) {
297 1
            $this->linkTags[] = Html::link()->attributes($options)->render();
298
        } else {
299
            $this->linkTags[$key] = Html::link()->attributes($options)->render();
300
        }
301 1
    }
302
303
    /**
304
     * Registers a CSS code block.
305
     *
306
     * @param string $css the content of the CSS code block to be registered
307
     * @param array $options the HTML attributes for the `<style>`-tag.
308
     * @param string $key the key that identifies the CSS code block. If null, it will use $css as the key. If two CSS
309
     * code blocks are registered with the same key, the latter will overwrite the former.
310
     */
311 1
    public function registerCss(string $css, array $options = [], string $key = null): void
312
    {
313 1
        $key = $key ?: md5($css);
314 1
        $this->css[$key] = Html::style($css, $options)->render();
315 1
    }
316
317
    /**
318
     * Registers a CSS file.
319
     *
320
     * This method should be used for simple registration of CSS files. If you want to use features of
321
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
322
     * {@see \Yiisoft\Assets\AssetBundle}.
323
     *
324
     * @param string $url the CSS file to be registered.
325
     * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
326
     * for the supported options.
327
     * @param string $key the key that identifies the CSS script file. If null, it will use $url as the key. If two CSS
328
     * files are registered with the same key, the latter will overwrite the former.
329
     */
330 1
    public function registerCssFile(string $url, array $options = [], string $key = null): void
331
    {
332 1
        $key = $key ?: $url;
333
334 1
        $this->cssFiles[$key] = Html::cssFile($url, $options)->render();
335 1
    }
336
337
    /**
338
     * Registers a JS code block.
339
     *
340
     * @param string $js the JS code block to be registered
341
     * @param int $position the position at which the JS script tag should be inserted in a page.
342
     *
343
     * The possible values are:
344
     *
345
     * - {@see POSITION_HEAD}: in the head section
346
     * - {@see POSITION_BEGIN}: at the beginning of the body section
347
     * - {@see POSITION_END}: at the end of the body section. This is the default value.
348
     * - {@see POSITION_LOAD}: executed when HTML page is completely loaded.
349
     * - {@see POSITION_READY}: executed when HTML document composition is ready.
350
     * @param string $key the key that identifies the JS code block. If null, it will use $js as the key. If two JS code
351
     * blocks are registered with the same key, the latter will overwrite the former.
352
     */
353 3
    public function registerJs(string $js, int $position = self::POSITION_END, string $key = null): void
354
    {
355 3
        $key = $key ?: md5($js);
356 3
        $this->js[$position][$key] = $js;
357 3
    }
358
359
    /**
360
     * Register a `script` tag
361
     *
362
     * @see registerJs()
363
     */
364 3
    public function registerScriptTag(Script $script, int $position = self::POSITION_END, string $key = null): void
365
    {
366 3
        $key = $key ?: md5($script->render());
367 3
        $this->js[$position][$key] = $script;
368 3
    }
369
370
    /**
371
     * Registers a JS file.
372
     *
373
     * This method should be used for simple registration of JS files. If you want to use features of
374
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
375
     * {@see \Yiisoft\Assets\AssetBundle}.
376
     *
377
     * @param string $url the JS file to be registered.
378
     * @param array $options the HTML attributes for the script tag. The following options are specially handled and
379
     * are not treated as HTML attributes:
380
     *
381
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
382
     *     * {@see POSITION_HEAD}: in the head section
383
     *     * {@see POSITION_BEGIN}: at the beginning of the body section
384
     *     * {@see POSITION_END}: at the end of the body section. This is the default value.
385
     *
386
     * Please refer to {@see \Yiisoft\Html\Html::javaScriptFile()} for other supported options.
387
     * @param string $key the key that identifies the JS script file. If null, it will use $url as the key. If two JS
388
     * files are registered with the same key at the same position, the latter will overwrite the former.
389
     * Note that position option takes precedence, thus files registered with the same key, but different
390
     * position option will not override each other.
391
     */
392 1
    public function registerJsFile(string $url, array $options = [], string $key = null): void
393
    {
394 1
        $key = $key ?: $url;
395
396 1
        $position = ArrayHelper::remove($options, 'position', self::POSITION_END);
397 1
        $this->jsFiles[$position][$key] = Html::javaScriptFile($url, $options)->render();
398 1
    }
399
400
    /**
401
     * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
402
     * variable names.
403
     *
404
     * @param string $name Name of the variable
405
     * @param array|string $value Value of the variable
406
     * @param int $position the position in a page at which the JavaScript variable should be inserted.
407
     *
408
     * The possible values are:
409
     *
410
     * - {@see POSITION_HEAD}: in the head section. This is the default value.
411
     * - {@see POSITION_BEGIN}: at the beginning of the body section.
412
     * - {@see POSITION_END}: at the end of the body section.
413
     * - {@see POSITION_LOAD}: enclosed within jQuery(window).load().
414
     *   Note that by using this position, the method will automatically register the jQuery js file.
415
     * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
416
     *   Note that by using this position, the method will automatically register the jQuery js file.
417
     */
418 1
    public function registerJsVar(string $name, $value, int $position = self::POSITION_HEAD): void
419
    {
420 1
        $js = sprintf('var %s = %s;', $name, \Yiisoft\Json\Json::htmlEncode($value));
421 1
        $this->registerJs($js, $position, $name);
422 1
    }
423
424
    /**
425
     * Renders the content to be inserted in the head section.
426
     *
427
     * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
428
     *
429
     * @return string the rendered content
430
     */
431 12
    protected function renderHeadHtml(): string
432
    {
433 12
        $lines = [];
434 12
        if (!empty($this->metaTags)) {
435 1
            $lines[] = implode("\n", $this->metaTags);
436
        }
437
438 12
        if (!empty($this->linkTags)) {
439 1
            $lines[] = implode("\n", $this->linkTags);
440
        }
441 12
        if (!empty($this->cssFiles)) {
442 1
            $lines[] = implode("\n", $this->cssFiles);
443
        }
444 12
        if (!empty($this->css)) {
445 1
            $lines[] = implode("\n", $this->css);
446
        }
447 12
        if (!empty($this->jsFiles[self::POSITION_HEAD])) {
448 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_HEAD]);
449
        }
450 12
        if (!empty($this->js[self::POSITION_HEAD])) {
451 1
            $lines[] = $this->generateJs($this->js[self::POSITION_HEAD]);
452
        }
453
454 12
        return empty($lines) ? '' : implode("\n", $lines);
455
    }
456
457
    /**
458
     * Renders the content to be inserted at the beginning of the body section.
459
     *
460
     * The content is rendered using the registered JS code blocks and files.
461
     *
462
     * @return string the rendered content
463
     */
464 12
    protected function renderBodyBeginHtml(): string
465
    {
466 12
        $lines = [];
467 12
        if (!empty($this->jsFiles[self::POSITION_BEGIN])) {
468 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_BEGIN]);
469
        }
470 12
        if (!empty($this->js[self::POSITION_BEGIN])) {
471
            $lines[] = $this->generateJs($this->js[self::POSITION_BEGIN]);
472
        }
473
474 12
        return empty($lines) ? '' : implode("\n", $lines);
475
    }
476
477
    /**
478
     * Renders the content to be inserted at the end of the body section.
479
     *
480
     * The content is rendered using the registered JS code blocks and files.
481
     *
482
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
483
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like normal
484
     * scripts.
485
     *
486
     * @return string the rendered content
487
     */
488 12
    protected function renderBodyEndHtml(bool $ajaxMode): string
489
    {
490 12
        $lines = [];
491
492 12
        if (!empty($this->jsFiles[self::POSITION_END])) {
493 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_END]);
494
        }
495
496 12
        if ($ajaxMode) {
497 3
            $scripts = array_merge(
498 3
                $this->js[self::POSITION_END] ?? [],
499 3
                $this->js[self::POSITION_READY] ?? [],
500 3
                $this->js[self::POSITION_LOAD] ?? [],
501
            );
502 3
            if (!empty($scripts)) {
503 3
                $lines[] = $this->generateJs($scripts);
504
            }
505
        } else {
506 9
            if (!empty($this->js[self::POSITION_END])) {
507 2
                $lines[] = $this->generateJs($this->js[self::POSITION_END]);
508
            }
509 9
            if (!empty($this->js[self::POSITION_READY])) {
510
                $js = "document.addEventListener('DOMContentLoaded', function(event) {\n" .
511 1
                    $this->generateJsWithoutTag($this->js[self::POSITION_READY]) .
512 1
                    "\n});";
513 1
                $lines[] = Html::script($js)->render();
514
            }
515 9
            if (!empty($this->js[self::POSITION_LOAD])) {
516
                $js = "window.addEventListener('load', function (event) {\n" .
517
                    $this->generateJsWithoutTag($this->js[self::POSITION_LOAD]) .
518
                    "\n});";
519
                $lines[] = Html::script($js)->render();
520
            }
521
        }
522
523 12
        return empty($lines) ? '' : implode("\n", $lines);
524
    }
525
526
    /**
527
     * Get title in views.
528
     *
529
     * in Layout:
530
     *
531
     * ```php
532
     * <title><?= Html::encode($this->getTitle()) ?></title>
533
     * ```
534
     *
535
     * in Views:
536
     *
537
     * ```php
538
     * $this->setTitle('Web Application - Yii 3.0.');
539
     * ```
540
     *
541
     * @return string
542
     */
543
    public function getTitle(): string
544
    {
545
        return $this->title;
546
    }
547
548
    /**
549
     * It processes the CSS configuration generated by the asset manager and converts it into HTML code.
550
     *
551
     * @param array $cssFiles
552
     */
553
    public function setCssFiles(array $cssFiles): void
554
    {
555
        foreach ($cssFiles as $key => $value) {
556
            $this->registerCssFile(
557
                $cssFiles[$key]['url'],
558
                $cssFiles[$key]['attributes']
559
            );
560
        }
561
    }
562
563
    /**
564
     * It processes the JS configuration generated by the asset manager and converts it into HTML code.
565
     *
566
     * @param array $jsFiles
567
     */
568
    public function setJsFiles(array $jsFiles): void
569
    {
570
        foreach ($jsFiles as $key => $value) {
571
            $this->registerJsFile(
572
                $jsFiles[$key]['url'],
573
                $jsFiles[$key]['attributes']
574
            );
575
        }
576
    }
577
578
    /**
579
     * It processes the JS strings generated by the asset manager.
580
     *
581
     * @param array $jsStrings
582
     */
583
    public function setJsStrings(array $jsStrings): void
584
    {
585
        foreach ($jsStrings as $value) {
586
            $this->registerJs(
587
                $value['string'],
588
                $value['attributes']['position']
589
            );
590
        }
591
    }
592
593
    /**
594
     * It processes the JS variables generated by the asset manager and converts it into JS code.
595
     *
596
     * @param array $jsVar
597
     */
598
    public function setJsVar(array $jsVar): void
599
    {
600
        foreach ($jsVar as $key => $value) {
601
            $this->registerJsVar(
602
                (string)$key,
603
                $value['variables'],
604
                $value['attributes']['position']
605
            );
606
        }
607
    }
608
609
    /**
610
     * Set title in views.
611
     *
612
     * {@see getTitle()}
613
     *
614
     * @param string $value
615
     */
616
    public function setTitle(string $value): void
617
    {
618
        $this->title = $value;
619
    }
620
621
    /**
622
     * @param Script[]|string[] $items
623
     */
624 4
    private function generateJs(array $items): string
625
    {
626 4
        $lines = [];
627
628 4
        $js = [];
629 4
        foreach ($items as $item) {
630 4
            if ($item instanceof Script) {
631 3
                if ($js !== []) {
632 2
                    $lines[] = Html::script(implode("\n", $js))->render();
633 2
                    $js = [];
634
                }
635 3
                $lines[] = $item->render();
636
            } else {
637 3
                $js[] = $item;
638
            }
639
        }
640 4
        if ($js !== []) {
641 2
            $lines[] = Html::script(implode("\n", $js))->render();
642
        }
643
644 4
        return implode("\n", $lines);
645
    }
646
647
    /**
648
     * @param Script[]|string[] $items
649
     */
650 1
    private function generateJsWithoutTag(array $items): string
651
    {
652 1
        $js = [];
653 1
        foreach ($items as $item) {
654 1
            $js[] = $item instanceof Script ? $item->getContent() : $item;
655
        }
656 1
        return implode("\n", $js);
657
    }
658
}
659