├── src ├── ExtensionInterface.php ├── Template │ ├── TemplateName.php │ ├── TemplateData.php │ └── Template.php └── Engine.php ├── LICENSE ├── composer.json ├── README.md └── CHANGELOG.md /src/ExtensionInterface.php: -------------------------------------------------------------------------------- 1 | */ 15 | private array $folder; 16 | 17 | private string $file; 18 | 19 | public function __construct(Engine $engine, string $name) 20 | { 21 | $this->name = $name; 22 | $parts = explode('::', $this->name); 23 | $count = count($parts); 24 | 25 | if ($count > 2) { 26 | throw new InvalidArgumentException( 27 | 'The template name "' . $this->name . '" is not valid. ' . 28 | 'You must use the folder namespace separator "::" once.' 29 | ); 30 | } 31 | 32 | if (!isset($parts[1])) { 33 | array_unshift($parts, 'main'); 34 | } 35 | 36 | $this->folder = $engine->getPath($parts[0]); 37 | $this->file = $parts[1] . '.' . $engine->getFileExtension(); 38 | } 39 | 40 | public function resolvePath(): string 41 | { 42 | $folderList = array_reverse($this->folder); 43 | 44 | foreach ($folderList as $folder) { 45 | $path = $folder . DIRECTORY_SEPARATOR . $this->file; 46 | 47 | if (is_file($path)) { 48 | return $path; 49 | } 50 | } 51 | 52 | throw new InvalidArgumentException('The template "' . $this->name . '" does not exist.'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobicms/render", 3 | "description": "Native PHP template engine", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "mobicms", 8 | "render", 9 | "templating", 10 | "templates", 11 | "views" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "mobiCMS Contributors", 16 | "homepage": "https://github.com/mobicms/render/graphs/contributors" 17 | } 18 | ], 19 | "config": { 20 | "allow-plugins": { 21 | "dealerdirect/phpcodesniffer-composer-installer": true, 22 | "pestphp/pest-plugin": true 23 | } 24 | }, 25 | "require": { 26 | "php": "~8.2 || ~8.3 || ~8.4" 27 | }, 28 | "require-dev": { 29 | "pestphp/pest": "^3.8", 30 | "slevomat/coding-standard": "^8.19", 31 | "squizlabs/php_codesniffer": "^3.13", 32 | "vimeo/psalm": "^6.12" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Mobicms\\Render\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "MobicmsTest\\Stubs\\": "tests/stubs/src" 42 | } 43 | }, 44 | "scripts": { 45 | "check": [ 46 | "@cs-check", 47 | "@static-analysis", 48 | "@test" 49 | ], 50 | "cs-check": "phpcs", 51 | "cs-fix": "phpcbf", 52 | "static-analysis": "psalm --no-diff --show-info=true", 53 | "test": "pest --colors=always", 54 | "test-coverage": [ 55 | "@putenv XDEBUG_MODE=coverage", 56 | "pest --colors=always --coverage --coverage-clover clover.xml --log-junit report.xml" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Template/TemplateData.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected array $sharedVariables = []; 16 | 17 | /** @var array */ 18 | protected array $templateVariables = []; 19 | 20 | /** 21 | * Add template data 22 | * 23 | * @param array $data 24 | * @param array $templates 25 | */ 26 | public function add(array $data, array $templates = []): self 27 | { 28 | return $templates === [] 29 | ? $this->shareWithAll($data) 30 | : $this->shareWithSome($data, $templates); 31 | } 32 | 33 | /** 34 | * Add data shared with all templates 35 | * 36 | * @param array $data 37 | */ 38 | public function shareWithAll(array $data): self 39 | { 40 | $this->sharedVariables = array_merge($this->sharedVariables, $data); 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Add data shared with some templates 47 | * 48 | * @param array $data 49 | * @param array $templates 50 | */ 51 | public function shareWithSome(array $data, array $templates): self 52 | { 53 | foreach ($templates as $template) { 54 | if (isset($this->templateVariables[$template])) { 55 | $this->templateVariables[$template] = array_merge((array) $this->templateVariables[$template], $data); 56 | } else { 57 | $this->templateVariables[$template] = $data; 58 | } 59 | } 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Get template data 66 | * 67 | * @return array 68 | */ 69 | public function get(?string $template = null): array 70 | { 71 | if (null !== $template && isset($this->templateVariables[$template])) { 72 | return array_merge($this->sharedVariables, (array) $this->templateVariables[$template]); 73 | } 74 | 75 | return $this->sharedVariables; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `mobicms/render` 2 | 3 | [![GitHub](https://img.shields.io/github/license/mobicms/render)](https://github.com/mobicms/render/blob/main/LICENSE) 4 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/mobicms/render)](https://github.com/mobicms/render/releases) 5 | [![Packagist](https://img.shields.io/packagist/dt/mobicms/render)](https://packagist.org/packages/mobicms/render) 6 | 7 | [![CI-Analysis](https://github.com/mobicms/render/workflows/analysis/badge.svg)](https://github.com/mobicms/render/actions?query=workflow%3AAnalysis) 8 | [![CI-Tests](https://github.com/mobicms/render/workflows/tests/badge.svg)](https://github.com/mobicms/render/actions?query=workflow%3ATests) 9 | [![Sonar Coverage](https://img.shields.io/sonar/coverage/mobicms_render?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/code?id=mobicms_render) 10 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mobicms_render&metric=alert_status)](https://sonarcloud.io/summary/overall?id=mobicms_render) 11 | 12 | **mobicms/render is a native PHP template system** that started on the basis of [league/plates v.3.3.0](https://github.com/thephpleague/plates/releases/tag/3.3.0). 13 | The development of the original package went in a direction that was not suitable for our projects where Plates was used, so it was decided to continue development as an independent package. 14 | Our goal was to simplify the source code as much as possible, get rid of the unnecessary and add the missing functionality. 15 | 16 | This package is part of [mobiCMS](https://github.com/mobicms/mobicms) and [JohnCMS](https://github.com/johncms/johncms), 17 | but can be used freely in any other projects. 18 | 19 | ## Installation 20 | 21 | The preferred method of installation is via [Composer](http://getcomposer.org). Run the following 22 | command to install the package and add it as a requirement to your project's 23 | `composer.json`: 24 | 25 | ```bash 26 | composer require mobicms/render 27 | ``` 28 | 29 | 30 | ## Documentation 31 | 32 | Check out the [documentation wiki](https://github.com/mobicms/render/wiki) for detailed information 33 | and code examples. 34 | 35 | 36 | ## Contributing 37 | Contributions are welcome! Please read [Contributing][contributing] for details. 38 | 39 | [![YAGNI](https://img.shields.io/badge/principle-YAGNI-blueviolet.svg)][yagni] 40 | [![KISS](https://img.shields.io/badge/principle-KISS-blueviolet.svg)][kiss] 41 | 42 | In our development, we follow the principles of [YAGNI][yagni] and [KISS][kiss]. 43 | The source code should not have extra unnecessary functionality and should be as simple and efficient as possible. 44 | 45 | 46 | ## License 47 | 48 | This package is licensed for use under the MIT License (MIT). 49 | Please see [LICENSE][license] for more information. 50 | 51 | 52 | ## Our links 53 | - [**mobiCMS Project**][website] website and support forum 54 | - [**GitHub**](https://github.com/mobicms) mobiCMS project repositories 55 | - [**Twitter**](https://twitter.com/mobicms) 56 | 57 | [website]: https://mobicms.org 58 | [yagni]: https://en.wikipedia.org/wiki/YAGNI 59 | [kiss]: https://en.wikipedia.org/wiki/KISS_principle 60 | [contributing]: https://github.com/mobicms/render/blob/main/.github/CONTRIBUTING.md 61 | [license]: https://github.com/mobicms/render/blob/main/LICENSE 62 | -------------------------------------------------------------------------------- /src/Engine.php: -------------------------------------------------------------------------------- 1 | > 26 | */ 27 | private array $nameSpaces = []; 28 | 29 | /** 30 | * Collection of template functions 31 | * 32 | * @var array 33 | */ 34 | protected array $functions = []; 35 | 36 | /** 37 | * Collection of preassigned template data 38 | */ 39 | protected TemplateData $templateData; 40 | 41 | public function __construct(string $fileExtension = 'phtml') 42 | { 43 | $this->fileExtension = $fileExtension; 44 | $this->templateData = new TemplateData(); 45 | } 46 | 47 | /** 48 | * Get the template file extension 49 | */ 50 | public function getFileExtension(): string 51 | { 52 | return $this->fileExtension; 53 | } 54 | 55 | /** 56 | * Add a new template folder for grouping templates under different namespaces 57 | */ 58 | public function addPath(string $folder, string $nameSpace = 'main'): self 59 | { 60 | if ($folder === '') { 61 | throw new InvalidArgumentException('You must specify folder.'); 62 | } 63 | 64 | if ($nameSpace === '') { 65 | throw new InvalidArgumentException('Namespace cannot be empty.'); 66 | } 67 | 68 | $folder = rtrim($folder, '/\\'); 69 | $this->nameSpaces[$nameSpace][] = $folder; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Get a template folder 76 | * 77 | * @return array 78 | */ 79 | public function getPath(string $name): array 80 | { 81 | if (! isset($this->nameSpaces[$name])) { 82 | throw new InvalidArgumentException('The template namespace "' . $name . '" was not found.'); 83 | } 84 | 85 | return $this->nameSpaces[$name]; 86 | } 87 | 88 | /** 89 | * Add preassigned template data 90 | * 91 | * @param array $data 92 | * @param array $templates 93 | */ 94 | public function addData(array $data, array $templates = []): self 95 | { 96 | $this->templateData->add($data, $templates); 97 | return $this; 98 | } 99 | 100 | /** 101 | * Get all preassigned template data 102 | * 103 | * @return array 104 | */ 105 | public function getTemplateData(?string $template = null): array 106 | { 107 | return $this->templateData->get($template); 108 | } 109 | 110 | public function registerFunction(string $name, callable $callback): self 111 | { 112 | if (isset($this->functions[$name])) { 113 | throw new InvalidArgumentException('The template function name "' . $name . '" is already registered.'); 114 | } 115 | 116 | if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $name) !== 1) { 117 | throw new InvalidArgumentException( 118 | 'Not a valid function name.' 119 | ); 120 | } 121 | 122 | $this->functions[$name] = $callback; 123 | return $this; 124 | } 125 | 126 | public function getFunction(string $name): callable 127 | { 128 | if (! isset($this->functions[$name])) { 129 | throw new InvalidArgumentException('The template function "' . $name . '" was not found.'); 130 | } 131 | 132 | return $this->functions[$name]; 133 | } 134 | 135 | public function doesFunctionExist(string $name): bool 136 | { 137 | return isset($this->functions[$name]); 138 | } 139 | 140 | public function loadExtension(ExtensionInterface $extension): self 141 | { 142 | $extension->register($this); 143 | return $this; 144 | } 145 | 146 | /** 147 | * Create a new template and render it 148 | * 149 | * @param array $params 150 | * @throws Throwable 151 | */ 152 | public function render(string $name, array $params = []): string 153 | { 154 | $template = new Template($this, $name); 155 | return $template->render($params); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | Detailed changes can see in the [repository log]. 7 | 8 | 9 | ## [Unreleased] 10 | 11 | #### Added 12 | - Nothing 13 | 14 | #### Changed 15 | - Nothing 16 | 17 | #### Deprecated 18 | - Nothing 19 | 20 | #### Removed 21 | - Nothing 22 | 23 | #### Fixed 24 | - Nothing 25 | 26 | #### Security 27 | - Nothing 28 | 29 | 30 | ## [4.0.0] - 2022-12-10 31 | 32 | #### Added 33 | - PHP 8.2 support 34 | 35 | #### Changed 36 | - Bumped minimum PHP version to 8.0 37 | - Various internal improvements 38 | 39 | 40 | ## [3.0.0] - 2021-07-29 41 | 42 | #### Added 43 | - `Engine::addPath()` 44 | Instead of three arguments of the old addFolder() method (namespace, default folder and search array), 45 | two arguments with a folder and optional namespace are used. 46 | 47 | #### Changed 48 | - Specifying a namespace now is not mandatory. 49 | If it is not specified, it will be used `main::` as default. 50 | - `Engine::getFolder()` renamed to `Engine::getPath()` 51 | - Various code improvements 52 | 53 | #### Removed 54 | - `Engine::addFolder()` 55 | - `TemplateFunftion` class 56 | 57 | 58 | ## [2.1.0] - 2021-07-28 59 | 60 | #### Added 61 | - PHP 8.x support 62 | - Source code static analysis using Psalm 63 | - Checking coding standards using PHP_CodeSniffer 64 | - Test coverage report and code quality rating using Scrutinizer-CI 65 | - CI services using GitHub Actions 66 | 67 | #### Changed 68 | - Used latest version of PhpUnit 69 | - Various code improvements 70 | 71 | #### Removed 72 | - Drop support for PHP older than 7.4 73 | 74 | 75 | ## [2.0.0] - 2019-12-31 76 | 77 | #### Removed 78 | - Template::end() 79 | - Template::insert() 80 | 81 | 82 | ## [1.1.0] - 2019-12-11 83 | 84 | #### Added 85 | - Template::sectionAppend() 86 | - Template::sectionReplace() 87 | 88 | #### Deprecated 89 | - Template::end() 90 | - Template::insert() 91 | 92 | 93 | ## [1.0.1] - 2019-12-08 94 | 95 | #### Changed 96 | - Small internal improvements 97 | 98 | 99 | ## [1.0.0] - 2019-12-05 100 | The development of this package started on the basis of [league/plates v.3.3.0](https://github.com/thephpleague/plates/releases/tag/3.3.0). 101 | The purpose of the development was to simplify the source code as much as possible, get rid of the unnecessary and add the missing functionality. 102 | 103 | Here are the most significant changes compared to the original packag. 104 | 105 | #### Added 106 | - Each namespace can have one default (fallback) folder and optional several search folders. 107 | The template is searched sequentially across all of these folders, from the last to the first. 108 | The first template found will be used. 109 | If not found (or not specified), it will use the template specified by default. 110 | 111 | #### Changed 112 | - All code rewritten to use PHP 7.2 or newer 113 | - All tests rewritten to use new PhpUnit 8.x 114 | - Specify a namespace and its path is mandatory. 115 | Now you cannot call template without specifying a namespace. 116 | - Namespace refactoring 117 | 118 | #### Removed 119 | - Folder with example. 120 | - Documentation (After editing will be added again). 121 | - Removed all extensions that were delivered with the package. 122 | - Due to replacement with a new algorithm, removed old fallback folder functionality. 123 | - Removed existing classes and methods: 124 | `[D]` completely removed as unnecessary 125 | `[C]` covered by new functionality 126 | `[S]` replaced with simpler code 127 | - `[S] Directory::class` 128 | - `[S] FileExtension::class` 129 | - `[S] Folder::class` 130 | - `[S] Folders::class` 131 | - `[S] Functions::class` 132 | - `[C] Engine::setDirectory()` 133 | - `[C] Engine::getDirectory()` 134 | - `[D] Engine::removeFolder()` 135 | - `[D] Engine::dropFunction()` 136 | - `[D] Engine::loadExtensions()` 137 | - `[D] Engine::path()` 138 | - `[D] Engine::exists()` 139 | - `[S] Engine::make()` 140 | - Some other code that is not used. 141 | 142 | [Unreleased]: https://github.com/mobicms/render/compare/4.0.0...HEAD 143 | [4.0.0]: https://github.com/mobicms/render/compare/3.0.0...4.0.0 144 | [3.0.0]: https://github.com/mobicms/render/compare/2.1.0...3.0.0 145 | [2.1.0]: https://github.com/mobicms/render/compare/2.0.0...2.1.0 146 | [2.0.0]: https://github.com/mobicms/render/compare/1.1.0...2.0.0 147 | [1.1.0]: https://github.com/mobicms/render/compare/1.0.1...1.1.0 148 | [1.0.1]: https://github.com/mobicms/render/compare/1.0.0...1.0.1 149 | [1.0.0]: https://github.com/mobicms/render/compare/segregation...1.0.0 150 | [repository log]: https://github.com/mobicms/render/commits/ 151 | -------------------------------------------------------------------------------- /src/Template/Template.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $sections = []; 22 | 23 | private Engine $engine; 24 | 25 | private string $templateFile; 26 | 27 | /** @var array */ 28 | private array $data = []; 29 | 30 | private string $sectionName = ''; 31 | 32 | private bool $appendSection = false; 33 | 34 | private string $layoutName = ''; 35 | 36 | /** @var array */ 37 | private array $layoutData = []; 38 | 39 | public function __construct(Engine $engine, string $name) 40 | { 41 | $this->engine = $engine; 42 | $templateName = new TemplateName($engine, $name); 43 | $this->templateFile = $templateName->resolvePath(); 44 | $this->data($this->engine->getTemplateData($name)); 45 | } 46 | 47 | /** 48 | * Magic method used to call extension functions 49 | * 50 | * @param array $arguments 51 | */ 52 | public function __call(string $name, array $arguments): mixed 53 | { 54 | return call_user_func_array($this->engine->getFunction($name), $arguments); 55 | } 56 | 57 | /** 58 | * Alias for render() method 59 | * 60 | * @throws Throwable 61 | * @throws \Exception 62 | */ 63 | public function __toString(): string 64 | { 65 | return $this->render(); 66 | } 67 | 68 | /** 69 | * Assign or get template data 70 | * 71 | * @param array $data 72 | * @return array 73 | */ 74 | public function data(array $data = []): array 75 | { 76 | $this->data = array_merge($this->data, $data); 77 | 78 | return $this->data; 79 | } 80 | 81 | /** 82 | * Render the template and layout 83 | * 84 | * @param array $data 85 | * @throws Throwable 86 | * 87 | * @psalm-suppress UnusedParam 88 | */ 89 | public function render(array $data = []): string 90 | { 91 | extract($this->data($data), EXTR_SKIP); 92 | 93 | try { 94 | $level = ob_get_level(); 95 | ob_start(); 96 | /** @psalm-suppress UnresolvableInclude */ 97 | include $this->templateFile; //NOSONAR 98 | $content = (string) ob_get_clean(); 99 | 100 | if ($this->layoutName !== '') { 101 | $layout = new self($this->engine, $this->layoutName); 102 | $layout->sections = array_merge($this->sections, ['content' => $content]); 103 | $content = $layout->render($this->layoutData); 104 | } 105 | 106 | return $content; 107 | } catch (Throwable $e) { 108 | while (ob_get_level() > $level) { 109 | ob_end_clean(); 110 | } 111 | 112 | throw $e; 113 | } 114 | } 115 | 116 | /** 117 | * Set the template's layout 118 | * 119 | * @param array $data 120 | */ 121 | public function layout(string $name, array $data = []): void 122 | { 123 | $this->layoutName = $name; 124 | $this->layoutData = $data; 125 | } 126 | 127 | public function sectionReplace(string $name, string $content): void 128 | { 129 | $this->sections[$name] = $content; 130 | } 131 | 132 | public function sectionAppend(string $name, string $content): void 133 | { 134 | if (! isset($this->sections[$name])) { 135 | $this->sections[$name] = ''; 136 | } 137 | 138 | $this->sections[$name] .= $content; 139 | } 140 | 141 | /** 142 | * Start a new section block 143 | */ 144 | public function start(string $name): void 145 | { 146 | if ($name === 'content') { 147 | throw new LogicException( 148 | 'The section name "content" is reserved.' 149 | ); 150 | } 151 | 152 | if ($this->sectionName !== '') { 153 | throw new LogicException('You cannot nest sections within other sections.'); 154 | } 155 | 156 | $this->sectionName = $name; 157 | 158 | ob_start(); 159 | } 160 | 161 | /** 162 | * Start a new append section block 163 | */ 164 | public function push(string $name): void 165 | { 166 | $this->appendSection = true; 167 | $this->start($name); 168 | } 169 | 170 | /** 171 | * Stop the current section block 172 | */ 173 | public function stop(): void 174 | { 175 | if ($this->sectionName === '') { 176 | throw new LogicException( 177 | 'You must start a section before you can stop it.' 178 | ); 179 | } 180 | 181 | if (! isset($this->sections[$this->sectionName])) { 182 | $this->sections[$this->sectionName] = ''; 183 | } 184 | 185 | $this->sections[$this->sectionName] = $this->appendSection 186 | ? $this->sections[$this->sectionName] . (string) ob_get_clean() 187 | : (string) ob_get_clean(); 188 | $this->sectionName = ''; 189 | $this->appendSection = false; 190 | } 191 | 192 | /** 193 | * Returns the content for a section block 194 | */ 195 | public function section(string $name, string $default = ''): string 196 | { 197 | if (! isset($this->sections[$name])) { 198 | return $default; 199 | } 200 | 201 | return $this->sections[$name]; 202 | } 203 | 204 | /** 205 | * Fetch a rendered template 206 | * 207 | * @param array $data 208 | * @throws Throwable 209 | */ 210 | public function fetch(string $name, array $data = []): string 211 | { 212 | return $this->engine->render($name, $data); 213 | } 214 | 215 | /** 216 | * Apply multiple functions to variable 217 | */ 218 | public function batch(string $var, string $functions): mixed 219 | { 220 | foreach (explode('|', $functions) as $function) { 221 | if ($this->engine->doesFunctionExist($function)) { 222 | /** @var mixed $var */ 223 | $var = call_user_func([$this, $function], $var); 224 | } elseif (is_callable($function)) { 225 | /** @var mixed $var */ 226 | $var = $function($var); 227 | } else { 228 | throw new LogicException( 229 | 'The batch function could not find the "' . $function . '" function.' 230 | ); 231 | } 232 | } 233 | 234 | return $var; 235 | } 236 | 237 | /** 238 | * Escape string 239 | */ 240 | public function e(string $string, string $functions = ''): string 241 | { 242 | if ('' !== $functions) { 243 | $string = (string) $this->batch($string, $functions); 244 | } 245 | 246 | return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); 247 | } 248 | } 249 | --------------------------------------------------------------------------------