├── 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 | Yii 4 | 5 |

Yii View Rendering Library

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/view/v)](https://packagist.org/packages/yiisoft/view) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/view/downloads)](https://packagist.org/packages/yiisoft/view) 11 | [![Build status](https://github.com/yiisoft/view/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/view/actions/workflows/build.yml) 12 | [![Code Coverage](https://codecov.io/gh/yiisoft/view/graph/badge.svg?token=V9PmRxWk9L)](https://codecov.io/gh/yiisoft/view) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fview%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/view/master) 14 | [![Static analysis](https://github.com/yiisoft/view/actions/workflows/static.yml/badge.svg?branch=master)](https://github.com/yiisoft/view/actions/workflows/static.yml?query=branch%3Amaster) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/view/coverage.svg)](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 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 178 | 179 | ### Follow updates 180 | 181 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 182 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 183 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 184 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 185 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](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 | --------------------------------------------------------------------------------