Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new View Decorators. #5567

Merged
merged 13 commits into from
Jan 15, 2022
Merged

Added new View Decorators. #5567

merged 13 commits into from
Jan 15, 2022

Conversation

lonnieezell
Copy link
Member

Description
Created View Decorators that allow you to modify HTML generated from the View or Parser classes just prior to it being cached.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

Copy link
Collaborator

@iRedds iRedds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This proposal defines global decorators, which makes it impossible to generate a view without decorators or selectively apply them.

Comment on lines 24 to 26
if (empty($decorators)) {
return $html;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the name of the interface should contain the suffix Interface.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 28 to 35
foreach ($decorators as $className) {
$decorator = new $className();

if (! $decorator instanceof ViewDecorator) {
throw ViewException::forInvalidDecorator($className);
}

$html = $decorator->decorate($html);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code can be optimized.

Suggested change
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);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go with the static method suggestion above this can be simplified even more:

Suggested change
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);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the static method suggestion here. Will change and simplify.

@kenjis kenjis added the enhancement PRs that improve existing functionalities label Jan 13, 2022
@iRedds
Copy link
Collaborator

iRedds commented Jan 13, 2022

Can you give a real life example of using view decorators?

Copy link
Member

@MGatner MGatner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm super excited for this! Lots of comments, and also some CI fixes that CS Fixer/Rector ought tot take care of.

Comment on lines 24 to 26
if (empty($decorators)) {
return $html;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

system/View/Interfaces/ViewDecorator.php Outdated Show resolved Hide resolved
app/Config/View.php Outdated Show resolved Hide resolved
@@ -17,4 +17,5 @@
'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.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go with the rename I think this would read better without the Interface suffix so I suggest making it natural:

Suggested change
'invalidDecoratorClass' => '{0} is not a valid ViewDecorator.',
'invalidDecoratorClass' => '{0} is not a valid View Decorator.',

system/View/Parser.php Show resolved Hide resolved
system/View/Traits/DecoratesViews.php Outdated Show resolved Hide resolved
system/View/Traits/DecoratesViews.php Outdated Show resolved Hide resolved
Comment on lines 28 to 35
foreach ($decorators as $className) {
$decorator = new $className();

if (! $decorator instanceof ViewDecorator) {
throw ViewException::forInvalidDecorator($className);
}

$html = $decorator->decorate($html);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go with the static method suggestion above this can be simplified even more:

Suggested change
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);

system/View/View.php Outdated Show resolved Hide resolved
@MGatner
Copy link
Member

MGatner commented Jan 13, 2022

Can you give a real life example of using view decorators?

@iRedds I'm excited for this because it takes some pressure off of Filters while still leveraging View Caching. Currently the only "middleware" solution we have for modifying the Response is to use a Filter with the after() method. IMO this puts too much pressure on Filters plus, as I mentioned, it means that your Filters run on the cached View content every time unnecessarily.

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' => '*',
        ],
    ];

@MGatner
Copy link
Member

MGatner commented Jan 13, 2022

@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:

https://github.com/tattersoftware/codeigniter4-alerts/blob/develop/src/Filters/AlertsFilter.php#L55-L68

@MGatner
Copy link
Member

MGatner commented Jan 13, 2022

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.

@lonnieezell
Copy link
Member Author

Can you give a real life example of using view decorators?

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.

@lonnieezell
Copy link
Member Author

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?

I agree with your assessment that View doesn't care here.

Copy link
Member

@MGatner MGatner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()?

system/Config/View.php Outdated Show resolved Hide resolved
@lonnieezell
Copy link
Member Author

Oh - @MGatner this should target 4.2, also, shouldn't it?

@lonnieezell
Copy link
Member Author

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 text_helper and the Autoloader.

@lonnieezell
Copy link
Member Author

Also, is there a case to be made for View::enableDecorators()/View::disableDecorators()?

My initial instinct is not really, but both you and IReds brought something similar up so maybe. :)

@kenjis
Copy link
Member

kenjis commented Jan 14, 2022

php-cs-fixer check failed, but it seems php-cs-fixer's phpdoc_types_order has a bug.

   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

@MGatner
Copy link
Member

MGatner commented Jan 14, 2022

Oh - @MGatner this should target 4.2, also, shouldn't it?

We merge 4.2 into develop! We'll see how that goes 😅

Copy link
Member

@MGatner MGatner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few wording changes. And since we are >= 7.4 now: typed properties!

system/View/ViewDecoratorTrait.php Outdated Show resolved Hide resolved
user_guide_src/source/outgoing/view_decorators.rst Outdated Show resolved Hide resolved
user_guide_src/source/outgoing/view_decorators.rst Outdated Show resolved Hide resolved
user_guide_src/source/outgoing/view_decorators.rst Outdated Show resolved Hide resolved
app/Config/View.php Outdated Show resolved Hide resolved
system/Config/View.php Outdated Show resolved Hide resolved
lonnieezell and others added 6 commits January 14, 2022 11:52
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>
Copy link
Member

@MGatner MGatner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! See if there are any workflow issues remaining.

@lonnieezell lonnieezell merged commit 6f46c35 into develop Jan 15, 2022
@kenjis kenjis deleted the view-decorators branch January 15, 2022 06:52
@MGatner
Copy link
Member

MGatner commented Jan 15, 2022

🥳

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement PRs that improve existing functionalities
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants