Zend Framework 1.X conflated the View with the Renderer – in other words, one was not done without the other.
This leads to anti-patterns like:
- ContextSwitch plugin, which simply changes the file extension of the view script to use. Which in turn leads to:
- View-related request-parsing within the controller.
- Short-circuiting via "exit" and "die" when doing automated JSON casting.
- Pulling variables out of the view object in order to cast to JSON.
- Disconnect between file extension and rendering solution.
- Passing the response object into view scripts in order to change/add view-related headers (content-type, client-side cache settings, etc.).
- The "ViewRenderer", which maps the module/controller/action to a given view script.
- Basically, view-related logic, again, within the controller.
- Relies on the controller directly acting on the View object in order to populate it.
- Leads to questions about how the URL relates to the module/controller/action and/or the selected view script.
Additionally, we've discovered other issues:
- Developers often want to use alternate renderers.
- But sometimes still want to use the available helpers.
- Specifying alternate layouts has proven to be difficult.
- Disabling/Enabling layouts has often proven to be difficult as well.
The lessons learned have been:
- A proper View solution should likely be aware of both the Request and Response objects. This would allow selecting an appropriate rendering strategy based on the request, as well as manipulating the Response directly.
- Controllers should not interact with the View directly, but instead return information the View can consume. (This is sometimes termed a "View Model".)
- We should likely provide a pluggable rendering strategy, and potentially multiple rendering solutions, in order to give developers more and easier flexibility.
- Rendering should not rely on file extensions; these should be determined by the renderer.
- Developers should provide an explicit map between requested views and the resource to consume (be it a script, an in-memory template, etc.)
- Layouts should be renderer-specific.
I propose a proper View layer above the renderer.
- MUST accept the Request and Response objects
- COULD accept a "view model" argument
- COULD accept the current MvcEvent (and pull the View Model from there)
- MUST allow multiple rendering strategies
- MUST allow developers to provide logic indicating how strategies are chosen
- MUST provide a baseline rendering strategy (e.g., PHP renderer)
- COULD provide multiple rendering strategies (e.g., JSON, Atom, RSS)
- Existing rendering strategies (PHP renderer) MUST provide configurable mapping mechanisms from requested view to resource (e.g., "blog/entry" -> "blog/entry.phtml"; "blog/entry" -> "blog/entry.mustache"; "blog/entry" -> "table blog, row identified by entry")
- MUST be capable of querying the Request object to determine Renderer
- COULD allow providing maps between Content-Types and Renderers
- SHOULD allow providing programmatic strategies for renderer selection
- MUST allow writing rendered content to the Response object
- SHOULD allow providing programmatic strategies for updating the Response
- MUST allow manipulation of Response headers
- IF layout functionality is provided:
- MUST allow returning partial HTML responses (e.g. for use as XHR response payloads)
- MUST provide exactly one methodology for mapping rendered content to layout "buckets".
- Re-usable modules SHOULD use the most generic solution possible. As such, the recommendations should be to use explicit view model returns, and non-format-specific view models when doing so.
- Rationale: modules should not assume anything about the selected rendering environment and/or needs of the application in which they operate.
Controllers will simply return a value. Currently, if a Response object is returned, this indicates that processing is done, and the MVC will short-circuit all events and return it.
Under this RFC, controllers could optionally return View Model objects. These would contain variables to provide to the renderer, as well as options/configuration for the renderer to hint how we expect it to interact with the model. As an example, these options might indicate the template to use, whether or not to use a layout, or other such information.
We could also register an event listener on the controller that would look for associative array responses. This listener would then pass that array to a new View Model object as the variables, and attempt to map the current request/route match to a template. This would provide RAD benefits, provide BC with pre-beta3 code, etc.
This demonstrates setting both view variables, as well as the template to use when renderering. Note that the template name does not include any suffixes; the idea here is to hand over template resolution to the renderer, which will determine what, if any, suffix to use.
In this case, the return value will be converted to a ViewModel by a listener on the dispatch event. The associative array will be passed as variables; the assumption will be that there are no options to pass to the renderer.
For this to work, the renderer will need to compose an inflector that would introspect the RouteMatch to generate the template name to invoke. By default, we will ship one such inflector, which will not be configurable, and which follows very strict rules of inflection. (In our use case above, we'd inflect to "foo/bar/baz" – namespace, controller, action.)
This demonstrates passing multiple options to the result object. Like the previous example, we have a template value, but we also add a flag for the renderer, "use_layout", and provide a boolean false indicating we're disabling layouts for this particular action.
Notice in this example that the only difference is using the "JsonModel" type.
What would "template" and "use_layout" do in this situation?
- "template" could be a mapped handler that takes the values passed and builds an appropriate data structure to pass to the JSON serializer. This is somewhat analogous to using ContextSwitch in ZF1 to use a different "view script" to return JSON.
- "use_layout" might be hinting that the response should be part of a JSONP payload, and the "layout" would wrap the results in a callback. Alternately, it might simply be ignored by the renderer.
- If multiple actions are invoked, how would "results" be handled? Merged or aggregated?
We have the possibility of executing multiple controllers/actions within a single request – using the forward() controller plugin or by simply invoking an additional controller. The question now is:
- How do we aggregate the results of each of these?
- How would the View layer handle an aggregate result? Should we allow multipart return types? (the HTTP spec allows this) If so, should we provide functionality for this in the framework, or expect the end-user to create custom functionality to acommodate these situations?
Most likely, if multiple content-types are detected, or multiple results detected, if strategies are not present for these situations, the View should raise an exception.
I'm purposely postponing discussion of these situations at this time, as I think we need to explore the use cases in more detail.
- Renderers would require a resolver
- Resolver would map the requested "script" to a resource the renderer can consume/use
- E.g. "foo/bar/baz" => "module/Foo/views/bar/baz.phtml"
- E.g. "foo/bar/baz" => "views/bar/baz.phtml"
- E.g. "foo/bar/baz" => "SELECT content FROM template WHERE id = ?"
- E.g. "foo/bar/baz" => pull mustache tokens from memcached where id = ?
- Renderers would have options
- E.g., "use_layout", "template_stack", etc.
- Rendering SHOULD require both script/resource AND variables/container
- E.g. "render('foo/bar/baz', $assocArrayOfVars)"
- E.g. "render('foo/bar/baz', $arrayObject)"
- Layouts would be renderer specific
- E.g. Mustache might pass a view result as a view model to a layout
- E.g. PhpRenderer might pass results of renderering to a chosen layout template
- Partial HTML responses should always be possible (e.g., for use as an XHR response payload).
- Any provided layout solution(s) should use standard bucket names for predictability. The most obvious approach is to use HTML5 names: head, header, footer, nav, article.
There are multiple approaches to layouts. In ZF1, we have a two-pass system – application view is rendered first, and then passed as content to a second rendering of the layout. This plays towards returning partial HTML responses, but requires a way of capturing the results of rendering to named buckets.
Several developers have suggested using template inheritance (ala Smarty, Twig, Mustache, etc.). This plays well with predictability and performance, but does not play well with partial HTML responses (though there are workarounds).
For the immediate goals of this RFC, I propose:
- Adding template "stacking" capabilities to the PhpRenderr via an "extends" keyword:
- Adding a "layout" view helper for manipulating layout options, and a listener on the view layer for rendering the layout, passing in previously rendered content.
- Composes the Request and Response objects
- Composes one or more Renderers
- One must be marked as the "default" to use
- Composes one or more Strategies for selecting Renderers
- E.g., "JsonViewModel => invoke JsonRenderer"
- E.g., "If Request Accept header is application/json => invoke JsonRenderer"
- Most likely, this would traverse the various Accept values on priority, choosing the first renderer that satisfies a given Accept value.
- E.g., "401 status code => invoke HtmlRenderer"
- Composes one or more Strategies for updating the response post-rendering
- E.g., "when HtmlRenderer is selected, update Response to use Content-Type application/xhtml+xml"
- E.g., "when JsonRenderer is selected, update Response to use Content-Type application/json"
- Potentially loops through result objects, and renders each
- Would we assume multipart return type at this time?
- If a single content-type should be returned, should we raise an error if one or more renderers indicate different Content-Types?
In the use cases below, I use the methods "addRenderingStrategy()" and "addResponseStrategy()". These would basically proxy to a composed EventManager instance, selecting the appropriate event and using a default priority if none provided. You could certainly simply attach directly to the EventManager instance, however. This use case would be encouraged; the various strategies for an application should be aggregated in ListenerAggregate instances in order to keep related functionality in one place, as well as to assist with DI and configuration.
In the case of rendering strategies, the first "strategy" to return a Renderer would short-circuit further strategy execution.
The following would select the JsonRenderer if a JsonViewModel is set as the view model.
The following would loop through the Accept header values and attempt to match to a renderer.
Next, we'll set a response strategy. In the first case, we'll update the Content-Type to "application/json" if the JsonRenderer is active.
We may want to hint certain headers and/or status codes from our view scripts. In such cases, we'd introspect the renderer and update the response.