Skip to content

Instantly share code, notes, and snippets.

@weierophinney
Created January 10, 2012 20:09
Show Gist options
  • Save weierophinney/1590915 to your computer and use it in GitHub Desktop.
Save weierophinney/1590915 to your computer and use it in GitHub Desktop.
ZF2 View RFC

Table of Contents

Views RFC

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.

Proposal

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.

Usage Examples

Controllers

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.

Basic, generic usage

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.

Basic, generic usage, without explicit return

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.)

Basic, generic usage; disabling layout

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.

Specific typing

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.

Unresolved Questions

 * 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.

Changes to Renderers

 * 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

 * 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.

View layer

 * 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?

Setting up Renderers

Setting up rendering and response strategies

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.

Interfaces and Skeletons

View Models

View Resolvers

View Renderers

View Event

View

@beberlei
Copy link

Because the MVC Package has a dependency on the View package. Now i need to use all the view stuff. If i just had a Zend\Mvc\ViewModel interface, then i dont have to care about the Zend\View package. Just having an Interface is not enough for proper decoupling. It has to be in the package that is actually using the interface. Only the implementations should be whereever they wan't.

@weierophinney
Copy link
Author

Not necessarily. If you explicitly create and return ViewModel objects, yes, you depend on the Zend\View package. If you use the event listener that munges your returned associative arrays, then, yes, you depend on the Zend\View package. But if you don't use those things, then there's no dependency.

What are your use cases for using the ViewModel objects without the View layer? I think that's the heart of your argument, and the part I don't understand.

@trq
Copy link

trq commented Jun 9, 2012

Shouldn't line 317 be:

$content .= ob_get_clean();

I don't understand how it could work any other way.

@weierophinney
Copy link
Author

@trq this was an RFC, not a final implementation. (BTW, it's complete in the current ZF2 master.)

@trq
Copy link

trq commented Jun 10, 2012

Even the current implementation in master has this same line:

https://github.com/zendframework/zf2/blob/master/library/Zend/View/Renderer/PhpRenderer.php#L470

@weierophinney
Copy link
Author

weierophinney commented Jun 10, 2012 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment