Added new View Decorators. - #5567
Conversation
iRedds
left a comment
There was a problem hiding this comment.
This proposal defines global decorators, which makes it impossible to generate a view without decorators or selectively apply them.
| if (empty($decorators)) { | ||
| return $html; | ||
| } |
There was a problem hiding this comment.
The decorators property is an array. An empty array will not be processed by foreach.
It seems to me there is no point in this check.
There was a problem hiding this comment.
Using empty() is conceptually expandable to if (! isset(config('View')->decorators) || config('View')->decorators) === []) - in other words, it covers backwards-compatibility for projects that don't add the $decorators property to their App Config file. Alternatively we could (should?) add $decorators to CodeIgniter\Config\View in which case then I would agree we can drop this check.
I know @lonnieezell has been opposed to duplicating Config values between system/ and app/, but given that the View config class already exists in system/ I suspect this is the way to go.
There was a problem hiding this comment.
I did add it the system config file. Well I've corrected that since I put it in the wrong file last night. Silly me :) Will remove.
| * chance to modify the output from the view() calls | ||
| * prior to it being cached. | ||
| */ | ||
| interface ViewDecorator |
There was a problem hiding this comment.
I think that the name of the interface should contain the suffix Interface.
There was a problem hiding this comment.
I think the current name makes sense in the subdirectory Interfaces, but none of our other libraries have a dedicated Interfaces namespace so I would be in favor of moving this file up a directory and renaming it as @iRedds suggests.
There was a problem hiding this comment.
Agree - will make it match the rest of them. I was even the one who initially argued the point that they should be named that way.
| foreach ($decorators as $className) { | ||
| $decorator = new $className(); | ||
|
|
||
| if (! $decorator instanceof ViewDecorator) { | ||
| throw ViewException::forInvalidDecorator($className); | ||
| } | ||
|
|
||
| $html = $decorator->decorate($html); |
There was a problem hiding this comment.
This code can be optimized.
| foreach ($decorators as $className) { | |
| $decorator = new $className(); | |
| if (! $decorator instanceof ViewDecorator) { | |
| throw ViewException::forInvalidDecorator($className); | |
| } | |
| $html = $decorator->decorate($html); | |
| foreach ($decorators as $decorator) | |
| if (! is_subclass_of($decorator, ViewDecorator::class) { | |
| throw ViewException::forInvalidDecorator($decorator); | |
| } | |
| $html = (new $decorator())->decorate($html); |
There was a problem hiding this comment.
If we go with the static method suggestion above this can be simplified even more:
| foreach ($decorators as $className) { | |
| $decorator = new $className(); | |
| if (! $decorator instanceof ViewDecorator) { | |
| throw ViewException::forInvalidDecorator($className); | |
| } | |
| $html = $decorator->decorate($html); | |
| foreach ($decorators as $className) { | |
| if (! is_subclass_of($decorator, ViewDecorator::class) { | |
| throw ViewException::forInvalidDecorator($className); | |
| } | |
| $html = $decorator::decorate($html); |
There was a problem hiding this comment.
I like the static method suggestion here. Will change and simplify.
|
Can you give a real life example of using view decorators? |
MGatner
left a comment
There was a problem hiding this comment.
I'm super excited for this! Lots of comments, and also some CI fixes that CS Fixer/Rector ought tot take care of.
| if (empty($decorators)) { | ||
| return $html; | ||
| } |
There was a problem hiding this comment.
Using empty() is conceptually expandable to if (! isset(config('View')->decorators) || config('View')->decorators) === []) - in other words, it covers backwards-compatibility for projects that don't add the $decorators property to their App Config file. Alternatively we could (should?) add $decorators to CodeIgniter\Config\View in which case then I would agree we can drop this check.
I know @lonnieezell has been opposed to duplicating Config values between system/ and app/, but given that the View config class already exists in system/ I suspect this is the way to go.
| * chance to modify the output from the view() calls | ||
| * prior to it being cached. | ||
| */ | ||
| interface ViewDecorator |
There was a problem hiding this comment.
I think the current name makes sense in the subdirectory Interfaces, but none of our other libraries have a dedicated Interfaces namespace so I would be in favor of moving this file up a directory and renaming it as @iRedds suggests.
| 'noCellClass' => 'No view cell class provided.', | ||
| 'invalidCellClass' => 'Unable to locate view cell class: {0}.', | ||
| 'tagSyntaxError' => 'You have a syntax error in your Parser tags: {0}', | ||
| 'invalidDecoratorClass' => '{0} is not a valid ViewDecorator.', |
There was a problem hiding this comment.
If we go with the rename I think this would read better without the Interface suffix so I suggest making it natural:
| 'invalidDecoratorClass' => '{0} is not a valid ViewDecorator.', | |
| 'invalidDecoratorClass' => '{0} is not a valid View Decorator.', |
| foreach ($decorators as $className) { | ||
| $decorator = new $className(); | ||
|
|
||
| if (! $decorator instanceof ViewDecorator) { | ||
| throw ViewException::forInvalidDecorator($className); | ||
| } | ||
|
|
||
| $html = $decorator->decorate($html); |
There was a problem hiding this comment.
If we go with the static method suggestion above this can be simplified even more:
| foreach ($decorators as $className) { | |
| $decorator = new $className(); | |
| if (! $decorator instanceof ViewDecorator) { | |
| throw ViewException::forInvalidDecorator($className); | |
| } | |
| $html = $decorator->decorate($html); | |
| foreach ($decorators as $className) { | |
| if (! is_subclass_of($decorator, ViewDecorator::class) { | |
| throw ViewException::forInvalidDecorator($className); | |
| } | |
| $html = $decorator::decorate($html); |
@iRedds I'm excited for this because it takes some pressure off of Most of my "HTML injection" libraries use the Filters method, here is an example that would be replaced by a single Decorator: https://github.com/tattersoftware/codeigniter4-alerts/blob/96cd7fa922cad556fb20f015d746f38fb85c748f/src/Filters/AlertsFilter.php One other "plus" of Decorators is they are a lot easier to configure and apply! For a third-party library to activate its Filter it needs to use one of these hack-y FIlter configs: ... and then the implementing project still needs to add it to the desired routes in app/Config/Filters.php: public $globals = [
'after' => [
'alerts' => '*',
],
]; |
|
@lonnieezell what about some built-in tools or core handling for non-HTML output? Is there any scenario where we need to be "header-aware" (like disabling Decorators for JSON content), or is that up to the developer? For example, I add this to all my HTML-injection Filters: |
|
Thinking about this more my hunch is that View doesn't care at all about formatting or headers and it would be on the developer to know that passing content through will subject it to Decorators. I suspect most APIs dealing with JSON/XML aren't using View for the content anyway. Probably worth mentioning in the User Guide either way. |
Two of them: I've implemented View Components in Bonfire, but they require on overriding the View class to work. Essentially they look for custom html tags and replace them with other content. It could also be used for a "snippet" functionality that kept small bits of text/html and will replace placeholders with it globally. I've seen this on some CMS' before. |
I agree with your assessment that View doesn't care here. |
MGatner
left a comment
There was a problem hiding this comment.
I assume you are still looking at some of the others. One typing change here.
Also, is there a case to be made for View::enableDecorators()/View::disableDecorators()?
|
Oh - @MGatner this should target 4.2, also, shouldn't it? |
|
I think everything is accounted for now, though I had to skip the pre-commit checks since there are two PHPStan errors for me locally with the latest develop in |
My initial instinct is not really, but both you and IReds brought something similar up so maybe. :) |
|
php-cs-fixer check failed, but it seems php-cs-fixer's 1) CodeIgniter4/tests/system/Cookie/CookieStoreTest.php (phpdoc_types_order)
---------- begin diff ----------
--- /home/runner/work/CodeIgniter4/CodeIgniter4/tests/system/Cookie/CookieStoreTest.php
+++ /home/runner/work/CodeIgniter4/CodeIgniter4/tests/system/Cookie/CookieStoreTest.php
@@ -107,7 +107,7 @@
$prod = $cookies['prod']->getOptions();
/**
- * @var MockObject&CookieStore $store
+ * @var CookieStore|MockObject $store
*/
$store = $this->getMockBuilder(CookieStore::class)
->setConstructorArgs([$cookies])https://github.com/codeigniter4/CodeIgniter4/runs/4812733176?check_suite_focus=true |
We merge 4.2 into develop! We'll see how that goes 😅 |
MGatner
left a comment
There was a problem hiding this comment.
A few wording changes. And since we are >= 7.4 now: typed properties!
Co-authored-by: MGatner <mgatner@icloud.com>
Co-authored-by: MGatner <mgatner@icloud.com>
Co-authored-by: MGatner <mgatner@icloud.com>
Co-authored-by: MGatner <mgatner@icloud.com>
Co-authored-by: MGatner <mgatner@icloud.com>
Co-authored-by: MGatner <mgatner@icloud.com>
MGatner
left a comment
There was a problem hiding this comment.
Looks good to me! See if there are any workflow issues remaining.
…4 into view-decorators
|
🥳 |
Description
Created View Decorators that allow you to modify HTML generated from the View or Parser classes just prior to it being cached.
Checklist: