├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADE.md
├── composer-require-checker.json
├── composer.json
├── config
├── di-web.php
├── di.php
└── params.php
├── rector.php
└── src
├── Cache
├── CachedContent.php
└── DynamicContent.php
├── Event
├── AfterRenderEventInterface.php
├── View
│ ├── AfterRender.php
│ ├── BeforeRender.php
│ ├── PageBegin.php
│ ├── PageEnd.php
│ └── ViewEvent.php
└── WebView
│ ├── AfterRender.php
│ ├── BeforeRender.php
│ ├── BodyBegin.php
│ ├── BodyEnd.php
│ ├── Head.php
│ ├── PageBegin.php
│ ├── PageEnd.php
│ └── WebViewEvent.php
├── Exception
└── ViewNotFoundException.php
├── PhpTemplateRenderer.php
├── State
├── LocaleState.php
├── StateTrait.php
├── ThemeState.php
├── ViewState.php
└── WebViewState.php
├── TemplateRendererInterface.php
├── Theme.php
├── View.php
├── ViewContext.php
├── ViewContextInterface.php
├── ViewInterface.php
├── ViewTrait.php
└── WebView.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii View Change Log
2 |
3 | ## 12.2.1 under development
4 |
5 | - no changes in this release.
6 |
7 | ## 12.2.0 May 16, 2025
8 |
9 | - New #284: Allow to pass `Stringable` objects to `WebView::setTitle()` method (@vjik)
10 | - Bug #283: Allow using multiple theme paths in `yiisoft/view → theme → pathMap` package parameter (@mariovials, @vjik)
11 |
12 | ## 12.1.0 March 15, 2025
13 |
14 | - Chg #280: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@vjik)
15 | - Enh #282: Allow using `../` in the name of the view to refer to parent directory of the directory containing the view
16 | currently being rendered (@vjik)
17 | - Bug #280: Explicitly mark nullable parameters (@vjik)
18 | - Bug #282: Fix exception message when relative path is used without currently rendered view (@vjik)
19 |
20 | ## 12.0.0 December 23, 2024
21 |
22 | - New #278: Add `ViewInterface::deepClone()` method that clones object, including state cloning (@vjik)
23 | - Chg #276: Allow to pass `null` to `ViewInterface` methods `withBasePath()` and `withContext()` (@vjik)
24 | - Bug #279: Fix clearing theme in `View::withClearedState()` and `WebView::withClearedState()` (@vjik)
25 |
26 | ## 11.0.1 October 08, 2024
27 |
28 | - Enh #275: Make `psr/event-dispatcher` dependency optional (@vjik)
29 |
30 | ## 11.0.0 October 02, 2024
31 |
32 | - Chg #271: Remove deprecated methods `withDefaultExtension()` and `getDefaultExtension()` from `ViewInterface` (@vjik)
33 | - Chg #271: Rename configuration parameter `defaultExtension` to `fallbackExtension` (@vjik)
34 | - Chg #272: Add variadic parameter `$default` to `ViewInterface::getParameter()` (@vjik)
35 | - Enh #269: Bump PHP version to `^8.1` and refactor code (@vjik)
36 | - Enh #273: Use more specific psalm types in results of `WebView` methods: `getLinkTags()`, `getCss()`, `getCssFiles()`,
37 | `getJs()` and `getJsFiles()` (@vjik)
38 | - Bug #273: Fix empty string and "0" keys in `WebView` methods: `registerCss()`, `registerStyleTag()`,
39 | `registerCssFile()`, `registerJs()`, `registerScriptTag()` and `registerJsFile()` (@vjik)
40 |
41 | ## 10.0.0 June 28, 2024
42 |
43 | - Chg #266: Change logic of template file searching in `ViewInterface::render()` (@vjik)
44 | - Chg #266: Remove `ViewInterface::renderFile()` (@vjik)
45 | - Chg #266: When the view cannot be resolved in `ViewInterface::render()`, change exception from `RuntimeException` to
46 | `LogicException` (@vjik)
47 |
48 | ## 9.0.0 May 28, 2024
49 |
50 | - New #242: Add `View::getLocale()` and `WebView::getLocale()` methods (@Tigrov)
51 | - New #243: Add immutable method `ViewInterface::withTheme()` (@Gerych1984)
52 | - Chg #232: Deprecate `ViewInterface::withDefaultExtension()` and `ViewInterface::getDefaultExtension()` in favor of
53 | `withFallbackExtension()` and `getFallbackExtensions()` (@rustamwin)
54 | - Enh #226: Adjust config to make `View` and `WebView` more configurable (@rustamwin)
55 | - Enh #232, #233: Make fallback extension configurable & support multiple fallbacks (@rustamwin)
56 | - Enh #248: Add types to `ViewInterface::setParameter()` and `ViewInterface::addToParameter()` parameters (@vjik)
57 | - Enh #250: Make event dispatcher in `View` and `WebView` optional (@vjik)
58 | - Enh #251: Make base path in `View` and `WebView` optional (@vjik)
59 | - Bug #224: Fix signature of `CachedContent::cache()` (@vjik)
60 | - Bug #226: Fix `reset` config for referenced definitions (@rustamwin)
61 | - Bug #232: Fix render templates that contain dots in their name (@rustamwin)
62 |
63 | ## 8.0.0 February 16, 2023
64 |
65 | - Chg #219: Adapt configuration group names to Yii conventions (@vjik)
66 | - Enh #222: Add support for `yiisoft/cache` version `^3.0` (@vjik)
67 |
68 | ## 7.0.1 January 16, 2023
69 |
70 | - Chg: Allow `yiisoft/arrays` `^3.0` (@samdark)
71 |
72 | ## 7.0.0 December 06, 2022
73 |
74 | - Chg #211: Change return type of immutable methods in `ViewInterface` from `self` to `static` (@vjik)
75 | - Enh #211: Raise minimum PHP version to `^8.0` (@xepozz, @vjik)
76 | - Enh #213: Add support for `yiisoft/html` version `^3.0` (@vjik)
77 |
78 | ## 6.0.0 July 21, 2022
79 |
80 | - New #199: Add immutable method `ViewInterface::withLocale()` that set locale (@thenotsoft, @vjik, @samdark)
81 | - Chg #199: Renamed method `ViewInterface::setLanguage()` to `ViewInterface::setLocale()` (@thenotsoft, @samdark)
82 | - Chg #199: Renamed method `ViewInterface::withSourceLanguage()` to
83 | `ViewInterface::withSourceLocale()` (@thenotsoft, @samdark)
84 | - New #204: Add method `ViewInterface::withBasePath()` that set base path to the view directory (@thenotsoft, @vjik)
85 | - Chg #208: Add support for `yiisoft/files` version `^2.0` (@DplusG)
86 |
87 | ## 5.0.1 June 30, 2022
88 |
89 | - Enh #205: Add support for `yiisoft/cache` version `^2.0` (@vjik)
90 |
91 | ## 5.0.0 February 03, 2022
92 |
93 | - New #193: Add simple view context class `ViewContext` (@vjik)
94 | - New #193: Add method `ViewInterface::withContextPath()` that set view context path (@vjik)
95 | - New #194: Add method `ViewInterface::addToParameter()` that add value(s) to end of specified array parameter (@vjik)
96 | - New #195: Add method `ViewInterface::withClearedState()` that cleared state of view (parameters, blocks, etc.) (@vjik)
97 | - Chg #195: Mutable method `ViewInterface::setPlaceholderSalt()` replaced to immutable `withPlaceholderSalt()` (@vjik)
98 | - Chg #196: Renamed and made mutable methods of `ViewInterface`: `withTheme()` to `setTheme()`,
99 | `withLanguage()` to `setLanguage()` (@vjik)
100 | - Enh #195: Methods `removeParameter()` and `removeBlock()` of `ViewInterface` returns self (@vjik)
101 | - Enh #195: Methods of `WebView` returns self: `registerMeta()`, `registerMetaTag()`, `registerLink()`,
102 | `registerLinkTag()`, `registerCss()`, ` registerCssFromFile()`, ` registerStyleTag()`, ` registerCssFile()`,
103 | `addCssFiles()`, `addCssStrings()`, `registerJs()`, `registerScriptTag()`, `registerJsFile()`, `registerJsVar()`,
104 | `addJsFiles()`, `addJsStrings()`, `addJsVars()` (@vjik)
105 | - Bug #188: Use common state for cloned instances of `View` and `WebView` (@vjik)
106 | - Bug #195: Fix configuration: set parameters after reset `View` and `WebView` (@vjik)
107 |
108 | ## 4.0.0 October 25, 2021
109 |
110 | - Chg #185: Add interface `ViewInterface` that classes `View` and `WebView` implement (@vjik)
111 | - Enh #187: Improve exception message on getting not exist block or parameter in `View` and `WebView` (@vjik)
112 | - Bug #189: Flush currently being rendered view files on change context via `View::withContext()`
113 | or `WebView::withContext()` (@vjik)
114 |
115 | ## 3.0.2 October 25, 2021
116 |
117 | - Chg #190: Update the `yiisoft/arrays` dependency to `^2.0` (@vjik)
118 |
119 | ## 3.0.1 September 18, 2021
120 |
121 | - Bug: Fix incorrect method in `web` configuration (@vjik)
122 |
123 | ## 3.0.0 September 18, 2021
124 |
125 | - Сhg: In configuration `params.php` rename parameter `commonParameters` to `parameters` (@vjik)
126 | - Chg: Remove methods `View::withAddedCommonParameters()` and `WebView::withAddedCommonParameters()` (@vjik)
127 | - Chg: In classes `View` and `WebView` rename methods `setCommonParameters()` to `setParameters()`,
128 | `setCommonParameter()` to `setParameter()`, `removeCommonParameter()` to `removeParameter()`, `getCommonParameter()`
129 | to `getParameter()`, `hasCommonParameter()` to `hasParameter()` (@vjik)
130 | - Chg: Add fluent interface for setters in `View` and `WebView` classes (@vjik)
131 |
132 | ## 2.1.0 September 14, 2021
133 |
134 | - New #183: Add immutable methods `View::withAddedCommonParameters()` and `WebView::withAddedCommonParameters()` (@vjik)
135 |
136 | ## 2.0.1 August 30, 2021
137 |
138 | - Chg #182: Use definitions from `yiisoft/definitions` in configuration (@vjik)
139 |
140 | ## 2.0.0 August 24, 2021
141 |
142 | - Chg: Use `yiisoft/html` `^2.0` (@samdark)
143 |
144 | ## 1.0.1 August 20, 2021
145 |
146 | - New #177: Add second parameter to `View::getCommonParameter()` and `WebView::getCommonParameter()` for the default
147 | value to be returned if the specified parameter does not exist (@vjik)
148 | - Chg #176: Finalize classes `Yiisoft\View\Event\WebView\BeforeRender`, `Yiisoft\View\Event\WebView\BodyBegin`,
149 | `Yiisoft\View\Event\WebView\BodyEnd`, `Yiisoft\View\Event\WebView\PageBegin`, `Yiisoft\View\Event\WebView\PageEnd`,
150 | `Yiisoft\View\Exception\ViewNotFoundException` (@vjik)
151 |
152 | ## 1.0.0 July 05, 2021
153 |
154 | - Initial release.
155 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii View Rendering Library
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/view)
10 | [](https://packagist.org/packages/yiisoft/view)
11 | [](https://github.com/yiisoft/view/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/view)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/view/master)
14 | [](https://github.com/yiisoft/view/actions/workflows/static.yml?query=branch%3Amaster)
15 | [](https://shepherd.dev/github/yiisoft/view)
16 |
17 | This library provides templates rendering abstraction supporting layout-view-subview hierarchy, custom renderers with
18 | PHP-based as default, and more. It's used in [Yii Framework](https://www.yiiframework.com/) but is usable separately.
19 |
20 | ## Requirements
21 |
22 | - PHP 8.1 or higher.
23 |
24 | ## Installation
25 |
26 | The package could be installed with [Composer](https://getcomposer.org):
27 |
28 | ```shell
29 | composer require yiisoft/view
30 | ```
31 |
32 | ## General usage
33 |
34 | The package provides two use cases for managing view templates:
35 |
36 | - [Basic functionality](docs/guide/en/basic-functionality.md) for use in any environment.
37 | - Advanced functionality for [use in a web environment](docs/guide/en/use-in-web-environment.md).
38 |
39 | ### State of `View` and `WebView` services
40 |
41 | While being immutable and, by itself, stateless, both `View` and `WebView` services have sets of stateful and mutable
42 | data.
43 |
44 | `View` service:
45 | - parameters,
46 | - blocks,
47 | - theme,
48 | - locale.
49 |
50 | `WebView` service:
51 | - parameters,
52 | - blocks,
53 | - theme,
54 | - locale,
55 | - title,
56 | - meta and link tags,
57 | - JS/CSS strings,
58 | - JS/CSS files.
59 |
60 | The state of `View` and `WebView` isn't cloned when the services are cloned. So when
61 | using `with*()`, both new and old instances are sharing the same set of stateful mutable data. It allows, for example,
62 | to get `WebView` via type-hinting in a controller and change context path:
63 |
64 | ```php
65 | final class BlogController {
66 | private WebView $view;
67 | public function __construct (WebView $view) {
68 | $this->view = $view->withContextPath(__DIR__.'/views');
69 | }
70 | }
71 | ```
72 |
73 | and then register CSS in a widget:
74 |
75 | ```php
76 | final class LastPosts extends Widget
77 | {
78 | private WebView $view;
79 | public function __construct (WebView $view) {
80 | $this->view = $view;
81 | }
82 | protected function run(): string
83 | {
84 | ...
85 | $this->view->registerCss('.lastPosts { background: #f1f1f1; }');
86 | ...
87 | }
88 | }
89 | ```
90 |
91 | #### Locale state
92 |
93 | You can change the locale by using `setLocale()`, which will be applied to all other instances that used current state
94 | including existing ones. If you need to change the locale only for a single instance, you can use the immutable
95 | `withLocale()` method. Locale will be applied to all views rendered within views with `render()` calls.
96 |
97 | Example with mutable method:
98 |
99 | ```php
100 | final class LocaleMiddleware implements MiddlewareInterface
101 | {
102 | ...
103 | private WebView $view;
104 | ...
105 | public function __construct (
106 | ...
107 | WebView $view
108 | ...
109 | ) {
110 | $this->view = $view;
111 | }
112 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
113 | {
114 | ...
115 | $this->view->setLocale($locale);
116 | ...
117 | }
118 | }
119 | ```
120 |
121 | Example with immutable method:
122 |
123 | ```php
124 | final class BlogController {
125 | private WebView $view;
126 | public function __construct (WebView $view) {
127 | $this->view = $view;
128 | }
129 | public function index() {
130 | return $this->view->withLocale('es')->render('index');
131 | }
132 | }
133 | ```
134 |
135 | #### Reset state
136 |
137 | To get a deep cloned `View` or `WebView` with a reset state use `withClearedState()`:
138 |
139 | ```php
140 | $view = $view->withClearedState();
141 | ```
142 |
143 | #### Deep clone
144 |
145 | To get a deep cloned `View` or `WebView`, including state cloning, use `deepClone()` method:
146 |
147 | ```php
148 | $view = $view->deepClone();
149 | ```
150 |
151 | ## Extensions
152 |
153 | - [Yii View Renderer](https://github.com/yiisoft/yii-view-renderer) - a wrapper that's used in
154 | [Yii Framework](https://www.yiiframework.com/).
155 | Adds extra functionality for a web environment and compatibility
156 | with [PSR-7](https://www.php-fig.org/psr/psr-7) interfaces.
157 | - [yiisoft/view-twig](https://github.com/yiisoft/view-twig) - an extension that provides a view renderer that will
158 | allow you to use the [Twig](https://twig.symfony.com) view template engine, instead of the default PHP renderer.
159 |
160 | ## Documentation
161 |
162 | - Guide: [English](docs/guide/en/README.md), [Português - Brasil](docs/guide/pt-BR/README.md)
163 | - [Internals](docs/internals.md)
164 |
165 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that.
166 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
167 |
168 | ## License
169 |
170 | The Yii View Rendering Library is free software. It's released under the terms of the BSD License.
171 | Please see [`LICENSE`](./LICENSE.md) for more information.
172 |
173 | Maintained by [Yii Software](https://www.yiiframework.com/).
174 |
175 | ### Support the project
176 |
177 | [](https://opencollective.com/yiisoft)
178 |
179 | ### Follow updates
180 |
181 | [](https://www.yiiframework.com/)
182 | [](https://twitter.com/yiiframework)
183 | [](https://t.me/yii3en)
184 | [](https://www.facebook.com/groups/yiitalk)
185 | [](https://yiiframework.com/go/slack)
186 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | # Upgrading Instructions for Yii View
2 |
3 | This file contains the upgrade notes. These notes highlight changes that could break your
4 | application when you upgrade the package from one version to another.
5 |
6 | > **Important!** The following upgrading instructions are cumulative. That is, if you want
7 | > to upgrade from version A to version C and there is version B between A and C, you need
8 | > to following the instructions for both A and B.
9 |
10 | ## Upgrade from 11.x
11 |
12 | - `ViewInterface` was changed. You should adjust it in your own implementations.
13 | - Added `deepClone()` method.
14 | - Allowed to pass `null` to `withBasePath()` and `withContext()` methods.
15 |
16 | ## Upgrade from 10.x
17 |
18 | - Removed `ViewInterface` methods `withDefaultExtension()` and `getDefaultExtension()`. Use `withFallbackExtension()`
19 | and `getFallbackExtensions()` instead, respectively.
20 | - Rename configuration parameter `defaultExtension` to `fallbackExtension`.
21 | - Added variadic parameter `$default` to `ViewInterface::getParameter()`.
22 |
23 | ## Upgrade from 9.x
24 |
25 | - Use `render()` method instead of `renderFile()` in `View` And `WebView` classes.
26 | - Changed logic of template file searching in `ViewInterface::render()`, view name can be:
27 | - the absolute path to the view file, e.g. "/path/to/view.php";
28 | - the name of the view starting with `//` to join the base path, e.g. "//site/index";
29 | - the name of the view starting with `./` to join the directory containing the view currently being rendered
30 | (i.e., this happens when rendering a view within another view), e.g. "./widget";
31 | - the name of the view without the starting `//` or `./` (e.g. "site/index"), so view file will be
32 | looked for under the view path of the context set via `withContext()` (if the context instance was not set
33 | `withContext()`, it will be looked for under the base path).
34 |
--------------------------------------------------------------------------------
/composer-require-checker.json:
--------------------------------------------------------------------------------
1 | {
2 | "symbol-whitelist": [
3 | "Psr\\EventDispatcher\\StoppableEventInterface"
4 | ],
5 | "php-core-extensions": [
6 | "Core",
7 | "date",
8 | "json",
9 | "hash",
10 | "pcre",
11 | "Phar",
12 | "Reflection",
13 | "SPL",
14 | "random",
15 | "standard"
16 | ],
17 | "scan-files": []
18 | }
19 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/view",
3 | "type": "library",
4 | "description": "Yii View Rendering Library",
5 | "keywords": [
6 | "yii",
7 | "view"
8 | ],
9 | "homepage": "https://www.yiiframework.com/",
10 | "license": "BSD-3-Clause",
11 | "support": {
12 | "issues": "https://github.com/yiisoft/view/issues?state=open",
13 | "source": "https://github.com/yiisoft/view",
14 | "forum": "https://www.yiiframework.com/forum/",
15 | "wiki": "https://www.yiiframework.com/wiki/",
16 | "irc": "ircs://irc.libera.chat:6697/yii",
17 | "chat": "https://t.me/yii3en"
18 | },
19 | "funding": [
20 | {
21 | "type": "opencollective",
22 | "url": "https://opencollective.com/yiisoft"
23 | },
24 | {
25 | "type": "github",
26 | "url": "https://github.com/sponsors/yiisoft"
27 | }
28 | ],
29 | "require": {
30 | "php": "8.1 - 8.4",
31 | "yiisoft/arrays": "^2.0 || ^3.0",
32 | "yiisoft/cache": "^1.0 || ^2.0 || ^3.0",
33 | "yiisoft/files": "^1.0 || ^2.0",
34 | "yiisoft/html": "^2.5 || ^3.0",
35 | "yiisoft/json": "^1.0"
36 | },
37 | "require-dev": {
38 | "maglnet/composer-require-checker": "^4.7.1",
39 | "phpunit/phpunit": "^10.5.46",
40 | "rector/rector": "^2.0.16",
41 | "roave/infection-static-analysis-plugin": "^1.35",
42 | "spatie/phpunit-watcher": "^1.24",
43 | "vimeo/psalm": "^5.26.1 || ^6.8.9",
44 | "yiisoft/aliases": "^3.0",
45 | "yiisoft/di": "^1.3",
46 | "yiisoft/psr-dummy-provider": "^1.0.2",
47 | "yiisoft/test-support": "^3.0.2"
48 | },
49 | "extra": {
50 | "config-plugin-options": {
51 | "source-directory": "config"
52 | },
53 | "config-plugin": {
54 | "params": "params.php",
55 | "di": "di.php",
56 | "di-web": "di-web.php"
57 | }
58 | },
59 | "suggestion": {
60 | "psr/event-dispatcher": "Use any PSR-compatible event dispatcher to dispatch `View`/`WebView` events."
61 | },
62 | "autoload": {
63 | "psr-4": {
64 | "Yiisoft\\View\\": "src"
65 | }
66 | },
67 | "autoload-dev": {
68 | "psr-4": {
69 | "Yiisoft\\View\\Tests\\": "tests"
70 | }
71 | },
72 | "config": {
73 | "sort-packages": true,
74 | "allow-plugins": {
75 | "infection/extension-installer": true,
76 | "composer/package-versions-deprecated": true
77 | }
78 | },
79 | "scripts": {
80 | "test": "phpunit --testdox --no-interaction",
81 | "test-watch": "phpunit-watcher watch"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/config/di-web.php:
--------------------------------------------------------------------------------
1 | static function (Aliases $aliases) use ($params) {
16 | $pathMap = [];
17 |
18 | foreach ($params['yiisoft/view']['theme']['pathMap'] as $key => $value) {
19 | $pathMap[$aliases->get($key)] = is_array($value)
20 | ? $aliases->getArray($value)
21 | : $aliases->get($value);
22 | }
23 |
24 | return new Theme(
25 | $pathMap,
26 | $params['yiisoft/view']['theme']['basePath'],
27 | $params['yiisoft/view']['theme']['baseUrl']
28 | );
29 | },
30 |
31 | WebView::class => [
32 | '__construct()' => [
33 | 'basePath' => DynamicReference::to(
34 | static fn (Aliases $aliases) => $aliases->get($params['yiisoft/view']['basePath'])
35 | ),
36 | ],
37 | 'setParameters()' => [$params['yiisoft/view']['parameters']],
38 | 'withRenderers()' => [$params['yiisoft/view']['renderers']],
39 | 'withFallbackExtension()' => [...(array) $params['yiisoft/view']['fallbackExtension']],
40 | 'reset' => function (ContainerInterface $container) use ($params) {
41 | /** @var WebView $this */
42 | $this->clear();
43 | $parameters = $params['yiisoft/view']['parameters'];
44 | foreach ($parameters as $name => $parameter) {
45 | $parameters[$name] = $parameter instanceof ReferenceInterface ?
46 | $parameter->resolve($container) :
47 | $parameter;
48 | }
49 | $this->setParameters($parameters);
50 | },
51 | ],
52 | ];
53 |
--------------------------------------------------------------------------------
/config/di.php:
--------------------------------------------------------------------------------
1 | [
15 | '__construct()' => [
16 | 'basePath' => DynamicReference::to(
17 | static fn (Aliases $aliases) => $aliases->get($params['yiisoft/view']['basePath'])
18 | ),
19 | ],
20 | 'setParameters()' => [$params['yiisoft/view']['parameters']],
21 | 'withRenderers()' => [$params['yiisoft/view']['renderers']],
22 | 'withFallbackExtension()' => [...(array) $params['yiisoft/view']['fallbackExtension']],
23 | 'reset' => function (ContainerInterface $container) use ($params) {
24 | /** @var View $this */
25 | $this->clear();
26 | $parameters = $params['yiisoft/view']['parameters'];
27 | foreach ($parameters as $name => $parameter) {
28 | $parameters[$name] = $parameter instanceof ReferenceInterface ?
29 | $parameter->resolve($container) :
30 | $parameter;
31 | }
32 | $this->setParameters($parameters);
33 | },
34 | ],
35 | ];
36 |
--------------------------------------------------------------------------------
/config/params.php:
--------------------------------------------------------------------------------
1 | [
7 | 'basePath' => '',
8 | 'parameters' => [],
9 | 'theme' => [
10 | 'pathMap' => [],
11 | 'basePath' => '',
12 | 'baseUrl' => '',
13 | ],
14 | 'renderers' => [],
15 | 'fallbackExtension' => 'php', // Available array also, e.g. ['php', 'tpl']
16 | ],
17 | ];
18 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | withPaths([
12 | __DIR__ . '/src',
13 | __DIR__ . '/tests',
14 | ])
15 | ->withPhpSets(php81: true)
16 | ->withRules([
17 | InlineConstructorDefaultToPropertyRector::class,
18 | ])
19 | ->withSkip([
20 | __DIR__ . '/tests/public/view/parameters.php',
21 | RemoveExtraParametersRector::class => [__DIR__ . '/src/PhpTemplateRenderer.php'],
22 | NullToStrictStringFuncCallArgRector::class,
23 | ]);
24 |
--------------------------------------------------------------------------------
/src/Cache/CachedContent.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | private array $dynamicContents = [];
29 |
30 | /**
31 | * @var string[]
32 | */
33 | private array $variations = [];
34 |
35 | /**
36 | * @param string $id The unique identifier of the cached content.
37 | * @param CacheInterface $cache The cache instance.
38 | * @param DynamicContent[] $dynamicContents The dynamic content instances.
39 | * @param string[] $variations List of string factors that would cause the variation of the content being cached.
40 | */
41 | public function __construct(
42 | private readonly string $id,
43 | private readonly CacheInterface $cache,
44 | array $dynamicContents = [],
45 | array $variations = []
46 | ) {
47 | $this->cacheKeyNormalizer = new CacheKeyNormalizer();
48 | $this->setDynamicContents($dynamicContents);
49 | $this->setVariations($variations);
50 | }
51 |
52 | /**
53 | * Caches, replaces placeholders with actual dynamic content, and returns the full actual content.
54 | *
55 | * @param string $content The content of the item to cache store.
56 | * @param DateInterval|int|null $ttl The TTL of the cached content.
57 | * @param Dependency|null $dependency The dependency of the cached content.
58 | * @param float $beta The value for calculating the range that is used for "Probably early expiration".
59 | *
60 | * @see CacheInterface::getOrSet()
61 | *
62 | * @return string The rendered cached content.
63 | */
64 | public function cache(
65 | string $content,
66 | DateInterval|int|null $ttl = 60,
67 | Dependency|null $dependency = null,
68 | float $beta = 1.0
69 | ): string {
70 | /** @psalm-suppress MixedArgument */
71 | return $this->replaceDynamicPlaceholders(
72 | $this->cache->getOrSet($this->cacheKey(), static fn(): string => $content, $ttl, $dependency, $beta),
73 | );
74 | }
75 |
76 | /**
77 | * Returns cached content with placeholders replaced with actual dynamic content.
78 | *
79 | * @return string|null The cached content. Null is returned if valid content is not found in the cache.
80 | */
81 | public function get(): ?string
82 | {
83 | /** @var string|null $content */
84 | $content = $this->cache
85 | ->psr()
86 | ->get($this->cacheKey());
87 |
88 | if ($content === null) {
89 | return null;
90 | }
91 |
92 | return $this->replaceDynamicPlaceholders($content);
93 | }
94 |
95 | /**
96 | * Generates a unique key used for storing the content in cache.
97 | *
98 | * @return string A valid cache key.
99 | */
100 | private function cacheKey(): string
101 | {
102 | return $this->cacheKeyNormalizer->normalize(array_merge([self::class, $this->id], $this->variations));
103 | }
104 |
105 | /**
106 | * Replaces placeholders with actual dynamic content.
107 | *
108 | * @param string $content The content to be replaced.
109 | *
110 | * @return string The content with replaced placeholders.
111 | */
112 | private function replaceDynamicPlaceholders(string $content): string
113 | {
114 | $dynamicContents = [];
115 |
116 | foreach ($this->dynamicContents as $dynamicContent) {
117 | $dynamicContents[$dynamicContent->placeholder()] = $dynamicContent->content();
118 | }
119 |
120 | if (!empty($dynamicContents)) {
121 | $content = strtr($content, $dynamicContents);
122 | }
123 |
124 | return $content;
125 | }
126 |
127 | /**
128 | * Sets dynamic content instances.
129 | *
130 | * @param array $dynamicContents The dynamic content instances to set.
131 | */
132 | private function setDynamicContents(array $dynamicContents): void
133 | {
134 | foreach ($dynamicContents as $dynamicContent) {
135 | if (!($dynamicContent instanceof DynamicContent)) {
136 | throw new InvalidArgumentException(sprintf(
137 | 'Invalid dynamic content "%s" specified. It must be a "%s" instance.',
138 | get_debug_type($dynamicContent),
139 | DynamicContent::class,
140 | ));
141 | }
142 |
143 | $this->dynamicContents[$dynamicContent->id()] = $dynamicContent;
144 | }
145 | }
146 |
147 | /**
148 | * Sets variations.
149 | *
150 | * @param array $variations The variations to set.
151 | */
152 | private function setVariations(array $variations): void
153 | {
154 | foreach ($variations as $variation) {
155 | if (!is_string($variation)) {
156 | throw new InvalidArgumentException(sprintf(
157 | 'Invalid variation "%s" specified. It must be a string type.',
158 | get_debug_type($variation),
159 | ));
160 | }
161 |
162 | $this->variations[] = $variation;
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/Cache/DynamicContent.php:
--------------------------------------------------------------------------------
1 | contentGenerator = $contentGenerator;
28 | }
29 |
30 | /**
31 | * Returns a unique identifier of the dynamic content.
32 | *
33 | * @return string The unique identifier of the dynamic content.
34 | */
35 | public function id(): string
36 | {
37 | return $this->id;
38 | }
39 |
40 | /**
41 | * Generates the dynamic content.
42 | *
43 | * @return string The generated dynamic content.
44 | *
45 | * @psalm-suppress MixedInferredReturnType, MixedReturnStatement
46 | */
47 | public function content(): string
48 | {
49 | return ($this->contentGenerator)($this->parameters);
50 | }
51 |
52 | /**
53 | * Returns the placeholder of the dynamic content.
54 | *
55 | * @return string The placeholder of the dynamic content.
56 | */
57 | public function placeholder(): string
58 | {
59 | return "id]]>";
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Event/AfterRenderEventInterface.php:
--------------------------------------------------------------------------------
1 | view;
30 | }
31 |
32 | public function getFile(): string
33 | {
34 | return $this->file;
35 | }
36 |
37 | public function getParameters(): array
38 | {
39 | return $this->parameters;
40 | }
41 |
42 | public function getResult(): string
43 | {
44 | return $this->result;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Event/View/BeforeRender.php:
--------------------------------------------------------------------------------
1 | stopPropagation = true;
31 | }
32 |
33 | public function isPropagationStopped(): bool
34 | {
35 | return $this->stopPropagation;
36 | }
37 |
38 | public function getView(): View
39 | {
40 | return $this->view;
41 | }
42 |
43 | public function getFile(): string
44 | {
45 | return $this->file;
46 | }
47 |
48 | public function getParameters(): array
49 | {
50 | return $this->parameters;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Event/View/PageBegin.php:
--------------------------------------------------------------------------------
1 | view;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Event/WebView/AfterRender.php:
--------------------------------------------------------------------------------
1 | view;
30 | }
31 |
32 | public function getFile(): string
33 | {
34 | return $this->file;
35 | }
36 |
37 | public function getParameters(): array
38 | {
39 | return $this->parameters;
40 | }
41 |
42 | public function getResult(): string
43 | {
44 | return $this->result;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Event/WebView/BeforeRender.php:
--------------------------------------------------------------------------------
1 | stopPropagation = true;
31 | }
32 |
33 | public function isPropagationStopped(): bool
34 | {
35 | return $this->stopPropagation;
36 | }
37 |
38 | public function getView(): WebView
39 | {
40 | return $this->view;
41 | }
42 |
43 | public function getFile(): string
44 | {
45 | return $this->file;
46 | }
47 |
48 | public function getParameters(): array
49 | {
50 | return $this->parameters;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Event/WebView/BodyBegin.php:
--------------------------------------------------------------------------------
1 | view;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/ViewNotFoundException.php:
--------------------------------------------------------------------------------
1 | bindTo($view)($template, $parameters);
37 |
38 | /**
39 | * @var string We assume that in this case active output buffer is always existed, so `ob_get_clean()`
40 | * returns a string.
41 | */
42 | return ob_get_clean();
43 | } catch (Throwable $e) {
44 | while (ob_get_level() > $obInitialLevel) {
45 | ob_end_clean();
46 | }
47 | throw $e;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/State/LocaleState.php:
--------------------------------------------------------------------------------
1 | locale = $locale;
25 | return $this;
26 | }
27 |
28 | /**
29 | * Gets the locale code.
30 | *
31 | * @return string The locale code.
32 | */
33 | public function getLocale(): string
34 | {
35 | return $this->locale;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/State/StateTrait.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | private array $parameters = [];
21 |
22 | /**
23 | * @var array Named content blocks that are common for all view templates.
24 | * @psalm-var array
25 | */
26 | private array $blocks = [];
27 |
28 | /**
29 | * Sets a common parameters that is accessible in all view templates.
30 | *
31 | * @param array $parameters Parameters that are common for all view templates.
32 | *
33 | * @psalm-param array $parameters
34 | *
35 | * @see setParameter()
36 | */
37 | public function setParameters(array $parameters): static
38 | {
39 | /** @var mixed $value */
40 | foreach ($parameters as $id => $value) {
41 | $this->setParameter($id, $value);
42 | }
43 | return $this;
44 | }
45 |
46 | /**
47 | * Sets a common parameter that is accessible in all view templates.
48 | *
49 | * @param string $id The unique identifier of the parameter.
50 | * @param mixed $value The value of the parameter.
51 | */
52 | public function setParameter(string $id, mixed $value): static
53 | {
54 | $this->parameters[$id] = $value;
55 | return $this;
56 | }
57 |
58 | /**
59 | * Add values to end of common array parameter. If specified parameter does not exist or him is not array,
60 | * then parameter will be added as empty array.
61 | *
62 | * @param string $id The unique identifier of the parameter.
63 | * @param mixed ...$value Value(s) for add to end of array parameter.
64 | *
65 | * @throws InvalidArgumentException When specified parameter already exists and is not an array.
66 | */
67 | public function addToParameter(string $id, mixed ...$value): static
68 | {
69 | /** @var mixed $array */
70 | $array = $this->parameters[$id] ?? [];
71 | if (!is_array($array)) {
72 | throw new InvalidArgumentException(
73 | sprintf('The "%s" parameter already exists and is not an array.', $id)
74 | );
75 | }
76 |
77 | $this->setParameter($id, array_merge($array, $value));
78 |
79 | return $this;
80 | }
81 |
82 | /**
83 | * Removes a common parameter.
84 | *
85 | * @param string $id The unique identifier of the parameter.
86 | */
87 | public function removeParameter(string $id): static
88 | {
89 | unset($this->parameters[$id]);
90 | return $this;
91 | }
92 |
93 | /**
94 | * Gets a common parameter value by ID.
95 | *
96 | * @param string $id The unique identifier of the parameter.
97 | * @param mixed $default The default value to be returned if the specified parameter does not exist.
98 | *
99 | * @throws InvalidArgumentException If specified parameter does not exist and not passed default value.
100 | *
101 | * @return mixed The value of the parameter.
102 | */
103 | public function getParameter(string $id, mixed ...$default): mixed
104 | {
105 | if (isset($this->parameters[$id])) {
106 | return $this->parameters[$id];
107 | }
108 |
109 | if (!empty($default)) {
110 | return reset($default);
111 | }
112 |
113 | throw new InvalidArgumentException('Parameter "' . $id . '" not found.');
114 | }
115 |
116 | public function getParameters(): array
117 | {
118 | return $this->parameters;
119 | }
120 |
121 | /**
122 | * Checks the existence of a common parameter by ID.
123 | *
124 | * @param string $id The unique identifier of the parameter.
125 | *
126 | * @return bool Whether a custom parameter that is common for all view templates exists.
127 | */
128 | public function hasParameter(string $id): bool
129 | {
130 | return isset($this->parameters[$id]);
131 | }
132 |
133 | /**
134 | * Sets a content block.
135 | *
136 | * @param string $id The unique identifier of the block.
137 | * @param string $content The content of the block.
138 | */
139 | public function setBlock(string $id, string $content): static
140 | {
141 | $this->blocks[$id] = $content;
142 | return $this;
143 | }
144 |
145 | /**
146 | * Removes a content block.
147 | *
148 | * @param string $id The unique identifier of the block.
149 | */
150 | public function removeBlock(string $id): static
151 | {
152 | unset($this->blocks[$id]);
153 | return $this;
154 | }
155 |
156 | /**
157 | * Gets content of the block by ID.
158 | *
159 | * @param string $id The unique identifier of the block.
160 | *
161 | * @return string The content of the block.
162 | */
163 | public function getBlock(string $id): string
164 | {
165 | if (isset($this->blocks[$id])) {
166 | return $this->blocks[$id];
167 | }
168 |
169 | throw new InvalidArgumentException('Block "' . $id . '" not found.');
170 | }
171 |
172 | /**
173 | * Checks the existence of a content block by ID.
174 | *
175 | * @param string $id The unique identifier of the block.
176 | *
177 | * @return bool Whether a content block exists.
178 | */
179 | public function hasBlock(string $id): bool
180 | {
181 | return isset($this->blocks[$id]);
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/State/ThemeState.php:
--------------------------------------------------------------------------------
1 | theme = $theme;
27 |
28 | return $this;
29 | }
30 |
31 | /**
32 | * Gets the theme instance, or `null` if no theme has been set.
33 | *
34 | * @return Theme|null The theme instance, or `null` if no theme has been set.
35 | */
36 | public function getTheme(): ?Theme
37 | {
38 | return $this->theme;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/State/ViewState.php:
--------------------------------------------------------------------------------
1 | parameters = [];
17 | $this->blocks = [];
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/State/WebViewState.php:
--------------------------------------------------------------------------------
1 | >
45 | *
46 | * @see registerLink()
47 | * @see registerLinkTag()
48 | */
49 | private array $linkTags = [];
50 |
51 | /**
52 | * @var array The registered CSS code blocks.
53 | * @psalm-var array>
54 | *
55 | * {@see registerCss()}
56 | */
57 | private array $css = [];
58 |
59 | /**
60 | * @var array The registered CSS files.
61 | * @psalm-var array>
62 | *
63 | * {@see registerCssFile()}
64 | */
65 | private array $cssFiles = [];
66 |
67 | /**
68 | * @var array The registered JS code blocks
69 | * @psalm-var array>
70 | *
71 | * {@see registerJs()}
72 | */
73 | private array $js = [];
74 |
75 | /**
76 | * @var array The registered JS files.
77 | * @psalm-var array>
78 | *
79 | * {@see registerJsFile()}
80 | */
81 | private array $jsFiles = [];
82 |
83 | /**
84 | * Get title in views.
85 | */
86 | public function getTitle(): string
87 | {
88 | return $this->title;
89 | }
90 |
91 | /**
92 | * @return Meta[] The registered meta tags.
93 | */
94 | public function getMetaTags(): array
95 | {
96 | return $this->metaTags;
97 | }
98 |
99 | /**
100 | * @return array The registered link tags.
101 | * @psalm-return array>
102 | */
103 | public function getLinkTags(): array
104 | {
105 | return $this->linkTags;
106 | }
107 |
108 | /**
109 | * @return array The registered CSS code blocks.
110 | * @psalm-return array>
111 | */
112 | public function getCss(): array
113 | {
114 | return $this->css;
115 | }
116 |
117 | /**
118 | * @return array The registered CSS files.
119 | * @psalm-return array>
120 | */
121 | public function getCssFiles(): array
122 | {
123 | return $this->cssFiles;
124 | }
125 |
126 | /**
127 | * @return array The registered JS code blocks
128 | * @psalm-return array>
129 | */
130 | public function getJs(): array
131 | {
132 | return $this->js;
133 | }
134 |
135 | /**
136 | * @return array The registered JS files.
137 | * @psalm-return array>
138 | */
139 | public function getJsFiles(): array
140 | {
141 | return $this->jsFiles;
142 | }
143 |
144 | /**
145 | * Set title in views.
146 | *
147 | * {@see getTitle()}
148 | */
149 | public function setTitle(string $value): self
150 | {
151 | $this->title = $value;
152 | return $this;
153 | }
154 |
155 | /**
156 | * Registers a meta tag.
157 | *
158 | * For example, a description meta tag can be added like the following:
159 | *
160 | * ```php
161 | * $state->registerMeta([
162 | * 'name' => 'description',
163 | * 'content' => 'This website is about funny raccoons.'
164 | * ]);
165 | * ```
166 | *
167 | * @param array $attributes The HTML attributes for the meta tag.
168 | * @param string|null $key The key that identifies the meta tag. If two meta tags are registered with the same key,
169 | * the latter will overwrite the former. If this is null, the new meta tag will be appended to the existing ones.
170 | */
171 | public function registerMeta(array $attributes, ?string $key = null): void
172 | {
173 | $this->registerMetaTag(Html::meta($attributes), $key);
174 | }
175 |
176 | /**
177 | * Registers a {@see Meta} tag.
178 | *
179 | * @see registerMeta()
180 | */
181 | public function registerMetaTag(Meta $meta, ?string $key = null): void
182 | {
183 | $key === null
184 | ? $this->metaTags[] = $meta
185 | : $this->metaTags[$key] = $meta;
186 | }
187 |
188 | /**
189 | * Registers a link tag.
190 | *
191 | * For example, a link tag for a custom [favicon](https://www.w3.org/2005/10/howto-favicon) can be added like the
192 | * following:
193 | *
194 | * ```php
195 | * $view->registerLink(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
196 | * ```
197 | *
198 | * **Note:** To register link tags for CSS stylesheets, use {@see registerCssFile()]} instead, which has more
199 | * options for this kind of link tag.
200 | *
201 | * @param array $attributes The HTML attributes for the link tag.
202 | * @param int $position The position at which the link tag should be inserted in a page.
203 | * @param string|null $key The key that identifies the link tag. If two link tags are registered with the same key,
204 | * the latter will overwrite the former. If this is null, the new link tag will be appended to the existing ones.
205 | */
206 | public function registerLink(array $attributes, int $position = WebView::POSITION_HEAD, ?string $key = null): void
207 | {
208 | $this->registerLinkTag(Html::link()->attributes($attributes), $position, $key);
209 | }
210 |
211 | /**
212 | * Registers a {@see Link} tag.
213 | *
214 | * @see registerLink()
215 | */
216 | public function registerLinkTag(Link $link, int $position = WebView::POSITION_HEAD, ?string $key = null): void
217 | {
218 | $key === null
219 | ? $this->linkTags[$position][] = $link
220 | : $this->linkTags[$position][$key] = $link;
221 | }
222 |
223 | /**
224 | * Registers a CSS code block.
225 | *
226 | * @param string $css The content of the CSS code block to be registered.
227 | * @param array $attributes The HTML attributes for the {@see Style} tag.
228 | * @param string|null $key The key that identifies the CSS code block. If `null`, it will use `$css` as the key.
229 | * If two CSS code blocks are registered with the same key, the latter will overwrite the former.
230 | */
231 | public function registerCss(
232 | string $css,
233 | int $position = WebView::POSITION_HEAD,
234 | array $attributes = [],
235 | ?string $key = null
236 | ): void {
237 | $this->css[$position][$key ?? md5($css)] = $attributes === [] ? $css : Html::style($css, $attributes);
238 | }
239 |
240 | /**
241 | * Registers a CSS code block from file.
242 | *
243 | * @param string $path The path or URL to CSS file.
244 | *
245 | * @see registerCss()
246 | */
247 | public function registerCssFromFile(
248 | string $path,
249 | int $position = WebView::POSITION_HEAD,
250 | array $attributes = [],
251 | ?string $key = null
252 | ): void {
253 | $css = file_get_contents($path);
254 | if ($css === false) {
255 | throw new RuntimeException(sprintf('File %s could not be read.', $path));
256 | }
257 |
258 | $this->registerCss($css, $position, $attributes, $key);
259 | }
260 |
261 | /**
262 | * Register a {@see Style} tag.
263 | *
264 | * @see registerJs()
265 | */
266 | public function registerStyleTag(Style $style, int $position = WebView::POSITION_HEAD, ?string $key = null): void
267 | {
268 | $this->css[$position][$key ?? md5($style->render())] = $style;
269 | }
270 |
271 | /**
272 | * Registers a CSS file.
273 | *
274 | * This method should be used for simple registration of CSS files. If you want to use features of
275 | * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
276 | * {@see \Yiisoft\Assets\AssetBundle}.
277 | *
278 | * @param string $url The CSS file to be registered.
279 | * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
280 | * for the supported options.
281 | * @param string|null $key The key that identifies the CSS script file. If `null`, it will use `$url` as the key.
282 | * If two CSS files are registered with the same key, the latter will overwrite the former.
283 | */
284 | public function registerCssFile(
285 | string $url,
286 | int $position = WebView::POSITION_HEAD,
287 | array $options = [],
288 | ?string $key = null
289 | ): void {
290 | if (!$this->isValidCssPosition($position)) {
291 | throw new InvalidArgumentException('Invalid position of CSS file.');
292 | }
293 |
294 | $this->cssFiles[$position][$key ?? $url] = Html::cssFile($url, $options)->render();
295 | }
296 |
297 | /**
298 | * It processes the CSS configuration generated by the asset manager and converts it into HTML code.
299 | */
300 | public function addCssFiles(array $cssFiles): void
301 | {
302 | /** @var mixed $value */
303 | foreach ($cssFiles as $key => $value) {
304 | $this->registerCssFileByConfig(
305 | is_string($key) ? $key : null,
306 | is_array($value) ? $value : [$value],
307 | );
308 | }
309 | }
310 |
311 | /**
312 | * It processes the CSS strings generated by the asset manager.
313 | */
314 | public function addCssStrings(array $cssStrings): void
315 | {
316 | /** @var mixed $value */
317 | foreach ($cssStrings as $key => $value) {
318 | $this->registerCssStringByConfig(
319 | is_string($key) ? $key : null,
320 | is_array($value) ? $value : [$value, WebView::POSITION_HEAD],
321 | );
322 | }
323 | }
324 |
325 | /**
326 | * Registers a JS code block.
327 | *
328 | * @param string $js the JS code block to be registered
329 | * @param int $position the position at which the JS script tag should be inserted in a page.
330 | *
331 | * The possible values are:
332 | *
333 | * - {@see WebView::POSITION_HEAD}: in the head section
334 | * - {@see WebView::POSITION_BEGIN}: at the beginning of the body section
335 | * - {@see WebView::POSITION_END}: at the end of the body section. This is the default value.
336 | * - {@see WebView::POSITION_LOAD}: executed when HTML page is completely loaded.
337 | * - {@see WebView::POSITION_READY}: executed when HTML document composition is ready.
338 | * @param string|null $key The key that identifies the JS code block. If `null`, it will use `$js` as the key.
339 | * If two JS code blocks are registered with the same key, the latter will overwrite the former.
340 | */
341 | public function registerJs(string $js, int $position = WebView::POSITION_END, ?string $key = null): void
342 | {
343 | $this->js[$position][$key ?? md5($js)] = $js;
344 | }
345 |
346 | /**
347 | * Register a `script` tag
348 | *
349 | * @see registerJs()
350 | */
351 | public function registerScriptTag(Script $script, int $position = WebView::POSITION_END, ?string $key = null): void
352 | {
353 | $this->js[$position][$key ?? md5($script->render())] = $script;
354 | }
355 |
356 | /**
357 | * Registers a JS file.
358 | *
359 | * This method should be used for simple registration of JS files. If you want to use features of
360 | * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
361 | * {@see \Yiisoft\Assets\AssetBundle}.
362 | *
363 | * @param string $url The JS file to be registered.
364 | * @param array $options The HTML attributes for the script tag. The following options are specially handled and
365 | * are not treated as HTML attributes:
366 | *
367 | * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
368 | * * {@see WebView::POSITION_HEAD}: in the head section
369 | * * {@see WebView::POSITION_BEGIN}: at the beginning of the body section
370 | * * {@see WebView::POSITION_END}: at the end of the body section. This is the default value.
371 | *
372 | * Please refer to {@see \Yiisoft\Html\Html::javaScriptFile()} for other supported options.
373 | * @param string|null $key The key that identifies the JS script file. If null, it will use $url as the key.
374 | * If two JS files are registered with the same key at the same position, the latter will overwrite the former.
375 | * Note that position option takes precedence, thus files registered with the same key, but different
376 | * position option will not override each other.
377 | */
378 | public function registerJsFile(
379 | string $url,
380 | int $position = WebView::POSITION_END,
381 | array $options = [],
382 | ?string $key = null
383 | ): void {
384 | if (!$this->isValidJsPosition($position)) {
385 | throw new InvalidArgumentException('Invalid position of JS file.');
386 | }
387 |
388 | $this->jsFiles[$position][$key ?? $url] = Html::javaScriptFile($url, $options)->render();
389 | }
390 |
391 | /**
392 | * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
393 | * variable names.
394 | *
395 | * @param string $name Name of the variable
396 | * @param mixed $value Value of the variable
397 | * @param int $position The position in a page at which the JavaScript variable should be inserted.
398 | *
399 | * The possible values are:
400 | *
401 | * - {@see WebView::POSITION_HEAD}: in the head section. This is the default value.
402 | * - {@see WebView::POSITION_BEGIN}: at the beginning of the body section.
403 | * - {@see WebView::POSITION_END}: at the end of the body section.
404 | * - {@see WebView::POSITION_LOAD}: enclosed within jQuery(window).load().
405 | * Note that by using this position, the method will automatically register the jQuery js file.
406 | * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
407 | * Note that by using this position, the method will automatically register the jQuery js file.
408 | */
409 | public function registerJsVar(string $name, mixed $value, int $position = WebView::POSITION_HEAD): void
410 | {
411 | $js = sprintf('var %s = %s;', $name, Json::htmlEncode($value));
412 | $this->registerJs($js, $position, $name);
413 | }
414 |
415 | /**
416 | * It processes the JS configuration generated by the asset manager and converts it into HTML code.
417 | */
418 | public function addJsFiles(array $jsFiles): void
419 | {
420 | /** @var mixed $value */
421 | foreach ($jsFiles as $key => $value) {
422 | $this->registerJsFileByConfig(
423 | is_string($key) ? $key : null,
424 | is_array($value) ? $value : [$value],
425 | );
426 | }
427 | }
428 |
429 | /**
430 | * It processes the JS strings generated by the asset manager.
431 | *
432 | * @throws InvalidArgumentException
433 | */
434 | public function addJsStrings(array $jsStrings): void
435 | {
436 | /** @var mixed $value */
437 | foreach ($jsStrings as $key => $value) {
438 | $this->registerJsStringByConfig(
439 | is_string($key) ? $key : null,
440 | is_array($value) ? $value : [$value, WebView::POSITION_END]
441 | );
442 | }
443 | }
444 |
445 | /**
446 | * It processes the JS variables generated by the asset manager and converts it into JS code.
447 | *
448 | * @throws InvalidArgumentException
449 | */
450 | public function addJsVars(array $jsVars): void
451 | {
452 | /** @var mixed $value */
453 | foreach ($jsVars as $key => $value) {
454 | if (is_string($key)) {
455 | $this->registerJsVar($key, $value, WebView::POSITION_HEAD);
456 | } else {
457 | $this->registerJsVarByConfig((array) $value);
458 | }
459 | }
460 | }
461 |
462 | /**
463 | * Clears the data for working with the event loop:
464 | * - the added parameters and blocks;
465 | * - the registered meta tags, link tags, css/js scripts, files and title.
466 | */
467 | public function clear(): void
468 | {
469 | $this->parameters = [];
470 | $this->blocks = [];
471 | $this->title = '';
472 | $this->metaTags = [];
473 | $this->linkTags = [];
474 | $this->css = [];
475 | $this->cssFiles = [];
476 | $this->js = [];
477 | $this->jsFiles = [];
478 | }
479 |
480 | /**
481 | * @throws InvalidArgumentException
482 | */
483 | private function registerCssFileByConfig(?string $key, array $config): void
484 | {
485 | if (!array_key_exists(0, $config)) {
486 | throw new InvalidArgumentException('Do not set CSS file.');
487 | }
488 | $file = $config[0];
489 |
490 | if (!is_string($file)) {
491 | throw new InvalidArgumentException(
492 | sprintf(
493 | 'CSS file should be string. Got %s.',
494 | get_debug_type($file),
495 | )
496 | );
497 | }
498 |
499 | $position = (int) ($config[1] ?? WebView::POSITION_HEAD);
500 |
501 | unset($config[0], $config[1]);
502 | $this->registerCssFile($file, $position, $config, $key);
503 | }
504 |
505 | /**
506 | * @throws InvalidArgumentException
507 | */
508 | private function registerCssStringByConfig(?string $key, array $config): void
509 | {
510 | if (!array_key_exists(0, $config)) {
511 | throw new InvalidArgumentException('Do not set CSS string.');
512 | }
513 | $css = $config[0];
514 |
515 | if (!is_string($css) && !($css instanceof Style)) {
516 | throw new InvalidArgumentException(
517 | sprintf(
518 | 'CSS string should be string or instance of \\' . Style::class . '. Got %s.',
519 | get_debug_type($css),
520 | )
521 | );
522 | }
523 |
524 | $position = $config[1] ?? WebView::POSITION_HEAD;
525 | if (!$this->isValidCssPosition($position)) {
526 | throw new InvalidArgumentException('Invalid position of CSS strings.');
527 | }
528 |
529 | unset($config[0], $config[1]);
530 | if ($config !== []) {
531 | $css = ($css instanceof Style ? $css : Html::style($css))->attributes($config);
532 | }
533 |
534 | is_string($css)
535 | ? $this->registerCss($css, $position, [], $key)
536 | : $this->registerStyleTag($css, $position, $key);
537 | }
538 |
539 | /**
540 | * @psalm-assert =int $position
541 | */
542 | private function isValidCssPosition(mixed $position): bool
543 | {
544 | return in_array(
545 | $position,
546 | [
547 | WebView::POSITION_HEAD,
548 | WebView::POSITION_BEGIN,
549 | WebView::POSITION_END,
550 | ],
551 | true,
552 | );
553 | }
554 |
555 | /**
556 | * @throws InvalidArgumentException
557 | */
558 | private function registerJsFileByConfig(?string $key, array $config): void
559 | {
560 | if (!array_key_exists(0, $config)) {
561 | throw new InvalidArgumentException('Do not set JS file.');
562 | }
563 | $file = $config[0];
564 |
565 | if (!is_string($file)) {
566 | throw new InvalidArgumentException(
567 | sprintf(
568 | 'JS file should be string. Got %s.',
569 | get_debug_type($file),
570 | )
571 | );
572 | }
573 |
574 | $position = (int) ($config[1] ?? WebView::POSITION_END);
575 |
576 | unset($config[0], $config[1]);
577 | $this->registerJsFile($file, $position, $config, $key);
578 | }
579 |
580 | /**
581 | * @throws InvalidArgumentException
582 | */
583 | private function registerJsStringByConfig(?string $key, array $config): void
584 | {
585 | if (!array_key_exists(0, $config)) {
586 | throw new InvalidArgumentException('Do not set JS string.');
587 | }
588 | $js = $config[0];
589 |
590 | if (!is_string($js) && !($js instanceof Script)) {
591 | throw new InvalidArgumentException(
592 | sprintf(
593 | 'JS string should be string or instance of \\' . Script::class . '. Got %s.',
594 | get_debug_type($js),
595 | )
596 | );
597 | }
598 |
599 | $position = $config[1] ?? WebView::POSITION_END;
600 | if (!$this->isValidJsPosition($position)) {
601 | throw new InvalidArgumentException('Invalid position of JS strings.');
602 | }
603 |
604 | unset($config[0], $config[1]);
605 | if ($config !== []) {
606 | $js = ($js instanceof Script ? $js : Html::script($js))->attributes($config);
607 | }
608 |
609 | is_string($js)
610 | ? $this->registerJs($js, $position, $key)
611 | : $this->registerScriptTag($js, $position, $key);
612 | }
613 |
614 | /**
615 | * @throws InvalidArgumentException
616 | */
617 | private function registerJsVarByConfig(array $config): void
618 | {
619 | if (!array_key_exists(0, $config)) {
620 | throw new InvalidArgumentException('Do not set JS variable name.');
621 | }
622 | $key = $config[0];
623 |
624 | if (!is_string($key)) {
625 | throw new InvalidArgumentException(
626 | sprintf(
627 | 'JS variable name should be string. Got %s.',
628 | get_debug_type($key),
629 | )
630 | );
631 | }
632 |
633 | if (!array_key_exists(1, $config)) {
634 | throw new InvalidArgumentException('Do not set JS variable value.');
635 | }
636 | /** @var mixed */
637 | $value = $config[1];
638 |
639 | $position = $config[2] ?? WebView::POSITION_HEAD;
640 | if (!$this->isValidJsPosition($position)) {
641 | throw new InvalidArgumentException('Invalid position of JS variable.');
642 | }
643 |
644 | $this->registerJsVar($key, $value, $position);
645 | }
646 |
647 | /**
648 | * @psalm-assert =int $position
649 | */
650 | private function isValidJsPosition(mixed $position): bool
651 | {
652 | return in_array(
653 | $position,
654 | [
655 | WebView::POSITION_HEAD,
656 | WebView::POSITION_BEGIN,
657 | WebView::POSITION_END,
658 | WebView::POSITION_READY,
659 | WebView::POSITION_LOAD,
660 | ],
661 | true,
662 | );
663 | }
664 | }
665 |
--------------------------------------------------------------------------------
/src/TemplateRendererInterface.php:
--------------------------------------------------------------------------------
1 | '/app/themes/basic']`, then the themed version for
35 | * a view file `/app/views/site/index.php` will be `/app/themes/basic/site/index.php`.
36 | *
37 | * It is possible to map a single path to multiple paths. For example:
38 | *
39 | * ```php
40 | * 'yiisoft/view' => [
41 | * 'theme' => [
42 | * 'pathMap' => [
43 | * '/app/views' => [
44 | * '/app/themes/christmas',
45 | * '/app/themes/basic',
46 | * ],
47 | * ],
48 | * 'basePath' => '',
49 | * 'baseUrl' => '',
50 | * ],
51 | * ],
52 | * ```
53 | *
54 | * In this case, the themed version could be either `/app/themes/christmas/site/index.php` or
55 | * `/app/themes/basic/site/index.php`. The former has precedence over the latter if both files exist.
56 | *
57 | * To use the theme directly without configurations, you should set it using the {@see View::setTheme()} as follows:
58 | *
59 | * ```php
60 | * $pathMap = [...];
61 | * $basePath = '/path/to/private/themes/basic';
62 | * $baseUrl = '/path/to/public/themes/basic';
63 | *
64 | * $view->setTheme(new Theme([...], $basePath, $baseUrl));
65 | * ```
66 | */
67 | final class Theme
68 | {
69 | /**
70 | * @var array
71 | */
72 | private readonly array $pathMap;
73 | private string $basePath = '';
74 | private string $baseUrl = '';
75 |
76 | /**
77 | * @param array $pathMap The mapping between view directories and their corresponding
78 | * themed versions. The path map is used by {@see applyTo()} when a view is trying to apply the theme.
79 | * @param string $basePath The base path to the theme directory.
80 | * @param string $baseUrl The base URL for this theme.
81 | *
82 | * @psalm-param array $pathMap
83 | */
84 | public function __construct(array $pathMap = [], string $basePath = '', string $baseUrl = '')
85 | {
86 | $this->validatePathMap($pathMap);
87 | $this->pathMap = $pathMap;
88 |
89 | if ($basePath !== '') {
90 | $this->basePath = rtrim($basePath, '/');
91 | }
92 |
93 | if ($baseUrl !== '') {
94 | $this->baseUrl = rtrim($baseUrl, '/');
95 | }
96 | }
97 |
98 | /**
99 | * Returns the URL path for this theme.
100 | *
101 | * @return string The base URL (without ending slash) for this theme. All resources of this theme are considered
102 | * to be under this base URL.
103 | */
104 | public function getBaseUrl(): string
105 | {
106 | return $this->baseUrl;
107 | }
108 |
109 | /**
110 | * Returns the base path to the theme directory.
111 | *
112 | * @return string The root path of this theme. All resources of this theme are located under this directory.
113 | *
114 | * @see pathMap
115 | */
116 | public function getBasePath(): string
117 | {
118 | return $this->basePath;
119 | }
120 |
121 | /**
122 | * Converts a file to a themed file if possible.
123 | *
124 | * If there is no corresponding themed file, the original file will be returned.
125 | *
126 | * @param string $path The file to be themed
127 | *
128 | * @return string The themed file, or the original file if the themed version is not available.
129 | */
130 | public function applyTo(string $path): string
131 | {
132 | if ($this->pathMap === []) {
133 | return $path;
134 | }
135 |
136 | $path = FileHelper::normalizePath($path);
137 |
138 | foreach ($this->pathMap as $from => $tos) {
139 | $from = FileHelper::normalizePath($from) . '/';
140 |
141 | if (str_starts_with($path, $from)) {
142 | $n = strlen($from);
143 |
144 | foreach ((array) $tos as $to) {
145 | $to = FileHelper::normalizePath($to) . '/';
146 | $file = $to . substr($path, $n);
147 |
148 | if (is_file($file)) {
149 | return $file;
150 | }
151 | }
152 | }
153 | }
154 |
155 | return $path;
156 | }
157 |
158 | /**
159 | * Converts and returns a relative URL into an absolute URL using {@see getbaseUrl()}.
160 | *
161 | * @param string $url The relative URL to be converted.
162 | *
163 | * @return string The absolute URL
164 | */
165 | public function getUrl(string $url): string
166 | {
167 | if (($baseUrl = $this->getBaseUrl()) !== '') {
168 | return $baseUrl . '/' . ltrim($url, '/');
169 | }
170 |
171 | return $url;
172 | }
173 |
174 | /**
175 | * Converts and returns a relative file path into an absolute one using {@see getBasePath()}.
176 | *
177 | * @param string $path The relative file path to be converted.
178 | *
179 | * @return string The absolute file path.
180 | */
181 | public function getPath(string $path): string
182 | {
183 | if (($basePath = $this->getBasePath()) !== '') {
184 | return $basePath . '/' . ltrim($path, '/\\');
185 | }
186 |
187 | return $path;
188 | }
189 |
190 | /**
191 | * Validates the path map.
192 | *
193 | * @param array $pathMap The path map for validation.
194 | */
195 | private function validatePathMap(array $pathMap): void
196 | {
197 | /** @var mixed $destinations */
198 | foreach ($pathMap as $source => $destinations) {
199 | if (!is_string($source)) {
200 | $this->throwInvalidPathMapException();
201 | }
202 |
203 | /** @var mixed $destination */
204 | foreach ((array)$destinations as $destination) {
205 | if (!is_string($destination)) {
206 | $this->throwInvalidPathMapException();
207 | }
208 | }
209 | }
210 | }
211 |
212 | private function throwInvalidPathMapException(): never
213 | {
214 | throw new InvalidArgumentException(
215 | 'The path map should contain the mapping between view directories and corresponding theme directories.'
216 | );
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/View.php:
--------------------------------------------------------------------------------
1 | basePath = $basePath;
42 | $this->state = new ViewState();
43 | $this->localeState = new LocaleState();
44 | $this->themeState = new ThemeState();
45 | $this->eventDispatcher = $eventDispatcher;
46 | $this->setPlaceholderSalt(__DIR__);
47 | }
48 |
49 | /**
50 | * Returns a new instance with cleared state (blocks, parameters, etc.)
51 | */
52 | public function withClearedState(): static
53 | {
54 | $new = clone $this;
55 | $new->state = new ViewState();
56 | $new->localeState = new LocaleState();
57 | $new->themeState = new ThemeState();
58 | return $new;
59 | }
60 |
61 | /**
62 | * Returns a new instance with deep clone of the object, including state cloning.
63 | */
64 | public function deepClone(): static
65 | {
66 | $new = clone $this;
67 | $new->state = clone $this->state;
68 | $new->localeState = clone $this->localeState;
69 | $new->themeState = clone $this->themeState;
70 | return $new;
71 | }
72 |
73 | /**
74 | * Marks the beginning of a view.
75 | */
76 | public function beginPage(): void
77 | {
78 | ob_start();
79 | ob_implicit_flush(false);
80 | $this->eventDispatcher?->dispatch(new PageBegin($this));
81 | }
82 |
83 | /**
84 | * Marks the ending of a view.
85 | */
86 | public function endPage(): void
87 | {
88 | $this->eventDispatcher?->dispatch(new PageEnd($this));
89 |
90 | ob_end_flush();
91 | }
92 |
93 | protected function createBeforeRenderEvent(string $viewFile, array $parameters): StoppableEventInterface
94 | {
95 | return new BeforeRender($this, $viewFile, $parameters);
96 | }
97 |
98 | protected function createAfterRenderEvent(
99 | string $viewFile,
100 | array $parameters,
101 | string $result
102 | ): AfterRenderEventInterface {
103 | return new AfterRender($this, $viewFile, $parameters, $result);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/ViewContext.php:
--------------------------------------------------------------------------------
1 | viewPath;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/ViewContextInterface.php:
--------------------------------------------------------------------------------
1 | withRenderers(['twig' => new \Yiisoft\View\Twig\ViewRenderer($environment)]);
37 | * ```
38 | *
39 | * If no renderer is available for the given view file, the view file will be treated as a normal PHP
40 | * and rendered via {@see PhpTemplateRenderer}.
41 | *
42 | * @psalm-param array $renderers
43 | */
44 | public function withRenderers(array $renderers): static;
45 |
46 | /**
47 | * Returns a new instance with the specified source locale.
48 | *
49 | * @param string $locale The source locale.
50 | */
51 | public function withSourceLocale(string $locale): static;
52 |
53 | /**
54 | * Returns a new instance with the specified view context instance.
55 | *
56 | * @param ViewContextInterface|null $context The context under which the {@see render()} method is being invoked.
57 | */
58 | public function withContext(ViewContextInterface|null $context): static;
59 |
60 | /**
61 | * Returns a new instance with the specified view context path.
62 | *
63 | * @param string $path The context path under which the {@see render()} method is being invoked.
64 | */
65 | public function withContextPath(string $path): static;
66 |
67 | /**
68 | * Returns a new instance with specified salt for the placeholder signature {@see getPlaceholderSignature()}.
69 | *
70 | * @param string $salt The placeholder salt.
71 | */
72 | public function withPlaceholderSalt(string $salt): static;
73 |
74 | /**
75 | * Returns a new instance with cleared state (blocks, parameters, etc.)
76 | */
77 | public function withClearedState(): static;
78 |
79 | /**
80 | * Returns a new instance with deep clone of the object, including state cloning.
81 | */
82 | public function deepClone(): static;
83 |
84 | /**
85 | * Set the specified locale code.
86 | *
87 | * @param string $locale The locale code.
88 | */
89 | public function setLocale(string $locale): static;
90 |
91 | /**
92 | * Set the specified locale code.
93 | *
94 | * @param string $locale The locale code.
95 | */
96 | public function withLocale(string $locale): static;
97 |
98 | /**
99 | * Get the specified locale code.
100 | *
101 | * @return string The locale code.
102 | */
103 | public function getLocale(): string;
104 |
105 | /**
106 | * Gets the base path to the view directory.
107 | *
108 | * @return string The base view path.
109 | */
110 | public function getBasePath(): string;
111 |
112 | /**
113 | * Gets the theme instance, or `null` if no theme has been set.
114 | *
115 | * @return Theme|null The theme instance, or `null` if no theme has been set.
116 | */
117 | public function getTheme(): ?Theme;
118 |
119 | /**
120 | * Set the specified theme instance.
121 | *
122 | * @param Theme|null $theme The theme instance or `null` for reset theme.
123 | */
124 | public function setTheme(?Theme $theme): static;
125 |
126 | /**
127 | * Set the specified theme instance immutable.
128 | *
129 | * @param Theme|null $theme The theme instance or `null` for reset theme.
130 | */
131 | public function withTheme(?Theme $theme): static;
132 |
133 | /**
134 | * Sets a common parameters that is accessible in all view templates.
135 | *
136 | * @param array $parameters Parameters that are common for all view templates.
137 | *
138 | * @psalm-param array $parameters
139 | *
140 | * @see setParameter()
141 | */
142 | public function setParameters(array $parameters): static;
143 |
144 | /**
145 | * Sets a common parameter that is accessible in all view templates.
146 | *
147 | * @param string $id The unique identifier of the parameter.
148 | * @param mixed $value The value of the parameter.
149 | */
150 | public function setParameter(string $id, mixed $value): static;
151 |
152 | /**
153 | * Add values to end of common array parameter. If specified parameter does not exist or him is not array,
154 | * then parameter will be added as empty array.
155 | *
156 | * @param string $id The unique identifier of the parameter.
157 | * @param mixed ...$value Value(s) for add to end of array parameter.
158 | *
159 | * @throws InvalidArgumentException When specified parameter already exists and is not an array.
160 | */
161 | public function addToParameter(string $id, mixed ...$value): static;
162 |
163 | /**
164 | * Removes a common parameter.
165 | *
166 | * @param string $id The unique identifier of the parameter.
167 | */
168 | public function removeParameter(string $id): static;
169 |
170 | /**
171 | * Gets a common parameter value by ID.
172 | *
173 | * @param string $id The unique identifier of the parameter.
174 | * @param mixed $default The default value to be returned if the specified parameter does not exist.
175 | *
176 | * @throws InvalidArgumentException If specified parameter does not exist and not passed default value.
177 | *
178 | * @return mixed The value of the parameter.
179 | */
180 | public function getParameter(string $id, mixed ...$default): mixed;
181 |
182 | /**
183 | * Checks the existence of a common parameter by ID.
184 | *
185 | * @param string $id The unique identifier of the parameter.
186 | *
187 | * @return bool Whether a custom parameter that is common for all view templates exists.
188 | */
189 | public function hasParameter(string $id): bool;
190 |
191 | /**
192 | * Sets a content block.
193 | *
194 | * @param string $id The unique identifier of the block.
195 | * @param string $content The content of the block.
196 | */
197 | public function setBlock(string $id, string $content): static;
198 |
199 | /**
200 | * Removes a content block.
201 | *
202 | * @param string $id The unique identifier of the block.
203 | */
204 | public function removeBlock(string $id): static;
205 |
206 | /**
207 | * Gets content of the block by ID.
208 | *
209 | * @param string $id The unique identifier of the block.
210 | *
211 | * @return string The content of the block.
212 | */
213 | public function getBlock(string $id): string;
214 |
215 | /**
216 | * Checks the existence of a content block by ID.
217 | *
218 | * @param string $id The unique identifier of the block.
219 | *
220 | * @return bool Whether a content block exists.
221 | */
222 | public function hasBlock(string $id): bool;
223 |
224 | /**
225 | * Gets the view file currently being rendered.
226 | *
227 | * @return string|null The view file currently being rendered. `null` if no view file is being rendered.
228 | */
229 | public function getViewFile(): ?string;
230 |
231 | /**
232 | * Gets the placeholder signature.
233 | *
234 | * @return string The placeholder signature.
235 | */
236 | public function getPlaceholderSignature(): string;
237 |
238 | /**
239 | * Renders a view.
240 | *
241 | * The view to be rendered can be specified in one of the following formats:
242 | *
243 | * - the absolute path to the view file, e.g. "/path/to/view.php";
244 | * - the name of the view starting with `//` to join the base path {@see getBasePath()}, e.g. "//site/index";
245 | * - the name of the view starting with `./` to join the directory containing the view currently being rendered
246 | * (i.e., this happens when rendering a view within another view), e.g. "./widget";
247 | * - the name of the view without the starting `//` or `./` (e.g. "site/index"). The corresponding view file will be
248 | * looked for under the {@see ViewContextInterface::getViewPath()} of the context set via {@see withContext()}.
249 | * If the context instance was not set {@see withContext()}, it will be looked for under the base path.
250 | *
251 | * @param string $view The view name.
252 | * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
253 | * file.
254 | *
255 | * @throws LogicException If the view cannot be resolved.
256 | * @throws ViewNotFoundException If the view file does not exist.
257 | * @throws Throwable
258 | *
259 | * @return string The rendering result.
260 | */
261 | public function render(string $view, array $parameters = []): string;
262 |
263 | /**
264 | * Returns the localized version of a specified file.
265 | *
266 | * The searching is based on the specified locale code. In particular, a file with the same name will be looked
267 | * for under the subdirectory whose name is the same as the locale code. For example, given the file
268 | * "path/to/view.php" and locale code "zh-CN", the localized file will be looked for as "path/to/zh-CN/view.php".
269 | * If the file is not found, it will try a fallback with just a locale code that is "zh"
270 | * i.e. "path/to/zh/view.php".
271 | * If it is not found as well the original file will be returned.
272 | *
273 | * If the target and the source locale codes are the same, the original file will be returned.
274 | *
275 | * @param string $file The original file
276 | * @param string|null $locale The target locale that the file should be localized to.
277 | * @param string|null $sourceLocale The locale that the original file is in.
278 | *
279 | * @return string The matching localized file, or the original file if the localized version is not found.
280 | * If the target and the source locale codes are the same, the original file will be returned.
281 | */
282 | public function localize(string $file, ?string $locale = null, ?string $sourceLocale = null): string;
283 |
284 | /**
285 | * Clears the data for working with the event loop.
286 | */
287 | public function clear(): void;
288 | }
289 |
--------------------------------------------------------------------------------
/src/ViewTrait.php:
--------------------------------------------------------------------------------
1 |
51 | */
52 | private array $renderers = [];
53 |
54 | /**
55 | * @var array The view files currently being rendered. There may be multiple view files being
56 | * rendered at a moment because one view may be rendered within another.
57 | *
58 | * @psalm-var array>
59 | */
60 | private array $viewFiles = [];
61 |
62 | /**
63 | * Returns a new instance with specified base path to the view directory.
64 | *
65 | * @param string|null $basePath The base path to the view directory.
66 | */
67 | public function withBasePath(string|null $basePath): static
68 | {
69 | $new = clone $this;
70 | $new->basePath = $basePath;
71 | return $new;
72 | }
73 |
74 | /**
75 | * Returns a new instance with the specified renderers.
76 | *
77 | * @param array $renderers A list of available renderers indexed by their
78 | * corresponding supported file extensions.
79 | *
80 | * ```php
81 | * $view = $view->withRenderers(['twig' => new \Yiisoft\View\Twig\ViewRenderer($environment)]);
82 | * ```
83 | *
84 | * If no renderer is available for the given view file, the view file will be treated as a normal PHP
85 | * and rendered via {@see PhpTemplateRenderer}.
86 | *
87 | * @psalm-param array $renderers
88 | */
89 | public function withRenderers(array $renderers): static
90 | {
91 | $new = clone $this;
92 | $new->renderers = $renderers;
93 | return $new;
94 | }
95 |
96 | /**
97 | * Returns a new instance with the specified source locale.
98 | *
99 | * @param string $locale The source locale.
100 | */
101 | public function withSourceLocale(string $locale): static
102 | {
103 | $new = clone $this;
104 | $new->sourceLocale = $locale;
105 | return $new;
106 | }
107 |
108 | /**
109 | * Returns a new instance with the specified fallback view file extension.
110 | *
111 | * @param string $fallbackExtension The fallback view file extension. Default is {@see ViewInterface::PHP_EXTENSION}.
112 | * This will be appended to view file names if they don't exist.
113 | */
114 | public function withFallbackExtension(string $fallbackExtension, string ...$otherFallbacks): static
115 | {
116 | $new = clone $this;
117 | $new->fallbackExtensions = [$fallbackExtension, ...array_values($otherFallbacks)];
118 | return $new;
119 | }
120 |
121 | /**
122 | * Returns a new instance with the specified view context instance.
123 | *
124 | * @param ViewContextInterface|null $context The context under which the {@see render()} method is being invoked.
125 | */
126 | public function withContext(ViewContextInterface|null $context): static
127 | {
128 | $new = clone $this;
129 | $new->context = $context;
130 | $new->viewFiles = [];
131 | return $new;
132 | }
133 |
134 | /**
135 | * Returns a new instance with the specified view context path.
136 | *
137 | * @param string $path The context path under which the {@see render()} method is being invoked.
138 | */
139 | public function withContextPath(string $path): static
140 | {
141 | return $this->withContext(new ViewContext($path));
142 | }
143 |
144 | /**
145 | * Returns a new instance with specified salt for the placeholder signature {@see getPlaceholderSignature()}.
146 | *
147 | * @param string $salt The placeholder salt.
148 | */
149 | public function withPlaceholderSalt(string $salt): static
150 | {
151 | $new = clone $this;
152 | $new->setPlaceholderSalt($salt);
153 | return $new;
154 | }
155 |
156 | /**
157 | * Set the specified locale code.
158 | *
159 | * @param string $locale The locale code.
160 | */
161 | public function setLocale(string $locale): static
162 | {
163 | $this->localeState->setLocale($locale);
164 | return $this;
165 | }
166 |
167 | /**
168 | * Set the specified locale code.
169 | *
170 | * @param string $locale The locale code.
171 | */
172 | public function withLocale(string $locale): static
173 | {
174 | $new = clone $this;
175 | $new->localeState = new LocaleState($locale);
176 |
177 | return $new;
178 | }
179 |
180 | /**
181 | * Get the specified locale code.
182 | *
183 | * @return string The locale code.
184 | */
185 | public function getLocale(): string
186 | {
187 | return $this->localeState->getLocale();
188 | }
189 |
190 | /**
191 | * Gets the base path to the view directory.
192 | *
193 | * @return string The base view path.
194 | */
195 | public function getBasePath(): string
196 | {
197 | if ($this->basePath === null) {
198 | throw new LogicException('The base path is not set.');
199 | }
200 |
201 | return $this->basePath;
202 | }
203 |
204 | /**
205 | * Gets the fallback view file extension.
206 | *
207 | * @return string[] The fallback view file extension.
208 | */
209 | public function getFallbackExtensions(): array
210 | {
211 | return $this->fallbackExtensions;
212 | }
213 |
214 | /**
215 | * Gets the theme instance, or `null` if no theme has been set.
216 | *
217 | * @return Theme|null The theme instance, or `null` if no theme has been set.
218 | */
219 | public function getTheme(): ?Theme
220 | {
221 | return $this->themeState->getTheme();
222 | }
223 |
224 | /**
225 | * Set the specified theme instance.
226 | *
227 | * @param Theme|null $theme The theme instance or `null` for reset theme.
228 | */
229 | public function setTheme(?Theme $theme): static
230 | {
231 | $this->themeState->setTheme($theme);
232 | return $this;
233 | }
234 |
235 | public function withTheme(?Theme $theme): static
236 | {
237 | $new = clone $this;
238 | $new->themeState = new ThemeState($theme);
239 |
240 | return $new;
241 | }
242 |
243 | /**
244 | * Sets a common parameters that is accessible in all view templates.
245 | *
246 | * @param array $parameters Parameters that are common for all view templates.
247 | *
248 | * @psalm-param array $parameters
249 | *
250 | * @see setParameter()
251 | */
252 | public function setParameters(array $parameters): static
253 | {
254 | $this->state->setParameters($parameters);
255 | return $this;
256 | }
257 |
258 | /**
259 | * Sets a common parameter that is accessible in all view templates.
260 | *
261 | * @param string $id The unique identifier of the parameter.
262 | * @param mixed $value The value of the parameter.
263 | */
264 | public function setParameter(string $id, mixed $value): static
265 | {
266 | $this->state->setParameter($id, $value);
267 | return $this;
268 | }
269 |
270 | /**
271 | * Add values to end of common array parameter. If specified parameter does not exist or him is not array,
272 | * then parameter will be added as empty array.
273 | *
274 | * @param string $id The unique identifier of the parameter.
275 | * @param mixed ...$value Value(s) for add to end of array parameter.
276 | *
277 | * @throws InvalidArgumentException When specified parameter already exists and is not an array.
278 | */
279 | public function addToParameter(string $id, mixed ...$value): static
280 | {
281 | $this->state->addToParameter($id, ...$value);
282 | return $this;
283 | }
284 |
285 | /**
286 | * Removes a common parameter.
287 | *
288 | * @param string $id The unique identifier of the parameter.
289 | */
290 | public function removeParameter(string $id): static
291 | {
292 | $this->state->removeParameter($id);
293 | return $this;
294 | }
295 |
296 | /**
297 | * Gets a common parameter value by ID.
298 | *
299 | * @param string $id The unique identifier of the parameter.
300 | * @param mixed $default The default value to be returned if the specified parameter does not exist.
301 | *
302 | * @throws InvalidArgumentException If specified parameter does not exist and not passed default value.
303 | *
304 | * @return mixed The value of the parameter.
305 | */
306 | public function getParameter(string $id, mixed ...$default): mixed
307 | {
308 | return call_user_func([$this->state, 'getParameter'], $id, ...$default);
309 | }
310 |
311 | /**
312 | * Checks the existence of a common parameter by ID.
313 | *
314 | * @param string $id The unique identifier of the parameter.
315 | *
316 | * @return bool Whether a custom parameter that is common for all view templates exists.
317 | */
318 | public function hasParameter(string $id): bool
319 | {
320 | return $this->state->hasParameter($id);
321 | }
322 |
323 | /**
324 | * Sets a content block.
325 | *
326 | * @param string $id The unique identifier of the block.
327 | * @param string $content The content of the block.
328 | */
329 | public function setBlock(string $id, string $content): static
330 | {
331 | $this->state->setBlock($id, $content);
332 | return $this;
333 | }
334 |
335 | /**
336 | * Removes a content block.
337 | *
338 | * @param string $id The unique identifier of the block.
339 | */
340 | public function removeBlock(string $id): static
341 | {
342 | $this->state->removeBlock($id);
343 | return $this;
344 | }
345 |
346 | /**
347 | * Gets content of the block by ID.
348 | *
349 | * @param string $id The unique identifier of the block.
350 | *
351 | * @return string The content of the block.
352 | */
353 | public function getBlock(string $id): string
354 | {
355 | return $this->state->getBlock($id);
356 | }
357 |
358 | /**
359 | * Checks the existence of a content block by ID.
360 | *
361 | * @param string $id The unique identifier of the block.
362 | *
363 | * @return bool Whether a content block exists.
364 | */
365 | public function hasBlock(string $id): bool
366 | {
367 | return $this->state->hasBlock($id);
368 | }
369 |
370 | /**
371 | * Gets the view file currently being rendered.
372 | *
373 | * @return string|null The view file currently being rendered. `null` if no view file is being rendered.
374 | */
375 | public function getViewFile(): ?string
376 | {
377 | /** @psalm-suppress InvalidArrayOffset */
378 | return empty($this->viewFiles) ? null : end($this->viewFiles)['resolved'];
379 | }
380 |
381 | /**
382 | * Gets the placeholder signature.
383 | *
384 | * @return string The placeholder signature.
385 | */
386 | public function getPlaceholderSignature(): string
387 | {
388 | return $this->placeholderSignature;
389 | }
390 |
391 | /**
392 | * Renders a view.
393 | *
394 | * The view to be rendered can be specified in one of the following formats:
395 | *
396 | * - the absolute path to the view file, e.g. "/path/to/view.php";
397 | * - the name of the view starting with `//` to join the base path {@see getBasePath()}, e.g. "//site/index";
398 | * - the name of the view starting with `./` to join the directory containing the view currently being rendered
399 | * (i.e., this happens when rendering a view within another view), e.g. "./widget";
400 | * - the name of the view starting with `../` to join the parent directory containing the view currently being
401 | * rendered, e.g. "../_header";
402 | * - the name of the view without the starting `//`, `./` or `../` (e.g. "site/index"). The corresponding view file
403 | * will be looked for under the {@see ViewContextInterface::getViewPath()} of the context set via {@see withContext()}.
404 | * If the context instance was not set {@see withContext()}, it will be looked for under the base path.
405 | *
406 | * Warning: Using `..` in view path can lead to accessing unsafe data, e.g., `//../../etc/passwd`. Ensure that such
407 | * cases are handled properly.
408 | *
409 | * @param string $view The view name.
410 | * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
411 | * file.
412 | *
413 | * @throws LogicException If the view cannot be resolved.
414 | * @throws ViewNotFoundException If the view file does not exist.
415 | * @throws Throwable
416 | *
417 | * @return string The rendering result.
418 | */
419 | public function render(string $view, array $parameters = []): string
420 | {
421 | $viewFile = $this->findTemplateFile($view);
422 |
423 | $parameters = array_merge($this->state->getParameters(), $parameters);
424 |
425 | // TODO: these two match now
426 | $requestedFile = $viewFile;
427 |
428 | $theme = $this->getTheme();
429 | if ($theme !== null) {
430 | $viewFile = $theme->applyTo($viewFile);
431 | }
432 |
433 | if (is_file($viewFile)) {
434 | $viewFile = $this->localize($viewFile);
435 | } else {
436 | throw new ViewNotFoundException("The view file \"$viewFile\" does not exist.");
437 | }
438 |
439 | $output = '';
440 | $this->viewFiles[] = [
441 | 'resolved' => $viewFile,
442 | 'requested' => $requestedFile,
443 | ];
444 |
445 | if ($this->beforeRender($viewFile, $parameters)) {
446 | $ext = pathinfo($viewFile, PATHINFO_EXTENSION);
447 | $renderer = $this->renderers[$ext] ?? new PhpTemplateRenderer();
448 | $output = $renderer->render($this, $viewFile, $parameters);
449 | $output = $this->afterRender($viewFile, $parameters, $output);
450 | }
451 |
452 | array_pop($this->viewFiles);
453 |
454 | return $output;
455 | }
456 |
457 | /**
458 | * Returns the localized version of a specified file.
459 | *
460 | * The searching is based on the specified locale code. In particular, a file with the same name will be looked
461 | * for under the subdirectory whose name is the same as the locale code. For example, given the file
462 | * "path/to/view.php" and locale code "zh-CN", the localized file will be looked for as "path/to/zh-CN/view.php".
463 | * If the file is not found, it will try a fallback with just a locale code that is "zh"
464 | * i.e. "path/to/zh/view.php".
465 | * If it is not found as well the original file will be returned.
466 | *
467 | * If the target and the source locale codes are the same, the original file will be returned.
468 | *
469 | * @param string $file The original file
470 | * @param string|null $locale The target locale that the file should be localized to.
471 | * @param string|null $sourceLocale The locale that the original file is in.
472 | *
473 | * @return string The matching localized file, or the original file if the localized version is not found.
474 | * If the target and the source locale codes are the same, the original file will be returned.
475 | */
476 | public function localize(string $file, ?string $locale = null, ?string $sourceLocale = null): string
477 | {
478 | $locale ??= $this->localeState->getLocale();
479 | $sourceLocale ??= $this->sourceLocale;
480 |
481 | if ($locale === $sourceLocale) {
482 | return $file;
483 | }
484 |
485 | $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $locale . DIRECTORY_SEPARATOR . basename($file);
486 |
487 | if (is_file($desiredFile)) {
488 | return $desiredFile;
489 | }
490 |
491 | $locale = substr($locale, 0, 2);
492 |
493 | if ($locale === $sourceLocale) {
494 | return $file;
495 | }
496 |
497 | $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $locale . DIRECTORY_SEPARATOR . basename($file);
498 | return is_file($desiredFile) ? $desiredFile : $file;
499 | }
500 |
501 | /**
502 | * Clears the data for working with the event loop.
503 | */
504 | public function clear(): void
505 | {
506 | $this->viewFiles = [];
507 | $this->state->clear();
508 | $this->localeState = new LocaleState();
509 | $this->themeState = new ThemeState();
510 | }
511 |
512 | /**
513 | * Creates an event that occurs before rendering.
514 | *
515 | * @param string $viewFile The view file to be rendered.
516 | * @param array $parameters The parameter array passed to the {@see render()} method.
517 | *
518 | * @return StoppableEventInterface The stoppable event instance.
519 | */
520 | abstract protected function createBeforeRenderEvent(string $viewFile, array $parameters): StoppableEventInterface;
521 |
522 | /**
523 | * Creates an event that occurs after rendering.
524 | *
525 | * @param string $viewFile The view file being rendered.
526 | * @param array $parameters The parameter array passed to the {@see render()} method.
527 | * @param string $result The rendering result of the view file.
528 | *
529 | * @return AfterRenderEventInterface The event instance.
530 | */
531 | abstract protected function createAfterRenderEvent(
532 | string $viewFile,
533 | array $parameters,
534 | string $result
535 | ): AfterRenderEventInterface;
536 |
537 | /**
538 | * This method is invoked right before {@see render()} renders a view file.
539 | *
540 | * The default implementations will trigger the {@see \Yiisoft\View\Event\View\BeforeRender}
541 | * or {@see \Yiisoft\View\Event\WebView\BeforeRender} event. If you override this method,
542 | * make sure you call the parent implementation first.
543 | *
544 | * @param string $viewFile The view file to be rendered.
545 | * @param array $parameters The parameter array passed to the {@see render()} method.
546 | *
547 | * @return bool Whether to continue rendering the view file.
548 | */
549 | private function beforeRender(string $viewFile, array $parameters): bool
550 | {
551 | if ($this->eventDispatcher === null) {
552 | return true;
553 | }
554 |
555 | $event = $this->createBeforeRenderEvent($viewFile, $parameters);
556 | $event = $this->eventDispatcher->dispatch($event);
557 | /** @var StoppableEventInterface $event */
558 | return !$event->isPropagationStopped();
559 | }
560 |
561 | /**
562 | * This method is invoked right after {@see render()} renders a view file.
563 | *
564 | * The default implementations will trigger the {@see \Yiisoft\View\Event\View\AfterRender}
565 | * or {@see \Yiisoft\View\Event\WebView\AfterRender} event. If you override this method,
566 | * make sure you call the parent implementation first.
567 | *
568 | * @param string $viewFile The view file being rendered.
569 | * @param array $parameters The parameter array passed to the {@see render()} method.
570 | * @param string $result The rendering result of the view file.
571 | *
572 | * @return string Updated output. It will be passed to {@see render()} and returned.
573 | */
574 | private function afterRender(string $viewFile, array $parameters, string $result): string
575 | {
576 | if ($this->eventDispatcher === null) {
577 | return $result;
578 | }
579 |
580 | $event = $this->createAfterRenderEvent($viewFile, $parameters, $result);
581 |
582 | /** @var AfterRenderEventInterface $event */
583 | $event = $this->eventDispatcher->dispatch($event);
584 |
585 | return $event->getResult();
586 | }
587 |
588 | private function setPlaceholderSalt(string $salt): void
589 | {
590 | $this->placeholderSignature = dechex(crc32($salt));
591 | }
592 |
593 | /**
594 | * Finds the view file based on the given view name.
595 | *
596 | * @param string $view The view name of the view file. Please refer to
597 | * {@see render()} on how to specify this parameter.
598 | *
599 | * @throws LogicException If a relative view name is given while there is no currently rendered view.
600 | *
601 | * @return string The view file path. Note that the file may not exist.
602 | */
603 | private function findTemplateFile(string $view): string
604 | {
605 | $file = $this->resolveViewFilePath($view);
606 | $hasExtension = pathinfo($file, PATHINFO_EXTENSION) !== '';
607 |
608 | if ($hasExtension && is_file($file)) {
609 | return $file;
610 | }
611 |
612 | foreach ($this->fallbackExtensions as $fallbackExtension) {
613 | $fileWithFallbackExtension = $file . '.' . $fallbackExtension;
614 | if (is_file($fileWithFallbackExtension)) {
615 | return $fileWithFallbackExtension;
616 | }
617 | }
618 |
619 | if ($hasExtension) {
620 | return $file;
621 | }
622 |
623 | return $file . '.' . $this->fallbackExtensions[0];
624 | }
625 |
626 | private function resolveViewFilePath(string $view): string
627 | {
628 | if (str_starts_with($view, './')) {
629 | return dirname($this->getRequestedViewFile($view)) . substr($view, 1);
630 | }
631 |
632 | if (str_starts_with($view, '../')) {
633 | return dirname($this->getRequestedViewFile($view), 2) . substr($view, 2);
634 | }
635 |
636 | if (str_starts_with($view, '//')) {
637 | return $this->getBasePath() . substr($view, 1);
638 | }
639 |
640 | if ($this->isWindows()) {
641 | if (str_contains($view, ':')) {
642 | return $view;
643 | }
644 | } else {
645 | if (str_starts_with($view, '/')) {
646 | return $view;
647 | }
648 | }
649 |
650 | return ($this->context?->getViewPath() ?? $this->getBasePath()) . '/' . $view;
651 | }
652 |
653 | /**
654 | * @return string The requested view currently being rendered.
655 | */
656 | private function getRequestedViewFile(string $view): string
657 | {
658 | /** @psalm-suppress InvalidArrayOffset */
659 | return empty($this->viewFiles)
660 | ? throw new LogicException("Unable to resolve file for view \"$view\": no currently rendered view.")
661 | : end($this->viewFiles)['requested'];
662 | }
663 |
664 | /**
665 | * Returns whether the current environment is Windows.
666 | */
667 | private function isWindows(): bool
668 | {
669 | return DIRECTORY_SEPARATOR === '\\';
670 | }
671 | }
672 |
--------------------------------------------------------------------------------
/src/WebView.php:
--------------------------------------------------------------------------------
1 | ';
78 |
79 | /**
80 | * This is internally used as the placeholder for receiving the content registered for the beginning of the body
81 | * section.
82 | */
83 | private const PLACEHOLDER_BODY_BEGIN = '';
84 |
85 | /**
86 | * This is internally used as the placeholder for receiving the content registered for the end of the body section.
87 | */
88 | private const PLACEHOLDER_BODY_END = '';
89 |
90 | /**
91 | * @param string|null $basePath The full path to the base directory of views.
92 | * @param EventDispatcherInterface|null $eventDispatcher The event dispatcher instance.
93 | */
94 | public function __construct(?string $basePath = null, ?EventDispatcherInterface $eventDispatcher = null)
95 | {
96 | $this->basePath = $basePath;
97 | $this->state = new WebViewState();
98 | $this->localeState = new LocaleState();
99 | $this->themeState = new ThemeState();
100 | $this->eventDispatcher = $eventDispatcher;
101 | $this->setPlaceholderSalt(__DIR__);
102 | }
103 |
104 | /**
105 | * Returns a new instance with cleared state (blocks, parameters, registered CSS/JS, etc.)
106 | */
107 | public function withClearedState(): static
108 | {
109 | $new = clone $this;
110 | $new->state = new WebViewState();
111 | $new->localeState = new LocaleState();
112 | $new->themeState = new ThemeState();
113 | return $new;
114 | }
115 |
116 | /**
117 | * Returns a new instance with deep clone of the object, including state cloning.
118 | */
119 | public function deepClone(): static
120 | {
121 | $new = clone $this;
122 | $new->state = clone $this->state;
123 | $new->localeState = clone $this->localeState;
124 | $new->themeState = clone $this->themeState;
125 | return $new;
126 | }
127 |
128 | /**
129 | * Marks the position of an HTML head section.
130 | */
131 | public function head(): void
132 | {
133 | echo sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature());
134 | $this->eventDispatcher?->dispatch(new Head($this));
135 | }
136 |
137 | /**
138 | * Marks the beginning of an HTML body section.
139 | */
140 | public function beginBody(): void
141 | {
142 | echo sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature());
143 | $this->eventDispatcher?->dispatch(new BodyBegin($this));
144 | }
145 |
146 | /**
147 | * Marks the ending of an HTML body section.
148 | */
149 | public function endBody(): void
150 | {
151 | $this->eventDispatcher?->dispatch(new BodyEnd($this));
152 | echo sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature());
153 | }
154 |
155 | /**
156 | * Marks the beginning of an HTML page.
157 | */
158 | public function beginPage(): void
159 | {
160 | ob_start();
161 | ob_implicit_flush(false);
162 | $this->eventDispatcher?->dispatch(new PageBegin($this));
163 | }
164 |
165 | /**
166 | * Marks the ending of an HTML page.
167 | *
168 | * @param bool $ajaxMode Whether the view is rendering in AJAX mode. If true, the JS scripts registered at
169 | * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like
170 | * normal scripts.
171 | */
172 | public function endPage(bool $ajaxMode = false): void
173 | {
174 | $this->eventDispatcher?->dispatch(new PageEnd($this));
175 |
176 | /**
177 | * @var string $content We assume that in this case active output buffer is always existed, so `ob_get_clean()`
178 | * returns a string.
179 | */
180 | $content = ob_get_clean();
181 |
182 | echo strtr($content, [
183 | sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature()) => $this->renderHeadHtml(),
184 | sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature()) => $this->renderBodyBeginHtml(),
185 | sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature()) => $this->renderBodyEndHtml($ajaxMode),
186 | ]);
187 |
188 | $this->state->clear();
189 | }
190 |
191 | /**
192 | * Renders a view in response to an AJAX request.
193 | *
194 | * This method is similar to {@see render()} except that it will surround the view being rendered with the calls of
195 | * {@see beginPage()}, {@see head()}, {@see beginBody()}, {@see endBody()} and {@see endPage()}. By doing so, the
196 | * method is able to inject into the rendering result with JS/CSS scripts and files that are registered with the
197 | * view.
198 | *
199 | * @param string $view The view name. Please refer to {@see render()} on how to specify this parameter.
200 | * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
201 | * file.
202 | *
203 | * @return string The rendering result
204 | *
205 | * {@see render()}
206 | */
207 | public function renderAjax(string $view, array $parameters = []): string
208 | {
209 | ob_start();
210 | ob_implicit_flush(false);
211 |
212 | $this->beginPage();
213 | $this->head();
214 | $this->beginBody();
215 | echo $this->render($view, $parameters);
216 | $this->endBody();
217 | $this->endPage(true);
218 |
219 | /**
220 | * @var string We assume that in this case active output buffer is always existed, so `ob_get_clean()` returns
221 | * a string.
222 | */
223 | return ob_get_clean();
224 | }
225 |
226 | /**
227 | * Renders a string in response to an AJAX request.
228 | *
229 | * @param string $string The string.
230 | *
231 | * @return string The rendering result.
232 | */
233 | public function renderAjaxString(string $string): string
234 | {
235 | ob_start();
236 | ob_implicit_flush(false);
237 |
238 | $this->beginPage();
239 | $this->head();
240 | $this->beginBody();
241 | echo $string;
242 | $this->endBody();
243 | $this->endPage(true);
244 |
245 | /**
246 | * @var string We assume that in this case active output buffer is always existed, so `ob_get_clean()` returns
247 | * a string.
248 | */
249 | return ob_get_clean();
250 | }
251 |
252 | /**
253 | * Get title in views.
254 | *
255 | * in Layout:
256 | *
257 | * ```php
258 | * = Html::encode($this->getTitle()) ?>
259 | * ```
260 | *
261 | * in Views:
262 | *
263 | * ```php
264 | * $this->setTitle('Web Application - Yii 3.0.');
265 | * ```
266 | */
267 | public function getTitle(): string
268 | {
269 | return $this->state->getTitle();
270 | }
271 |
272 | /**
273 | * Set title in views.
274 | *
275 | * {@see getTitle()}
276 | */
277 | public function setTitle(string|Stringable $value): self
278 | {
279 | $this->state->setTitle((string) $value);
280 | return $this;
281 | }
282 |
283 | /**
284 | * Registers a meta tag.
285 | *
286 | * For example, a description meta tag can be added like the following:
287 | *
288 | * ```php
289 | * $view->registerMeta([
290 | * 'name' => 'description',
291 | * 'content' => 'This website is about funny raccoons.'
292 | * ]);
293 | * ```
294 | *
295 | * will result in the meta tag ``.
296 | *
297 | * @param array $attributes The HTML attributes for the meta tag.
298 | * @param string|null $key The key that identifies the meta tag. If two meta tags are registered with the same key,
299 | * the latter will overwrite the former. If this is null, the new meta tag will be appended to the existing ones.
300 | */
301 | public function registerMeta(array $attributes, ?string $key = null): self
302 | {
303 | $this->state->registerMeta($attributes, $key);
304 | return $this;
305 | }
306 |
307 | /**
308 | * Registers a {@see Meta} tag.
309 | *
310 | * @see registerMeta()
311 | */
312 | public function registerMetaTag(Meta $meta, ?string $key = null): self
313 | {
314 | $this->state->registerMetaTag($meta, $key);
315 | return $this;
316 | }
317 |
318 | /**
319 | * Registers a link tag.
320 | *
321 | * For example, a link tag for a custom [favicon](https://www.w3.org/2005/10/howto-favicon) can be added like the
322 | * following:
323 | *
324 | * ```php
325 | * $view->registerLink(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
326 | * ```
327 | *
328 | * which will result in the following HTML: ``.
329 | *
330 | * **Note:** To register link tags for CSS stylesheets, use {@see registerCssFile()]} instead, which has more
331 | * options for this kind of link tag.
332 | *
333 | * @param array $attributes The HTML attributes for the link tag.
334 | * @param int $position The position at which the link tag should be inserted in a page.
335 | * @param string|null $key The key that identifies the link tag. If two link tags are registered with the same
336 | * key, the latter will overwrite the former. If this is null, the new link tag will be appended
337 | * to the existing ones.
338 | */
339 | public function registerLink(array $attributes, int $position = self::POSITION_HEAD, ?string $key = null): self
340 | {
341 | $this->state->registerLink($attributes, $position, $key);
342 | return $this;
343 | }
344 |
345 | /**
346 | * Registers a {@see Link} tag.
347 | *
348 | * @see registerLink()
349 | */
350 | public function registerLinkTag(Link $link, int $position = self::POSITION_HEAD, ?string $key = null): self
351 | {
352 | $this->state->registerLinkTag($link, $position, $key);
353 | return $this;
354 | }
355 |
356 | /**
357 | * Registers a CSS code block.
358 | *
359 | * @param string $css The content of the CSS code block to be registered.
360 | * @param string|null $key The key that identifies the CSS code block. If null, it will use $css as the key.
361 | * If two CSS code blocks are registered with the same key, the latter will overwrite the former.
362 | * @param array $attributes The HTML attributes for the {@see Style} tag.
363 | */
364 | public function registerCss(
365 | string $css,
366 | int $position = self::POSITION_HEAD,
367 | array $attributes = [],
368 | ?string $key = null
369 | ): self {
370 | $this->state->registerCss($css, $position, $attributes, $key);
371 | return $this;
372 | }
373 |
374 | /**
375 | * Registers a CSS code block from file.
376 | *
377 | * @param string $path The path or URL to CSS file.
378 | *
379 | * @see registerCss()
380 | */
381 | public function registerCssFromFile(
382 | string $path,
383 | int $position = self::POSITION_HEAD,
384 | array $attributes = [],
385 | ?string $key = null
386 | ): self {
387 | $this->state->registerCssFromFile($path, $position, $attributes, $key);
388 | return $this;
389 | }
390 |
391 | /**
392 | * Register a {@see Style} tag.
393 | *
394 | * @see registerJs()
395 | */
396 | public function registerStyleTag(Style $style, int $position = self::POSITION_HEAD, ?string $key = null): self
397 | {
398 | $this->state->registerStyleTag($style, $position, $key);
399 | return $this;
400 | }
401 |
402 | /**
403 | * Registers a CSS file.
404 | *
405 | * This method should be used for simple registration of CSS files. If you want to use features of
406 | * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
407 | * {@see \Yiisoft\Assets\AssetBundle}.
408 | *
409 | * @param string $url The CSS file to be registered.
410 | * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
411 | * for the supported options.
412 | * @param string|null $key The key that identifies the CSS script file. If null, it will use $url as the key.
413 | * If two CSS files are registered with the same key, the latter will overwrite the former.
414 | */
415 | public function registerCssFile(
416 | string $url,
417 | int $position = self::POSITION_HEAD,
418 | array $options = [],
419 | ?string $key = null
420 | ): self {
421 | $this->state->registerCssFile($url, $position, $options, $key);
422 | return $this;
423 | }
424 |
425 | /**
426 | * It processes the CSS configuration generated by the asset manager and converts it into HTML code.
427 | */
428 | public function addCssFiles(array $cssFiles): self
429 | {
430 | $this->state->addCssFiles($cssFiles);
431 | return $this;
432 | }
433 |
434 | /**
435 | * It processes the CSS strings generated by the asset manager.
436 | */
437 | public function addCssStrings(array $cssStrings): self
438 | {
439 | $this->state->addCssStrings($cssStrings);
440 | return $this;
441 | }
442 |
443 | /**
444 | * Registers a JS code block.
445 | *
446 | * @param string $js the JS code block to be registered
447 | * @param int $position the position at which the JS script tag should be inserted in a page.
448 | *
449 | * The possible values are:
450 | *
451 | * - {@see POSITION_HEAD}: in the head section
452 | * - {@see POSITION_BEGIN}: at the beginning of the body section
453 | * - {@see POSITION_END}: at the end of the body section. This is the default value.
454 | * - {@see POSITION_LOAD}: executed when HTML page is completely loaded.
455 | * - {@see POSITION_READY}: executed when HTML document composition is ready.
456 | * @param string|null $key The key that identifies the JS code block. If null, it will use $js as the key.
457 | * If two JS code blocks are registered with the same key, the latter will overwrite the former.
458 | */
459 | public function registerJs(string $js, int $position = self::POSITION_END, ?string $key = null): self
460 | {
461 | $this->state->registerJs($js, $position, $key);
462 | return $this;
463 | }
464 |
465 | /**
466 | * Register a `script` tag
467 | *
468 | * @see registerJs()
469 | */
470 | public function registerScriptTag(Script $script, int $position = self::POSITION_END, ?string $key = null): self
471 | {
472 | $this->state->registerScriptTag($script, $position, $key);
473 | return $this;
474 | }
475 |
476 | /**
477 | * Registers a JS file.
478 | *
479 | * This method should be used for simple registration of JS files. If you want to use features of
480 | * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
481 | * {@see \Yiisoft\Assets\AssetBundle}.
482 | *
483 | * @param string $url The JS file to be registered.
484 | * @param array $options The HTML attributes for the script tag. The following options are specially handled and
485 | * are not treated as HTML attributes:
486 | *
487 | * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
488 | * * {@see POSITION_HEAD}: in the head section
489 | * * {@see POSITION_BEGIN}: at the beginning of the body section
490 | * * {@see POSITION_END}: at the end of the body section. This is the default value.
491 | *
492 | * Please refer to {@see \Yiisoft\Html\Html::javaScriptFile()} for other supported options.
493 | * @param string|null $key The key that identifies the JS script file. If null, it will use $url as the key.
494 | * If two JS files are registered with the same key at the same position, the latter will overwrite the former.
495 | * Note that position option takes precedence, thus files registered with the same key, but different
496 | * position option will not override each other.
497 | */
498 | public function registerJsFile(
499 | string $url,
500 | int $position = self::POSITION_END,
501 | array $options = [],
502 | ?string $key = null
503 | ): self {
504 | $this->state->registerJsFile($url, $position, $options, $key);
505 | return $this;
506 | }
507 |
508 | /**
509 | * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
510 | * variable names.
511 | *
512 | * @param string $name Name of the variable
513 | * @param mixed $value Value of the variable
514 | * @param int $position The position in a page at which the JavaScript variable should be inserted.
515 | *
516 | * The possible values are:
517 | *
518 | * - {@see POSITION_HEAD}: in the head section. This is the default value.
519 | * - {@see POSITION_BEGIN}: at the beginning of the body section.
520 | * - {@see POSITION_END}: at the end of the body section.
521 | * - {@see POSITION_LOAD}: enclosed within jQuery(window).load().
522 | * Note that by using this position, the method will automatically register the jQuery js file.
523 | * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
524 | * Note that by using this position, the method will automatically register the jQuery js file.
525 | */
526 | public function registerJsVar(string $name, mixed $value, int $position = self::POSITION_HEAD): self
527 | {
528 | $this->state->registerJsVar($name, $value, $position);
529 | return $this;
530 | }
531 |
532 | /**
533 | * It processes the JS configuration generated by the asset manager and converts it into HTML code.
534 | */
535 | public function addJsFiles(array $jsFiles): self
536 | {
537 | $this->state->addJsFiles($jsFiles);
538 | return $this;
539 | }
540 |
541 | /**
542 | * It processes the JS strings generated by the asset manager.
543 | *
544 | * @throws InvalidArgumentException
545 | */
546 | public function addJsStrings(array $jsStrings): self
547 | {
548 | $this->state->addJsStrings($jsStrings);
549 | return $this;
550 | }
551 |
552 | /**
553 | * It processes the JS variables generated by the asset manager and converts it into JS code.
554 | *
555 | * @throws InvalidArgumentException
556 | */
557 | public function addJsVars(array $jsVars): self
558 | {
559 | $this->state->addJsVars($jsVars);
560 | return $this;
561 | }
562 |
563 | protected function createBeforeRenderEvent(string $viewFile, array $parameters): StoppableEventInterface
564 | {
565 | return new BeforeRender($this, $viewFile, $parameters);
566 | }
567 |
568 | protected function createAfterRenderEvent(
569 | string $viewFile,
570 | array $parameters,
571 | string $result
572 | ): AfterRenderEventInterface {
573 | return new AfterRender($this, $viewFile, $parameters, $result);
574 | }
575 |
576 | /**
577 | * Renders the content to be inserted in the head section.
578 | *
579 | * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
580 | *
581 | * @return string The rendered content
582 | */
583 | private function renderHeadHtml(): string
584 | {
585 | $lines = [];
586 |
587 | if (!empty($this->state->getMetaTags())) {
588 | $lines[] = implode("\n", $this->state->getMetaTags());
589 | }
590 | if (!empty($this->state->getLinkTags()[self::POSITION_HEAD])) {
591 | $lines[] = implode("\n", $this->state->getLinkTags()[self::POSITION_HEAD]);
592 | }
593 | if (!empty($this->state->getCssFiles()[self::POSITION_HEAD])) {
594 | $lines[] = implode("\n", $this->state->getCssFiles()[self::POSITION_HEAD]);
595 | }
596 | if (!empty($this->state->getCss()[self::POSITION_HEAD])) {
597 | $lines[] = $this->generateCss($this->state->getCss()[self::POSITION_HEAD]);
598 | }
599 | if (!empty($this->state->getJsFiles()[self::POSITION_HEAD])) {
600 | $lines[] = implode("\n", $this->state->getJsFiles()[self::POSITION_HEAD]);
601 | }
602 | if (!empty($this->state->getJs()[self::POSITION_HEAD])) {
603 | $lines[] = $this->generateJs($this->state->getJs()[self::POSITION_HEAD]);
604 | }
605 |
606 | return empty($lines) ? '' : implode("\n", $lines);
607 | }
608 |
609 | /**
610 | * Renders the content to be inserted at the beginning of the body section.
611 | *
612 | * The content is rendered using the registered JS code blocks and files.
613 | *
614 | * @return string The rendered content.
615 | */
616 | private function renderBodyBeginHtml(): string
617 | {
618 | $lines = [];
619 |
620 | if (!empty($this->state->getLinkTags()[self::POSITION_BEGIN])) {
621 | $lines[] = implode("\n", $this->state->getLinkTags()[self::POSITION_BEGIN]);
622 | }
623 | if (!empty($this->state->getCssFiles()[self::POSITION_BEGIN])) {
624 | $lines[] = implode("\n", $this->state->getCssFiles()[self::POSITION_BEGIN]);
625 | }
626 | if (!empty($this->state->getCss()[self::POSITION_BEGIN])) {
627 | $lines[] = $this->generateCss($this->state->getCss()[self::POSITION_BEGIN]);
628 | }
629 | if (!empty($this->state->getJsFiles()[self::POSITION_BEGIN])) {
630 | $lines[] = implode("\n", $this->state->getJsFiles()[self::POSITION_BEGIN]);
631 | }
632 | if (!empty($this->state->getJs()[self::POSITION_BEGIN])) {
633 | $lines[] = $this->generateJs($this->state->getJs()[self::POSITION_BEGIN]);
634 | }
635 |
636 | return empty($lines) ? '' : implode("\n", $lines);
637 | }
638 |
639 | /**
640 | * Renders the content to be inserted at the end of the body section.
641 | *
642 | * The content is rendered using the registered JS code blocks and files.
643 | *
644 | * @param bool $ajaxMode Whether the view is rendering in AJAX mode. If true, the JS scripts registered at
645 | * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like normal
646 | * scripts.
647 | *
648 | * @return string The rendered content.
649 | */
650 | private function renderBodyEndHtml(bool $ajaxMode): string
651 | {
652 | $lines = [];
653 |
654 | if (!empty($this->state->getLinkTags()[self::POSITION_END])) {
655 | $lines[] = implode("\n", $this->state->getLinkTags()[self::POSITION_END]);
656 | }
657 | if (!empty($this->state->getCssFiles()[self::POSITION_END])) {
658 | $lines[] = implode("\n", $this->state->getCssFiles()[self::POSITION_END]);
659 | }
660 | if (!empty($this->state->getCss()[self::POSITION_END])) {
661 | $lines[] = $this->generateCss($this->state->getCss()[self::POSITION_END]);
662 | }
663 | if (!empty($this->state->getJsFiles()[self::POSITION_END])) {
664 | $lines[] = implode("\n", $this->state->getJsFiles()[self::POSITION_END]);
665 | }
666 |
667 | if ($ajaxMode) {
668 | $scripts = array_merge(
669 | $this->state->getJs()[self::POSITION_END] ?? [],
670 | $this->state->getJs()[self::POSITION_READY] ?? [],
671 | $this->state->getJs()[self::POSITION_LOAD] ?? [],
672 | );
673 | if (!empty($scripts)) {
674 | $lines[] = $this->generateJs($scripts);
675 | }
676 | } else {
677 | if (!empty($this->state->getJs()[self::POSITION_END])) {
678 | $lines[] = $this->generateJs($this->state->getJs()[self::POSITION_END]);
679 | }
680 | if (!empty($this->state->getJs()[self::POSITION_READY])) {
681 | $js = "document.addEventListener('DOMContentLoaded', function(event) {\n" .
682 | $this->generateJsWithoutTag($this->state->getJs()[self::POSITION_READY]) .
683 | "\n});";
684 | $lines[] = Html::script($js)->render();
685 | }
686 | if (!empty($this->state->getJs()[self::POSITION_LOAD])) {
687 | $js = "window.addEventListener('load', function(event) {\n" .
688 | $this->generateJsWithoutTag($this->state->getJs()[self::POSITION_LOAD]) .
689 | "\n});";
690 | $lines[] = Html::script($js)->render();
691 | }
692 | }
693 |
694 | return empty($lines) ? '' : implode("\n", $lines);
695 | }
696 |
697 | /**
698 | * @param string[]|Style[] $items
699 | */
700 | private function generateCss(array $items): string
701 | {
702 | $lines = [];
703 |
704 | $css = [];
705 | foreach ($items as $item) {
706 | if ($item instanceof Style) {
707 | if ($css !== []) {
708 | $lines[] = Html::style(implode("\n", $css))->render();
709 | $css = [];
710 | }
711 | $lines[] = $item->render();
712 | } else {
713 | $css[] = $item;
714 | }
715 | }
716 | if ($css !== []) {
717 | $lines[] = Html::style(implode("\n", $css))->render();
718 | }
719 |
720 | return implode("\n", $lines);
721 | }
722 |
723 | /**
724 | * @param Script[]|string[] $items
725 | */
726 | private function generateJs(array $items): string
727 | {
728 | $lines = [];
729 |
730 | $js = [];
731 | foreach ($items as $item) {
732 | if ($item instanceof Script) {
733 | if ($js !== []) {
734 | $lines[] = Html::script(implode("\n", $js))->render();
735 | $js = [];
736 | }
737 | $lines[] = $item->render();
738 | } else {
739 | $js[] = $item;
740 | }
741 | }
742 | if ($js !== []) {
743 | $lines[] = Html::script(implode("\n", $js))->render();
744 | }
745 |
746 | return implode("\n", $lines);
747 | }
748 |
749 | /**
750 | * @param Script[]|string[] $items
751 | */
752 | private function generateJsWithoutTag(array $items): string
753 | {
754 | $js = [];
755 | foreach ($items as $item) {
756 | $js[] = $item instanceof Script ? $item->getContent() : $item;
757 | }
758 | return implode("\n", $js);
759 | }
760 | }
761 |
--------------------------------------------------------------------------------