-
Notifications
You must be signed in to change notification settings - Fork 93
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
[BUGFIX] Properly restore renderingContext in renderChildren() #970
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
s2b
force-pushed
the
bugfix/renderChildren
branch
2 times, most recently
from
August 17, 2024 21:24
b20dc5d
to
097d7dc
Compare
It has long been possible to render recursive templates with Fluid, either by using partials or sections. In both cases, `<f:render />` is used to initiate the recursive behavior. For example, the following template creates a recursive main menu by using sections: ```xml <f:render section="menu" arguments="{items: mainMenu}" /> <f:section name="menu"> <ul> <f:for each="{items}" as="item"> <li> {item.title} <f:if condition="{item.children}"> <f:render section="menu" arguments="{items: item.children}" /> </f:if> </li> </f:for> </ul> </f:section> ``` However, this was only working because `ForViewHelper` was implemented as static ViewHelper by using Fluid's trait `CompileWithRenderStatic`. The trait changed Fluid's handling of ViewHelpers in a way that variables stay consistent even during recursions. This was made possible back in Fluid 1.0.5 with 819ca24. However, this never worked properly with ViewHelpers not using `renderStatic()` but rather the default `render()` method. In those implementations, `$this->renderChildren()` is used to access and render the ViewHelper's child nodes rather than executing the closure directly passed to `renderStatic()`. To support recursions also for ViewHelpers not using `renderStatic()`, this patch introduces new state to `AbstractViewHelper` to keep track of rendering contexts during recursions. Note that this state is only used for uncached templates, for cached templates this already worked without any changes. To explain the new behavior, let's take the example above and let's also assume that `ForViewHelper` is implemented using `render()` rather than `renderStatic()`. In uncached templates, the new behavior works as follows: 1. `$view->render()` gets called for the template 2. `TemplateParser` creates nodes from template string, among them one `ViewHelperNode` for each ViewHelper. 3. `RenderViewHelper` gets called by executing `$viewHelperNode->execute()`, which in turn calls `ViewHelperInvoker`, which in the end calls the render method of the ViewHelper (in this case `RenderViewHelper::renderStatic()`) 4. `RenderViewHelper` calls `$view->renderSection()`, which clones the current rendering context and puts the appropriate variables in it. The old rendering context gets saved to be restored later for the parent template. 5. Inside the section, `ForViewHelper` gets called, we end up in `ForViewHelper::render()` (because we use our custom implementation without `renderStatic()`) 6. `$this->renderChildren()` is called for each `{item}`. 7. If `{item.children}` is defined, things get interesting. `RenderViewHelper` calls `$view->renderSection()` again, which creates and saves another new rendering context, see 4. We're in a recursion now. 8. With each recursion level finishing, the view restores the original rendering context, so that the variables before the recursion are available to the rest of the template file. So the template/partial/section gets restored properly, but the `ForViewHelper` still has the last rendering context from within the recursion because Fluid didn't restore it to the previous state. In fact, Fluid couldn't really do that from outside because `$forViewHelper->renderChildren()` initiated the whole recursion and is still running (once for each recursion level). To solve this dilemma, this patch implements a rendering context stack similarly to the one used in the `$view` object, but on the ViewHelper level. Each ViewHelper keeps track of recursive rendering contexts for itself – again, only for uncached, non-static ViewHelpers. In addition to that, the patch simplifies the logic of `buildRenderChildrenClosure()`, which covers both cached/uncached and the contentArgumentName feature.
s2b
force-pushed
the
bugfix/renderChildren
branch
from
August 17, 2024 21:25
097d7dc
to
b44c1ae
Compare
lolli42
approved these changes
Aug 20, 2024
get. state. right. |
s2b
added a commit
that referenced
this pull request
Aug 20, 2024
It has long been possible to render recursive templates with Fluid, either by using partials or sections. In both cases, `<f:render />` is used to initiate the recursive behavior. For example, the following template creates a recursive main menu by using sections: ```xml <f:render section="menu" arguments="{items: mainMenu}" /> <f:section name="menu"> <ul> <f:for each="{items}" as="item"> <li> {item.title} <f:if condition="{item.children}"> <f:render section="menu" arguments="{items: item.children}" /> </f:if> </li> </f:for> </ul> </f:section> ``` However, this was only working because `ForViewHelper` was implemented as static ViewHelper by using Fluid's trait `CompileWithRenderStatic`. The trait changed Fluid's handling of ViewHelpers in a way that variables stay consistent even during recursions. This was made possible back in Fluid 1.0.5 with 819ca24. However, this never worked properly with ViewHelpers not using `renderStatic()` but rather the default `render()` method. In those implementations, `$this->renderChildren()` is used to access and render the ViewHelper's child nodes rather than executing the closure directly passed to `renderStatic()`. To support recursions also for ViewHelpers not using `renderStatic()`, this patch introduces new state to `AbstractViewHelper` to keep track of rendering contexts during recursions. Note that this state is only used for uncached templates, for cached templates this already worked without any changes. To explain the new behavior, let's take the example above and let's also assume that `ForViewHelper` is implemented using `render()` rather than `renderStatic()`. In uncached templates, the new behavior works as follows: 1. `$view->render()` gets called for the template 2. `TemplateParser` creates nodes from template string, among them one `ViewHelperNode` for each ViewHelper. 3. `RenderViewHelper` gets called by executing `$viewHelperNode->execute()`, which in turn calls `ViewHelperInvoker`, which in the end calls the render method of the ViewHelper (in this case `RenderViewHelper::renderStatic()`) 4. `RenderViewHelper` calls `$view->renderSection()`, which clones the current rendering context and puts the appropriate variables in it. The old rendering context gets saved to be restored later for the parent template. 5. Inside the section, `ForViewHelper` gets called, we end up in `ForViewHelper::render()` (because we use our custom implementation without `renderStatic()`) 6. `$this->renderChildren()` is called for each `{item}`. 7. If `{item.children}` is defined, things get interesting. `RenderViewHelper` calls `$view->renderSection()` again, which creates and saves another new rendering context, see 4. We're in a recursion now. 8. With each recursion level finishing, the view restores the original rendering context, so that the variables before the recursion are available to the rest of the template file. So the template/partial/section gets restored properly, but the `ForViewHelper` still has the last rendering context from within the recursion because Fluid didn't restore it to the previous state. In fact, Fluid couldn't really do that from outside because `$forViewHelper->renderChildren()` initiated the whole recursion and is still running (once for each recursion level). To solve this dilemma, this patch implements a rendering context stack similarly to the one used in the `$view` object, but on the ViewHelper level. Each ViewHelper keeps track of recursive rendering contexts for itself – again, only for uncached, non-static ViewHelpers. In addition to that, the patch simplifies the logic of `buildRenderChildrenClosure()`, which covers both cached/uncached and the contentArgumentName feature.
s2b
added a commit
that referenced
this pull request
Aug 20, 2024
…#980) It has long been possible to render recursive templates with Fluid, either by using partials or sections. In both cases, `<f:render />` is used to initiate the recursive behavior. For example, the following template creates a recursive main menu by using sections: ```xml <f:render section="menu" arguments="{items: mainMenu}" /> <f:section name="menu"> <ul> <f:for each="{items}" as="item"> <li> {item.title} <f:if condition="{item.children}"> <f:render section="menu" arguments="{items: item.children}" /> </f:if> </li> </f:for> </ul> </f:section> ``` However, this was only working because `ForViewHelper` was implemented as static ViewHelper by using Fluid's trait `CompileWithRenderStatic`. The trait changed Fluid's handling of ViewHelpers in a way that variables stay consistent even during recursions. This was made possible back in Fluid 1.0.5 with 819ca24. However, this never worked properly with ViewHelpers not using `renderStatic()` but rather the default `render()` method. In those implementations, `$this->renderChildren()` is used to access and render the ViewHelper's child nodes rather than executing the closure directly passed to `renderStatic()`. To support recursions also for ViewHelpers not using `renderStatic()`, this patch introduces new state to `AbstractViewHelper` to keep track of rendering contexts during recursions. Note that this state is only used for uncached templates, for cached templates this already worked without any changes. To explain the new behavior, let's take the example above and let's also assume that `ForViewHelper` is implemented using `render()` rather than `renderStatic()`. In uncached templates, the new behavior works as follows: 1. `$view->render()` gets called for the template 2. `TemplateParser` creates nodes from template string, among them one `ViewHelperNode` for each ViewHelper. 3. `RenderViewHelper` gets called by executing `$viewHelperNode->execute()`, which in turn calls `ViewHelperInvoker`, which in the end calls the render method of the ViewHelper (in this case `RenderViewHelper::renderStatic()`) 4. `RenderViewHelper` calls `$view->renderSection()`, which clones the current rendering context and puts the appropriate variables in it. The old rendering context gets saved to be restored later for the parent template. 5. Inside the section, `ForViewHelper` gets called, we end up in `ForViewHelper::render()` (because we use our custom implementation without `renderStatic()`) 6. `$this->renderChildren()` is called for each `{item}`. 7. If `{item.children}` is defined, things get interesting. `RenderViewHelper` calls `$view->renderSection()` again, which creates and saves another new rendering context, see 4. We're in a recursion now. 8. With each recursion level finishing, the view restores the original rendering context, so that the variables before the recursion are available to the rest of the template file. So the template/partial/section gets restored properly, but the `ForViewHelper` still has the last rendering context from within the recursion because Fluid didn't restore it to the previous state. In fact, Fluid couldn't really do that from outside because `$forViewHelper->renderChildren()` initiated the whole recursion and is still running (once for each recursion level). To solve this dilemma, this patch implements a rendering context stack similarly to the one used in the `$view` object, but on the ViewHelper level. Each ViewHelper keeps track of recursive rendering contexts for itself – again, only for uncached, non-static ViewHelpers. In addition to that, the patch simplifies the logic of `buildRenderChildrenClosure()`, which covers both cached/uncached and the contentArgumentName feature.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
It has long been possible to render recursive templates with Fluid,
either by using partials or sections. In both cases,
<f:render />
is used to initiate the recursive behavior.
For example, the following template creates a recursive main menu by
using sections:
However, this was only working because
ForViewHelper
wasimplemented as static ViewHelper by using Fluid's trait
CompileWithRenderStatic
. The trait changed Fluid's handlingof ViewHelpers in a way that variables stay consistent even
during recursions. This was made possible back in Fluid 1.0.5
with 819ca24.
However, this never worked properly with ViewHelpers not using
renderStatic()
but rather the defaultrender()
method. In thoseimplementations,
$this->renderChildren()
is used to access andrender the ViewHelper's child nodes rather than executing the closure
directly passed to
renderStatic()
.To support recursions also for ViewHelpers not using
renderStatic()
,this patch introduces new state to
AbstractViewHelper
to keeptrack of rendering contexts during recursions. Note that this state
is only used for uncached templates, for cached templates this already
worked without any changes.
To explain the new behavior, let's take the example above and let's
also assume that
ForViewHelper
is implemented usingrender()
ratherthan
renderStatic()
. In uncached templates, the new behavior worksas follows:
$view->render()
gets called for the templateTemplateParser
creates nodes from template string, among themone
ViewHelperNode
for each ViewHelper.RenderViewHelper
gets called by executing$viewHelperNode->execute()
,which in turn calls
ViewHelperInvoker
, which in the end calls the rendermethod of the ViewHelper (in this case
RenderViewHelper::renderStatic()
)RenderViewHelper
calls$view->renderSection()
, which clones thecurrent rendering context and puts the appropriate variables in it. The old
rendering context gets saved to be restored later for the parent template.
ForViewHelper
gets called, we end up inForViewHelper::render()
(because we use our custom implementation withoutrenderStatic()
)$this->renderChildren()
is called for each{item}
.{item.children}
is defined, things get interesting.RenderViewHelper
calls
$view->renderSection()
again, which creates and saves another newrendering context, see 4. We're in a recursion now.
rendering context, so that the variables before the recursion are available
to the rest of the template file.
So the template/partial/section gets restored properly, but the
ForViewHelper
still has the last rendering context from within the recursionbecause Fluid didn't restore it to the previous state. In fact, Fluid couldn't
really do that from outside because
$forViewHelper->renderChildren()
initiatedthe whole recursion and is still running (once for each recursion level).
To solve this dilemma, this patch implements a rendering context stack similarly
to the one used in the
$view
object, but on the ViewHelper level. Each ViewHelperkeeps track of recursive rendering contexts for itself – again, only for
uncached, non-static ViewHelpers.
In addition to that, the patch simplifies the logic of
buildRenderChildrenClosure()
,which covers both cached/uncached and the contentArgumentName feature.