From 1dc7cc4cf5744e348d3327c2519d2e4d63a175f4 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Wed, 18 Dec 2024 14:46:18 +0200 Subject: [PATCH 1/4] readme --- README.md | 53 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 878cb9f..5d588b8 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,12 @@ echo statements, which supports HTML output. For instance: If you prefer, you can configure the `ViewsManager` to pass models as strings instead of objects. Refer to the ` ViewsManager` section for details. -### 2.2) Custom property defaults +### 2.2) Model naming note + +To clarify: `this package does not require` the `Model suffix` in the names of model classes. In this document, we +use the Model suffix for class names purely for demonstration purposes. + +### 2.3) Custom property defaults Note: In the `TemplateModel` class, in order to satisfy the Model factory, the constructor is marked as final. If you need to @@ -141,7 +146,7 @@ class EmployeeTemplateModel extends BaseTemplateModel > automatically resolved > while object creation by your application's DI system. -### 2.3) Custom Model implementation (advanced usage) +### 2.4) Custom Model implementation (advanced usage) The only requirement for a Model is to implement the `TemplateModelInterface`. This means you can transform any class into a Model without needing to extend a specific base class, or even define public properties: @@ -362,12 +367,18 @@ one namespace within another, preserving their individual setup. Example of multi-namespace usage: -Suppose you have a namespace for Twig templates, including a `Button` model and a `button.twig` template: +Suppose you have registered a namespace for Twig templates: + +```php +$viewsManager->registerNamespace('App\Twig',$configForTwigNamespace); +``` + +This namespace includes a `Button` model and a `button.twig` template: Button's model: ```php -namespace TwigTemplates; +namespace App\Twig; use Prosopo\Views\BaseTemplateModel; @@ -383,12 +394,18 @@ Button's template: ``` -Additionally, you have a namespace for Blade templates, with a `Popup` model: +Additionally, you registered a namespace for Blade templates: + +```php +$viewsManager->registerNamespace('App\Blade',$configForBladeNamespace); +``` + +with a `Popup` model: Popup's model: ```php -namespace BladeTemplates; +namespace App\Blade; use Prosopo\Views\BaseTemplateModel; @@ -396,22 +413,23 @@ class PopupModel extends BaseTemplateModel { } ``` -Now is the cool part: you can safely use `ButtonModel` from the other namespace as a property of the `PopupModel` class: +Now is the cool part: you can safely use the `App\Twig\ButtonModel` class as a property of the +`App\Blade\PopupModel` class, so it looks like this: Popup's model: ```php -namespace BladeTemplates; +namespace App\Blade; use Prosopo\Views\BaseTemplateModel; -use TwigTemplates\ButtonModel; +use App\Twig\ButtonModel; class PopupModel extends BaseTemplateModel { public ButtonModel $buttonModel; } ``` -Popup's template: +Now you can use the `buttonModel` in the popup's template: ```html @@ -423,17 +441,20 @@ Popup's template: When you call `ViewsManager->makeModel()` for the `PopupModel` class: -1. The `PopupModel` instance will be created using the `ModelFactory` configured for the `BladeTemplates` namespace. -2. During automated property initialization, an instance of `ButtonModel` will be created using the `ModelFactory` - configured for the `TwigTemplates` namespace. +1. The `App\Blade\PopupModel` instance will be created using the `ModelFactory` from the `App\Blade` namespace. +2. During automated property initialization, an instance of `App\Twig\ButtonModel` will be created using the + `ModelFactory` + from for the `App\Twig` namespace. This design allows you to seamlessly reuse models across different namespaces while respecting the specific configuration of each namespace. -Namespace resolution also occurs when you call `ViewsManager->renderModel()`. For example: +Namespace resolution also occurs when you call `ViewsManager->renderModel()`. In this example: -* `ButtonModel` will be rendered using the `ViewTemplateRenderer` configured for Twig. -* `PopupModel` will be rendered using the `ViewTemplateRenderer` configured for Blade. +* `App\Twig\ButtonModel` will be rendered using the `ViewTemplateRenderer` from the `App\Twig` namespace, which is + configured for Twig. +* `App\Blade\PopupModel` will be rendered using the `ViewTemplateRenderer` from the `App\Blade` namespace, which is + configured for Blade. ## 4. View Renderer From 9bfd8a8010727f42c179e823c0f98f5043a60dc1 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Wed, 18 Dec 2024 18:51:16 +0200 Subject: [PATCH 2/4] readme --- README.md | 407 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 313 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 5d588b8..463ad8a 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,150 @@ # PHP Views -Blazing fast PHP Views with model-driven approach, multi-namespace support and -custom [Blade](https://laravel.com/docs/11.x/blade) implementation as a default template engine. +> **PHP Views** is a Composer package designed to simplify templating in PHP. It introduces a model-driven approach, +> supports +> multiple namespaces, and includes a custom [Blade](https://laravel.com/docs/11.x/blade) implementation as the default +> template engine. -### Benefits +## Table of Contents -* **Blazing fast:** Outperforms the original Laravel Blade (see the [Benchmark section](#4-benchmark)). -* **Zero Dependencies:** Lightweight and easy to integrate into any project. -* **Wide Compatibility:** PHP 7.4+, 8.0+ -* **Adherence to the [SOLID principles](https://en.wikipedia.org/wiki/SOLID):** The architecture allows you to easily - override any module to meet specific requirements. -* **Namespace Support**: Manage different templates seamlessly under a unified structure. -* **Reliable**: Covered by [Pest](https://pestphp.com/) tests and checked by [PHPStan](https://phpstan.org/). +- [1. Introduction](#1-introduction) +- [2. Installation and minimal usage](#2-installation-and-minimal-usage) +- [3. Model-driven approach](#3-model-driven-approach) +- [4. Views Manager](#4-views-manager) +- [5. View Renderer](#5-view-renderer) +- [6. Contribution](#6-contribution) +- [7. Credits](#7-credits) -### Flexible Usage +## 1. Introduction -You're free to use the package in your own way: +While many PHP frameworks come with their own solutions for views, there are countless PHP projects without a templating +system (including CMS, like WordPress). Writing pure PHP templates can be cumbersome and error-prone. PHP Views aims to +make templating simple, flexible, and accessible to anyone. -* Use it as your Views provider, combining model-driven approach with the built-in Blade. -* Employ its standalone [Blade](https://laravel.com/docs/11.x/blade) implementation as a template engine for your - Blade templates. -* Leverage its model-driven approach for any templates (e.g., [Twig](https://twig.symfony.com/), or pure PHP). -* Use it as a connector for templates that utilize different template engines. +### Freedom to choose your template engine -## Table of Contents +This package includes a custom Blade compiler and uses it as the default template engine. +However, flexibility is key. You can integrate [Twig](https://twig.symfony.com/), or any other template engine, with +just a few lines of code while +still enjoying the benefits of the model-driven approach and other features that PHP Views brings to the table. -- [1. Installation](#1-installation) -- [2. Model-driven approach](#2-model-driven-approach) -- [3. Views Manager](#3-views-manager) -- [4. View Renderer](#4-view-renderer) -- [5. Benchmark](#5-benchmark) -- [6. Contribution](#6-contribution) -- [7. Credits](#7-credits) +### Package Benefits + +* **Blazing fast**: Outperforms the original Laravel Blade (see the benchmark below). +* **Zero Dependencies**: Lightweight and easy to integrate into any project. +* **Wide Compatibility**: PHP 7.4+, 8.0+ +* [SOLID architecture](https://en.wikipedia.org/wiki/SOLID): Designed with flexibility in mind, allowing you to easily + override modules to meet your specific requirements +* **Namespace Support**: Seamlessly manage multiple templates under a unified, structured approach. +* **Reliable**: Thoroughly tested with [Pest](https://pestphp.com/) and checked by [PHPStan](https://phpstan.org/). + +### Usage flexibility + +The strength of this package lies in its flexibility. With PHP Views, you’re free to use it in a way that best fits your +project: + +* **As a Views provider:** Combine a model-driven approach with the built-in Blade engine for clean, dynamic templates. +* **As a standalone Blade engine:** Use its custom Blade implementation for your Blade-based templates. +* **For Model-Driven Flexibility:** Leverage the model-driven approach with any template engine, such + as [Twig](https://twig.symfony.com/) or even pure PHP. +* **As a Template Connector:** Integrate and unify templates that utilize different engines under one system. + +### Benchmark + +We conducted a [PHP performance benchmark](https://github.com/prosopo/php-views/blob/main/benchmark/src/Benchmark.php) +to compare this package with the Laravel's Blade (mocked using [jenssegers/blade](https://github.com/jenssegers/blade)) +and [Twig](https://twig.symfony.com/). Here are the results for 1000x renders: + +| Contestant | First Rendering, MS | Cached Rendering, MS | +|----------------------------------------|---------------------|----------------------| +| `prosopo/views` (without models) | 19.75 | 19.75 (no cache atm) | +| `prosopo/views` (with models) | 43.78 | 43.78 (no cache atm) | +| `illuminate/view` (Blade from Laravel) | 181.24 | 56.77 ms | +| `twig/twig` | 441.13 | 9.47 ms | + +The numbers speak for themselves. In uncached rendering, even with model class-related overhead, PHP Views’ +implementation significantly outperforms all competitors. While Twig offers a robust caching solution, PHP Views still +delivers better performance even than Laravel's Blade engine with caching. + +We used the following package versions: -## 1. Installation +* [illuminate/view](https://packagist.org/packages/illuminate/view) `11.7.0` +* [twig/twig](https://packagist.org/packages/twig/twig) `3.17.1` +* [jenssegers/blade](https://packagist.org/packages/jenssegers/blade) `2.0.1` + +Since the [benchmark](https://github.com/prosopo/php-views/blob/main/benchmark/src/Benchmark.php) is included in this +repository, you can easily run it locally to verify the results. + +1. `git clone https://github.com/prosopo/php-views.git` +2. `composer install; cd benchmark; composer install` +3. `php benchmark {1000}` - pass your renders count + +## 2. Installation and minimal usage + +### 2.1) Installation + +PHP Views is distributed as a Composer package, making installation straightforward: `composer require prosopo/views` -Don't forget to include the composer autoloader in your app: +After installation, ensure that your application includes the Composer autoloader (if it hasn’t been included already): `require __DIR__ . '/vendor/autoload.php';` -## 2. Model-driven approach +### 2.2) Minimal setup + +To get started, you’ll need to create three instances: `ViewTemplateRenderer`, `ViewNamespaceConfig`, and +`ViewsManager`. + +The main configuration takes place in `ViewNamespaceConfig`, where you define the folder for your templates and the root +namespace for the associated models. + +```php +use Prosopo\Views\View\ViewNamespaceConfig; +use Prosopo\Views\View\ViewTemplateRenderer; +use Prosopo\Views\ViewsManager; + +require __DIR__ . '/vendor/autoload.php'; + +// 1. Make the Template Renderer. +// (By default it uses the built-in Blade, but you can connect any) + +$viewTemplateRenderer = new ViewTemplateRenderer(); + +// 2. Make the namespace config + +$namespaceConfig = (new ViewNamespaceConfig($viewTemplateRenderer)) + ->setTemplatesRootPath(__DIR__ . './templates') + ->setTemplateFileExtension('.blade.php'); + +// 3. Make the Views Manager instance: + +$viewsManager = new ViewsManager(); + +// 4. Add the root namespace of your Template Models + +$viewsManager->registerNamespace('MyPackage\Views', $namespaceConfig); +``` + +### 2.3) Model definition + +Now you're ready to create your first model. Similar to many frameworks, such as Laravel, this package embraces a +model-driven approach to templates. -Similar to many frameworks, such as Laravel, this package embraces a model-driven approach to templates. Each template -is paired with its own Model, where the Model's public properties and methods act as arguments available within the -template. +Each template is paired with its own Model, where the Model's public properties and +methods act as arguments available within the template. -Model class: +Model class must extend the `BaseTemplateModel` class or implement the `TemplateModelInterface`: ```php namespace MyPackage\Views; use Prosopo\Views\BaseTemplateModel; -use Prosopo\Views\Interfaces\Model\TemplateModelInterface; class EmployeeTemplateModel extends BaseTemplateModel { public int $salary; public int $bonus; - // Use the abstract interface to accept any Model. - public TemplateModelInterface $innerModel; - // Use a specific class only when you want to restrict the usage to it. public CompanyTemplateModel $company; public function total(): int @@ -80,14 +163,144 @@ from which {{ $salary }} is a salary, and {{ $bonus }} is a bonus. Est. taxes: {{ $company->calcTaxes($salary) }}

-{!! $innerModel !!} -

Company info:

{!! $company !!} ``` -### 2.1) Benefits of the model-driven approach +As you can see, all the public properties of the model are accessible within the template, including nested models like +`$company`. This also enables you to call their public methods directly within the template. + +The `BaseTemplateModel` class which we inherited overrides the `__toString()` method, allowing inner models to be +rendered as strings using [Blade echo statements](https://laravel.com/docs/11.x/blade) with the HTML support. For +instance: + +`{!! $innerModel !!}` part of the template will render the model and print the result. + +> Naming clarification: this package `does not require` the `Model suffix` in the names of model classes. In this +> document, we use +> the +> Model suffix for class names purely for demonstration purposes. + +### 2.4) Automated templates matching + +The built-in `ModelTemplateResolver` automatically matches templates based on the Model names and their relative +namespaces. This automates the process of associating templates with their corresponding Models. + +Example: + +- src/ + - Views/ + - `Homepage.php` + - Settings + - `GeneralSettings.php` + - templates/ + - `homepage{.blade.php}` + - settings/ + - `general-settings{.blade.php}` + +We define the root templates folder along with the models root namespace in the `ViewsNamespaceConfig` (as we already +did above): + +```php +$namespaceConfig = (new ViewNamespaceConfig($viewTemplateRenderer)) + ->setTemplatesRootPath(__DIR__ . './templates') + ->setTemplateFileExtension('.blade.php'); + +// 3. Make the Views Manager instance: + +$viewsManager = new ViewsManager(); + +// 4. Add the root namespace of your Template Models + +$viewsManager->registerNamespace('MyPackage\Views', $namespaceConfig); +``` + +**Naming Note:** Use dashes in template names, as camelCase in Model names is automatically converted to dash-separated +names. + +### 2.5) Usage + +The `ViewsManager` instance we created during setup provides the `createModel` and `renderModel` methods. + +You can create, set values, and render a Model in a single step using the callback argument of the `renderView` method, +as shown below: + +```php +echo $viewsManager->renderModel( + EmployeeTemplateModel::class, + function (EmployeeTemplateModel $employee) use ($salary, $bonus) { + $employee->salary = $salary; + $employee->bonus = $bonus; + } +); +``` + +This approach enables a functional programming style when working with Models. + +**Multi-step creation and rendering** + +When you need split creation, call the `makeModel` method to create the model, and then render later when you need it. + +```php +$employee = $viewsManager->createModel(EmployeeTemplateModel::class); + +// ... + +$employee->salary = $salary; +$employee->bonus = $bonus; + +// ... + +echo $views->renderModel($employee); + +// Tip: you can still pass the callback as the second renderModel() argument +// to customize the Model properties before rendering. +``` + +**References Advice** + +The `ViewsManager` class implements three interfaces: `ViewNamespaceManagerInterface` for `registerNamespace`, +`ModelFactoryInterface` for `createModel`, and `ModelRendererInterface` for `renderModel`. + +When passing the `ViewsManager` instance to your methods, use one of these interfaces as the argument type instead of +the `ViewsManager` class itself. + +This approach ensures that only the specific actions you expect are accessible, promoting cleaner and more maintainable +code. + +> That's it! You’re now ready to start using the package. In the sections below, we'll dive deeper into its +> configuration and implementation details. + +## 3. Model-driven approach + +This package embraces a model-driven approach to templates. Each template is paired with its own Model, where the +Model's public properties and methods act as arguments available within the template. + +Model class must extend the `BaseTemplateModel` class or implement the `TemplateModelInterface`: + +```php +namespace MyPackage\Views; + +use Prosopo\Views\BaseTemplateModel; + +class EmployeeTemplateModel extends BaseTemplateModel +{ + public int $salary; + public int $bonus; +} +``` + +Public model properties intended for use in the template must have a defined type. By default, any public properties +without a type will be ignored. + +> Naming clarification: This package `does not require` the `Model suffix` in the names of model classes. In this +> document, we use +> the Model +> suffix +> for class names purely for demonstration purposes. + +### 3.1) Benefits 1. Typed variables: Eliminate the hassle of type matching and renaming associated with array-driven variables. 2. Reduced Routine: During object creation, public fields of the model without default values are automatically @@ -97,6 +310,30 @@ Est. taxes: {{ $company->calcTaxes($salary) }} maintain flexibility and avoid specifying the exact component. +### 3.2) Inner models + +Model can contain one or more nested models or even an array of models. To allow flexibility in deciding the model +type later, define its type as `TemplateModelInterface`. + +This approach enables you to assign any model to this field +during data loading. However, if you want to restrict the field to a specific model, you can define its exact class +type: + +```php +namespace MyPackage\Views; + +use Prosopo\Views\BaseTemplateModel; +use Prosopo\Views\Interfaces\Model\TemplateModelInterface; + +class EmployeeTemplateModel extends BaseTemplateModel +{ + // Use the abstract interface to accept any Model. + public TemplateModelInterface $innerModel; + // Use a specific class only when you want to restrict the usage to it. + public CompanyTemplateModel $company; +} +``` + By default, inner models are passed to templates as objects, enabling you to call their public methods directly. For example: @@ -107,15 +344,10 @@ echo statements, which supports HTML output. For instance: `{!! $innerModel !!}` will render the model and print the result. -If you prefer, you can configure the `ViewsManager` to pass models as strings instead of objects. Refer to the ` -ViewsManager` section for details. - -### 2.2) Model naming note +If you prefer, you can configure the `ViewsManager` to pass models as strings instead of objects. Refer to the [ +ViewsManager section](#4-views-manager) for details. -To clarify: `this package does not require` the `Model suffix` in the names of model classes. In this document, we -use the Model suffix for class names purely for demonstration purposes. - -### 2.3) Custom property defaults +### 3.3) Custom property defaults Note: In the `TemplateModel` class, in order to satisfy the Model factory, the constructor is marked as final. If you need to @@ -146,7 +378,7 @@ class EmployeeTemplateModel extends BaseTemplateModel > automatically resolved > while object creation by your application's DI system. -### 2.4) Custom Model implementation (advanced usage) +### 3.4) Custom Model implementation (advanced usage) The only requirement for a Model is to implement the `TemplateModelInterface`. This means you can transform any class into a Model without needing to extend a specific base class, or even define public properties: @@ -179,7 +411,7 @@ $namespaceConfig->setModelsAsStringsInTemplates(true); When this option is enabled, the renderer will automatically convert objects implementing `TemplateModelInterface` into strings before passing them to the template. -## 3. Views Manager +## 4. Views Manager The `ViewsManager` class provides the `registerNamespace`, `createModel` and `renderModel` methods. It acts as a namespace manager and brings together different namespace configurations. @@ -188,7 +420,7 @@ Each `ViewNamespace` has its own independent setup and set of modules. E.g. amon `ModelTemplateProvider`, which automates the process of linking models to their corresponding templates. -### 3.1) Setup +### 4.1) Setup ```php use Prosopo\Views\View\ViewNamespaceConfig; @@ -228,7 +460,7 @@ $viewsManager->registerNamespace('MyPackage\Views', $namespaceConfig); // Tip: you can have multiple namespaces, and mix their Models. ``` -### 3.2) Single-step Model creation and rendering +### 4.2) Single-step Model creation and rendering You can create, set values, and render a Model in a single step using the callback argument of the `renderView` method, as shown below: @@ -245,7 +477,7 @@ echo $viewsManager->renderModel( This approach enables a functional programming style when working with Models. -### 3.3) Multi-step creation and rendering +### 4.3) Multi-step creation and rendering When you need split creation, use the factory to create the model, and then render later when you need it. @@ -274,7 +506,7 @@ the `ViewsManager` class itself. This approach ensures that only the specific actions you expect are accessible, promoting cleaner and more maintainable code. -### 3.4) Automated templates matching +### 4.4) Automated templates matching The built-in `ModelTemplateResolver` automatically matches templates based on the Model names and their relative namespaces. This automates the process of associating templates with their corresponding Models. @@ -283,13 +515,29 @@ Example: - src/ - Views/ - - Homepage.php - - settings - - GeneralSettings.php + - `Homepage.php` + - Settings + - `GeneralSettings.php` - templates/ - - homepage{.blade.php} + - `homepage{.blade.php}` - settings/ - - general-settings{.blade.php} + - `general-settings{.blade.php}` + +We define the root templates folder along with the models root namespace in the `ViewsNamespaceConfig`: + +```php +$namespaceConfig = (new ViewNamespaceConfig($viewTemplateRenderer)) + ->setTemplatesRootPath(__DIR__ . './templates') + ->setTemplateFileExtension('.blade.php'); + +// 3. Make the Views Manager instance: + +$viewsManager = new ViewsManager(); + +// 4. Add the root namespace of your Template Models + +$viewsManager->registerNamespace('MyPackage\Views', $namespaceConfig); +``` **Naming Note:** Use dashes in template names, as camelCase in Model names is automatically converted to dash-separated names. @@ -298,7 +546,7 @@ names. > implement your own logic. In case the reason is the name-specific only, consider overriding the `ModelNameResolver` > module instead. -### 3.5) Custom modules +### 4.5) Custom modules By default, the `registerNamespace` class creates module instances for the namespace using classes from the current package. @@ -360,7 +608,7 @@ $namespaceConfig->getModules() > Note: The package includes only the Blade implementation. If you wish to use a different template engine, > like Twig, you need to install its Composer package and create a facade object, as demonstrated above. -### 3.6) Namespace mixing +### 4.6) Namespace mixing The `ViewsManager` class not only supporting multiple namespaces, but also enabling you to use Models from one namespace within another, preserving their individual setup. @@ -456,13 +704,13 @@ Namespace resolution also occurs when you call `ViewsManager->renderModel()`. In * `App\Blade\PopupModel` will be rendered using the `ViewTemplateRenderer` from the `App\Blade` namespace, which is configured for Blade. -## 4. View Renderer +## 5. View Renderer `ViewTemplateRenderer` is the class responsible for rendering templates in this package. By default, it integrates the Blade compiler, but it is fully customizable. You can replace the Blade compiler with your own implementation or use a simple stub to enable support for plain PHP template files. -### 4.1) Built-in Blade integration +### 5.1) Built-in Blade integration [Blade](https://laravel.com/docs/11.x/blade) is an elegant and powerful template engine originally designed for [Laravel](https://laravel.com/). @@ -499,7 +747,7 @@ However, we chose not to adopt this approach for several reasons: Thanks to great Blade's conceptual design, our compiler implementation required fewer than 200 lines of code. -### 4.2) View Renderer setup +### 5.2) View Renderer setup ```php use Prosopo\Views\View\ViewTemplateRenderer; @@ -533,7 +781,7 @@ echo $viewTemplateRenderer->renderTemplate('@if($var)The variable is set.@endif' > means that even if you can't or don't want to use the model-driven approach, you can still utilize it as an > independent Blade compiler. -### 4.3) Available View Renderer settings +### 5.3) Available View Renderer settings The `ViewTemplateRenderer` supports a variety of settings that let you customize features such as escaping, error handling, and more: @@ -575,7 +823,7 @@ $viewRendererConfig = (new ViewTemplateRendererConfig()) $viewTemplateRenderer = new ViewTemplateRenderer($viewRendererConfig); ``` -### 4.4) Custom View Renderer modules +### 5.4) Custom View Renderer modules By default, the `ViewTemplateRenderer` creates module instances using classes from the current package, including the Blade compiler. @@ -623,35 +871,6 @@ $views->registerNamespace('MyApp\Models', $viewNamespaceConfig); Now this namespace is configured to deal with plain PHP template files, while having all the package features, including model-driven approach and template error handling. -## 5. Benchmark - -We conducted a [PHP performance benchmark](https://github.com/prosopo/php-views/blob/main/benchmark/src/Benchmark.php) -to compare this package with the Laravel's Blade (mocked using [jenssegers/blade](https://github.com/jenssegers/blade)) -and [Twig](https://twig.symfony.com/). Here are the results for 1000x renders: - -| Contestant | First Rendering, MS | Cached Rendering, MS | -|----------------------------------------|---------------------|----------------------| -| `prosopo/views` (without models) | 19.75 | 19.75 (no cache atm) | -| `prosopo/views` (with models) | 43.78 | 43.78 (no cache atm) | -| `illuminate/view` (Blade from Laravel) | 181.24 | 56.77 ms | -| `twig/twig` | 441.13 | 9.47 ms | - -We used the following package versions: - -* [illuminate/view](https://packagist.org/packages/illuminate/view) `11.7.0` -* [twig/twig](https://packagist.org/packages/twig/twig) `3.17.1` -* [jenssegers/blade](https://packagist.org/packages/jenssegers/blade) `2.0.1` - -Since the [benchmark](https://github.com/prosopo/php-views/blob/main/benchmark/src/Benchmark.php) is included in this -repository, you can easily run it locally to verify the results. - -1. `git clone https://github.com/prosopo/php-views.git` -2. `composer install; cd benchmark; composer install` -3. `php benchmark {1000}` - pass your renders count - -We encourage you to enhance the benchmark further - feel free to make it more advanced and submit a pull request. We're -happy to review and accept contributions! 🚀 - ## 6. Contribution We would be excited if you decide to contribute! Please read From bfc5fc2442e2654ef6d8bd61fb931f41598de332 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Wed, 18 Dec 2024 19:23:07 +0200 Subject: [PATCH 3/4] readme --- README.md | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 463ad8a..f9d4752 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ ## Table of Contents -- [1. Introduction](#1-introduction) -- [2. Installation and minimal usage](#2-installation-and-minimal-usage) -- [3. Model-driven approach](#3-model-driven-approach) -- [4. Views Manager](#4-views-manager) -- [5. View Renderer](#5-view-renderer) -- [6. Contribution](#6-contribution) -- [7. Credits](#7-credits) +[1. Introduction](#1-introduction) +[2. Installation and minimal usage](#2-installation-and-minimal-usage) +[3. Model-driven approach](#3-model-driven-approach) +[4. Views Manager](#4-views-manager) +[5. View Renderer](#5-view-renderer) +[6. Contribution](#6-contribution) +[7. Credits](#7-credits) ## 1. Introduction @@ -23,7 +23,8 @@ make templating simple, flexible, and accessible to anyone. ### Freedom to choose your template engine -This package includes a custom Blade compiler and uses it as the default template engine. +This package includes a custom [Blade](https://laravel.com/docs/11.x/blade) compiler and uses it as the default template +engine. However, flexibility is key. You can integrate [Twig](https://twig.symfony.com/), or any other template engine, with just a few lines of code while still enjoying the benefits of the model-driven approach and other features that PHP Views brings to the table. @@ -216,12 +217,13 @@ $viewsManager = new ViewsManager(); $viewsManager->registerNamespace('MyPackage\Views', $namespaceConfig); ``` -**Naming Note:** Use dashes in template names, as camelCase in Model names is automatically converted to dash-separated +**Naming Note:** Use dashes in template names, as `camelCase` in Model names is automatically converted to +`dash-separated` names. ### 2.5) Usage -The `ViewsManager` instance we created during setup provides the `createModel` and `renderModel` methods. +The `ViewsManager` instance (which we created during the setup) provides the `createModel` and `renderModel` methods. You can create, set values, and render a Model in a single step using the callback argument of the `renderView` method, as shown below: @@ -539,7 +541,8 @@ $viewsManager = new ViewsManager(); $viewsManager->registerNamespace('MyPackage\Views', $namespaceConfig); ``` -**Naming Note:** Use dashes in template names, as camelCase in Model names is automatically converted to dash-separated +**Naming Note:** Use dashes in template names, as `camelCase` in Model names is automatically converted to +`dash-separated` names. > Tip: In case this approach doesn't work for your setup, you can override the `ModelTemplateResolver` module to @@ -595,13 +598,17 @@ $viewsManager = new ViewsManager(); // 4. Add the namespace (you can have multiple namespaces) $viewsManager->registerNamespace('MyPackage\Views', $namespaceConfig); + +// ... + +$viewsManager->renderModel(MyTwigModel::class); ``` You can override any namespace module in the following way: ```php $namespaceConfig->getModules() - // override any module, like Factory: + // override any available module, like TemplateRenderer or Factory: ->setModelFactory(new MyFactory()); ``` @@ -712,13 +719,17 @@ simple stub to enable support for plain PHP template files. ### 5.1) Built-in Blade integration -[Blade](https://laravel.com/docs/11.x/blade) is an elegant and powerful template engine originally designed -for [Laravel](https://laravel.com/). +[Blade](https://laravel.com/docs/11.x/blade) is an elegant and powerful template engine originally developed +for [Laravel](https://laravel.com/). Unlike [Twig](https://twig.symfony.com/), Blade embraces PHP usage +rather than restricting it. It enhances templates with syntax sugar (which we all love), making them clean and easy to +read. -However, since it isn't available as a standalone package, this package includes its own Blade compiler. +Blade introduces special shorthand tokens that simplify the most cumbersome syntax constructions, while still being +fully-fledged PHP with access to all its functions and capabilities. -It provides full support for [Blade's key features](https://laravel.com/docs/11.x/blade) -while remaining completely independent of Laravel. +Unfortunately, Blade isn't available as a standalone package, so this package includes its own Blade compiler. It +provides full support for [Blade's key features](https://laravel.com/docs/11.x/blade) while remaining completely +independent of Laravel. The following Blade tokens are supported: From 5c8db7fc2ebe41791e64f0c311a665e301d00109 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Thu, 19 Dec 2024 17:14:57 +0200 Subject: [PATCH 4/4] v 1.0.2 - added setupCallback to the ModelFactory --- README.md | 11 +- composer.json | 2 +- private-classes/Model/ModelFactory.php | 3 +- .../ModelFactoryWithDefaultsManagement.php | 3 +- .../Model/ModelFactoryWithSetupCallback.php | 33 +++++ .../Model/ModelRendererWithEventDetails.php | 2 +- private-classes/View/ViewNamespace.php | 3 + .../Model/ModelFactoryInterface.php | 4 +- .../Model/ModelRendererInterface.php | 2 +- src/ViewsManager.php | 6 +- tests/pest/Feature/ViewsManagerTest.php | 132 +++++++++++++++++- .../ModelFactoryWithSetupCallbackTest.php | 0 12 files changed, 184 insertions(+), 17 deletions(-) create mode 100644 private-classes/Model/ModelFactoryWithSetupCallback.php create mode 100644 tests/pest/Unit/Model/ModelFactoryWithSetupCallbackTest.php diff --git a/README.md b/README.md index f9d4752..a291659 100644 --- a/README.md +++ b/README.md @@ -256,8 +256,8 @@ $employee->bonus = $bonus; echo $views->renderModel($employee); -// Tip: you can still pass the callback as the second renderModel() argument -// to customize the Model properties before rendering. +// Tip: you can pass the callback as the second argument for both createModel() and renderModel() models +// to customize the Model properties before returning/rendering. ``` **References Advice** @@ -884,14 +884,15 @@ model-driven approach and template error handling. ## 6. Contribution -We would be excited if you decide to contribute! Please read -the [for-devs.md](https://github.com/prosopo/php-views/blob/main/for-devs.md) file for project guidelines and +We would be excited if you decide to contribute 🤝 + +Please read the [for-devs.md](https://github.com/prosopo/php-views/blob/main/for-devs.md) file for project guidelines and agreements. ## 7. Credits This package was created by [Maxim Akimov](https://github.com/light-source/) during the development of -the [WordPress integration for Prosopo Procaptcha](https://wordpress.org/plugins/prosopo-procaptcha/). +the [WordPress integration for Prosopo Procaptcha](https://github.com/prosopo/procaptcha-wordpress-plugin). [Procaptcha](https://prosopo.io/) is a privacy-friendly and cost-effective alternative to Google reCaptcha. diff --git a/composer.json b/composer.json index 5383e2b..f42e904 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "prosopo/views", - "description": "Blazing fast Views with model-driven approach, multi-namespace support and custom Blade implementation as a default template engine.", + "description": "Blazing fast Views with model-driven approach, Blade and multi-namespace support.", "homepage": "https://github.com/prosopo/php-views", "keywords": [ "Views", diff --git a/private-classes/Model/ModelFactory.php b/private-classes/Model/ModelFactory.php index df302d7..58ec5a5 100644 --- a/private-classes/Model/ModelFactory.php +++ b/private-classes/Model/ModelFactory.php @@ -4,6 +4,7 @@ namespace Prosopo\Views\PrivateClasses\Model; +use Closure; use Prosopo\Views\Interfaces\Model\ModelFactoryInterface; use Prosopo\Views\Interfaces\Object\ObjectReaderInterface; use Prosopo\Views\Interfaces\Object\PropertyValueProviderInterface; @@ -33,7 +34,7 @@ public function __construct( $this->templateRenderer = $templateRenderer; } - public function createModel(string $modelClass) + public function createModel(string $modelClass, ?Closure $setupModelCallback = null) { return new $modelClass( $this->objectReader, diff --git a/private-classes/Model/ModelFactoryWithDefaultsManagement.php b/private-classes/Model/ModelFactoryWithDefaultsManagement.php index 08fe05a..5d337ee 100644 --- a/private-classes/Model/ModelFactoryWithDefaultsManagement.php +++ b/private-classes/Model/ModelFactoryWithDefaultsManagement.php @@ -4,6 +4,7 @@ namespace Prosopo\Views\PrivateClasses\Model; +use Closure; use Prosopo\Views\Interfaces\Model\ModelFactoryInterface; use Prosopo\Views\Interfaces\Model\TemplateModelWithDefaultsInterface; use Prosopo\Views\Interfaces\Object\ObjectPropertyWriterInterface; @@ -29,7 +30,7 @@ public function __construct( $this->objectPropertyWriter = $objectPropertyWriter; } - public function createModel(string $modelClass) + public function createModel(string $modelClass, ?Closure $setupModelCallback = null) { $model = $this->modelFactory->createModel($modelClass); diff --git a/private-classes/Model/ModelFactoryWithSetupCallback.php b/private-classes/Model/ModelFactoryWithSetupCallback.php new file mode 100644 index 0000000..0dac9aa --- /dev/null +++ b/private-classes/Model/ModelFactoryWithSetupCallback.php @@ -0,0 +1,33 @@ +modelFactory = $modelFactory; + } + + public function createModel(string $modelClass, ?Closure $setupModelCallback = null) + { + $model = $this->modelFactory->createModel($modelClass); + + if (null !== $setupModelCallback) { + $setupModelCallback($model); + } + + return $model; + } +} diff --git a/private-classes/Model/ModelRendererWithEventDetails.php b/private-classes/Model/ModelRendererWithEventDetails.php index fff5a3e..1af9c31 100644 --- a/private-classes/Model/ModelRendererWithEventDetails.php +++ b/private-classes/Model/ModelRendererWithEventDetails.php @@ -28,7 +28,7 @@ public function __construct( $this->eventName = $eventName; } - public function renderModel($modelOrClass, Closure $setupModelCallback = null): string + public function renderModel($modelOrClass, ?Closure $setupModelCallback = null): string { $modelClass = true === is_string($modelOrClass) ? $modelOrClass : diff --git a/private-classes/View/ViewNamespace.php b/private-classes/View/ViewNamespace.php index 7490584..69f98a9 100644 --- a/private-classes/View/ViewNamespace.php +++ b/private-classes/View/ViewNamespace.php @@ -15,6 +15,7 @@ PropertyValueProviderForNullable}; use Prosopo\Views\PrivateClasses\Model\{ModelFactory, ModelFactoryWithDefaultsManagement, + ModelFactoryWithSetupCallback, ModelNameResolver, ModelNamespaceResolver, ModelRenderer, @@ -139,6 +140,8 @@ public function __construct( $objectPropertyWriter ); + $realModelFactory = new ModelFactoryWithSetupCallback($realModelFactory); + $realModelRenderer = $modules->getModelRenderer(); $realModelRenderer = null === $realModelRenderer ? new ModelRenderer( diff --git a/src/Interfaces/Model/ModelFactoryInterface.php b/src/Interfaces/Model/ModelFactoryInterface.php index 10a196c..e587613 100644 --- a/src/Interfaces/Model/ModelFactoryInterface.php +++ b/src/Interfaces/Model/ModelFactoryInterface.php @@ -4,6 +4,7 @@ namespace Prosopo\Views\Interfaces\Model; +use Closure; use Exception; interface ModelFactoryInterface @@ -12,10 +13,11 @@ interface ModelFactoryInterface * @template T of TemplateModelInterface * * @param class-string $modelClass + * @param Closure(T):void|null $setupModelCallback * * @return T * * @throws Exception */ - public function createModel(string $modelClass); + public function createModel(string $modelClass, ?Closure $setupModelCallback = null); } diff --git a/src/Interfaces/Model/ModelRendererInterface.php b/src/Interfaces/Model/ModelRendererInterface.php index ac3e2b3..40401b8 100644 --- a/src/Interfaces/Model/ModelRendererInterface.php +++ b/src/Interfaces/Model/ModelRendererInterface.php @@ -17,5 +17,5 @@ interface ModelRendererInterface * * @throws Exception */ - public function renderModel($modelOrClass, Closure $setupModelCallback = null): string; + public function renderModel($modelOrClass, ?Closure $setupModelCallback = null): string; } diff --git a/src/ViewsManager.php b/src/ViewsManager.php index eab1a7e..ab29ae2 100644 --- a/src/ViewsManager.php +++ b/src/ViewsManager.php @@ -61,7 +61,7 @@ public function registerNamespace(string $namespace, ViewNamespaceConfig $config return $viewNamespaceModules; } - public function createModel(string $modelClass) + public function createModel(string $modelClass, ?Closure $setupModelCallback = null) { if (false === $this->isModel($modelClass)) { throw $this->makeWrongModelException($modelClass); @@ -80,10 +80,10 @@ public function createModel(string $modelClass) throw $this->makeNamespaceNotResolvedException($modelNamespace); } - return $modelFactory->createModel($modelClass); + return $modelFactory->createModel($modelClass, $setupModelCallback); } - public function renderModel($modelOrClass, Closure $setupModelCallback = null): string + public function renderModel($modelOrClass, ?Closure $setupModelCallback = null): string { if (false === $this->isModel($modelOrClass)) { throw $this->makeWrongModelException($modelOrClass); diff --git a/tests/pest/Feature/ViewsManagerTest.php b/tests/pest/Feature/ViewsManagerTest.php index fbe1548..98c77e3 100644 --- a/tests/pest/Feature/ViewsManagerTest.php +++ b/tests/pest/Feature/ViewsManagerTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature; +use Closure; use org\bovigo\vfs\vfsStream; use ParseError; use PHPUnit\Framework\TestCase; @@ -517,6 +518,52 @@ public function testRenderSupportsInnerModelPrintWhenModelExtendsTheBaseClass(): $this->assertSame('Hey inner!', $viewsManager->renderModel($topModel)); } + public function testRenderPassesArrayOfInnerModelsAsObjects(): void + { + // given + vfsStream::setup('top', null, [ + 'folder1' => ['top-model.blade.php' => 'Hey {{ true === is_object($inners[0])? "inner object!": "string" }}'], + 'folder2' => [ 'inner-model.blade.php' => 'inner!'], + ]); + $bladeRenderer = new ViewTemplateRenderer(); + $firstNamespaceConfig = (new ViewNamespaceConfig($bladeRenderer)) + ->setTemplatesRootPath(vfsStream::url('top/folder1')) + ->setTemplateFileExtension('.blade.php'); + $secondNamespaceConfig = (new ViewNamespaceConfig($bladeRenderer)) + ->setTemplatesRootPath(vfsStream::url('top/folder2')) + ->setTemplateFileExtension('.blade.php'); + $secondNamespace = $this->defineRealModelClass( + __METHOD__ . '__second', + 'InnerModel', + [], + false + ); + $firstNamespace = $this->defineRealModelClass( + __METHOD__, + 'TopModel', + [ + [ + 'name' => 'inners', + 'visibility' => 'public', + ] + ], + false + ); + $views = new ViewsManager(); + + // when + $views->registerNamespace($firstNamespace, $firstNamespaceConfig); + $views->registerNamespace($secondNamespace, $secondNamespaceConfig); + + $innerModelClass = $secondNamespace . '\\InnerModel'; + $topModelClass = $firstNamespace . '\\TopModel'; + $topModel = new $topModelClass(); + $topModel->inners = [new $innerModelClass()]; + + // then + $this->assertSame('Hey inner object!', $views->renderModel($topModel)); + } + public function testRenderPassesInnerModelsAsStringsWhenFlagIsSet(): void { // given @@ -564,6 +611,53 @@ public function testRenderPassesInnerModelsAsStringsWhenFlagIsSet(): void $this->assertSame('Hey inner!', $views->renderModel($topModel)); } + public function testRenderPassesArrayOfInnerModelsAsStringsWhenFlagIsSet(): void + { + // given + vfsStream::setup('top', null, [ + 'folder1' => ['top-model.blade.php' => 'Hey @foreach ($inners as $inner){!! $inner !!}@endforeach'], + 'folder2' => [ 'inner-model.blade.php' => 'inner!'], + ]); + $bladeRenderer = new ViewTemplateRenderer(); + $firstNamespaceConfig = (new ViewNamespaceConfig($bladeRenderer)) + ->setTemplatesRootPath(vfsStream::url('top/folder1')) + ->setTemplateFileExtension('.blade.php') + ->setModelsAsStringsInTemplates(true); + $secondNamespaceConfig = (new ViewNamespaceConfig($bladeRenderer)) + ->setTemplatesRootPath(vfsStream::url('top/folder2')) + ->setTemplateFileExtension('.blade.php'); + $secondNamespace = $this->defineRealModelClass( + __METHOD__ . '__second', + 'InnerModel', + [], + false + ); + $firstNamespace = $this->defineRealModelClass( + __METHOD__, + 'TopModel', + [ + [ + 'name' => 'inners', + 'visibility' => 'public', + ] + ], + false + ); + $views = new ViewsManager(); + + // when + $views->registerNamespace($firstNamespace, $firstNamespaceConfig); + $views->registerNamespace($secondNamespace, $secondNamespaceConfig); + + $innerModelClass = $secondNamespace . '\\InnerModel'; + $topModelClass = $firstNamespace . '\\TopModel'; + $topModel = new $topModelClass(); + $topModel->inners = [new $innerModelClass()]; + + // then + $this->assertSame('Hey inner!', $views->renderModel($topModel)); + } + public function testRenderSupportsInnerModelsFromDifferentNamespaces(): void { // given @@ -733,6 +827,38 @@ public function testMakeModelImplementsInterface(): void $this->assertSame($modelClass, get_class($model)); } + public function testMakeModelCallsSetupCallback(): void + { + // given + $bladeRenderer = new ViewTemplateRenderer(); + $namespaceConfig = (new ViewNamespaceConfig($bladeRenderer)); + $views = new ViewsManager(); + + $modelNamespace = $this->defineRealModelClass( + __METHOD__, + 'FirstModel', + [ + [ + 'name' => 'message', + 'type' => 'string', + 'visibility' => 'public', + ] + ], + false + ); + + // when + $views->registerNamespace($modelNamespace, $namespaceConfig); + + $modelClass = $modelNamespace . '\\FirstModel'; + $model = $views->createModel($modelClass, function (object $model) { + $model->message = 'Hello World!'; + }); + + // then + $this->assertSame("Hello World!", $model->message); + } + public function testMakeModelSetsDefaultsForModelsThatExtendBaseClass(): void { // given @@ -800,7 +926,7 @@ public function testMakeModelSupportsDifferentNamespaces(): void $firstNamespaceConfig = (new ViewNamespaceConfig($bladeRenderer)); $firstNamespaceConfig->getModules()->setModelFactory(new class implements ModelFactoryInterface{ - public function createModel(string $modelClass): object + public function createModel(string $modelClass, ?Closure $setupModelCallback = null): object { $model = new $modelClass(); @@ -812,7 +938,7 @@ public function createModel(string $modelClass): object $secondNamespaceConfig = (new ViewNamespaceConfig($bladeRenderer)); $secondNamespaceConfig->getModules()->setModelFactory(new class implements ModelFactoryInterface{ - public function createModel(string $modelClass): object + public function createModel(string $modelClass, ?Closure $setupModelCallback = null): object { $model = new $modelClass(); @@ -869,7 +995,7 @@ public function testMakeModelWithDefaultsSupportsDifferentNamespaces(): void $firstNamespaceConfig ->getModules() ->setModelFactory(new class implements ModelFactoryInterface{ - public function createModel(string $modelClass): object + public function createModel(string $modelClass, ?Closure $setupModelCallback = null): object { $model = new $modelClass(); diff --git a/tests/pest/Unit/Model/ModelFactoryWithSetupCallbackTest.php b/tests/pest/Unit/Model/ModelFactoryWithSetupCallbackTest.php new file mode 100644 index 0000000..e69de29