For each request in Symfony2, the goal of the developer is always the same: to construct and return a Response
object that represents the resource being requested. This is most obvious inside a controller, which almost always returns a Response
object:
public function indexAction($name)
{
// create a Response object directly
return new Response('<html><body>Hello '.$name.'!</body></html');
// create a Response object using the content from a template
return new Response($this->renderView(
'MyBundle:Mycontroller:index.twig',
array('name' => $name)
));
}
Though optional, Symfony also provides a thin "view" layer: a centralized object that governs the preparation of the Response
object by rendering templates and performing other actions. Specifically, the view allows the same logic to be used to create a Response
whose content is HTML, JSON, XML or any other format.
public function indexAction($name)
{
return $this->handle(array('name' => $name), 'MyBundle:MyController:index.twig');
}
At the surface, the handle()
method simply renders the MyBundle:MyController:index.twig
template and passes the $name
variable to it. In reality, however, the process is much more powerful and is handled by the Symfony\\Bundle\\FrameworkBundle\\View\\DefaultView
object.
The DefaultView
object behaves differently based on the request format. By default, three request formats are supported
html
: The given template is rendered and its content is used to create and return a theResponse
object;json
: The parameters are transformed into a json-encoded string and used to create and return theResponse
object. See Transforming Parameters to JSON;xml
: The parameters are transformed into an XML document and used to create and return theResponse
object. See Transforming Parameters and XML.
Support for any number of other formats can be added (see Custom Format Handler).
In our example, the three formats would be handled in the following ways:
html
TheMyBundle:MyController:index.twig
is rendered normally;json
: Thearray('name' => $name))
is json-encoded and the resulting string is used to populate theResponse
object;xml
: Thearray('name' => $name))
is transformed into a simple XML document and used to populate theResponse
object.
This allows the same controller to return any number of different formats, without needing to modify the controller code. As a developer, it also gives you the power to choose how to process and handle specific formats on an application-wide (Using a Custom View) or controller-specific (Custom Format Handler) basis.
The centralized view object also allows for any number of parameters to be passed into every template. These global parameters can be set in the view configuration and then accessed inside any template that uses the view layer. See Configuration for more information.
// Notes on the configuration options (e.g. global parameters) when there // are such options
By default, DefaultView
handles three different formats: html
, json
, and xml
. To override the default behavior for these formats, or to add support for new formats, custom format handlers can be registered with the view service. A custom handler is a PHP callback that will be invoked whenever the view attempts to handle a specific format:
public function indexAction($name)
{
$this->get('view')->registerHandler('json', array($this, 'handleJson'));
return $this->handle(array('name' => $name), 'MyBundle:MyController:index.twig');
}
When the request format is json
, the method handleJson
will be called on the controller object. Suppose that we'd like to render the MyBundle:MyController:index.twig
template and use it to build a JSON array:
public function handleJson(DefaultView $view, Request $request, Response $response)
{
$content = $this->renderView($view->getTemplate(), $view->getParameters());
$json = json_encode(array('content' => $content, 'timestamp' => time()));
$response->setContent($json);
return $response;
}
The job of a custom handler is to prepare and return the Response
object by creating and setting content in the appropriate format. Here, we populate the Response
with a json-encoded string with the content from the template and a timestamp that might be used by client-side Javascript.
Tip
In order to type-hint the DefaultView
, Request
and Response
objects in the handleJson
method, the following would need to be registered at the top of the controller class:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\View\DefaultView;
In addition to creating content, the view is also responsible for processing redirects. Consider the following example:
public function updateAction($slug)
{
// .. perform some update logic
return $this->redirectToRoute('article_show', array('slug' => $slug));
}
In all formats, the default behavior is to create and return a Response
object with a Location
header and a 301 or 302 status code. This triggers the default redirect behavior and directs the client's browser to redirect to the given page.
This behavior can be controlled on a format-by-format basis. Let's revisit the custom handler handleJson
from earlier and add some redirect logic to it:
public function handleJson(DefaultView $view, Request $request, Response $response) { if ($redirect = $view->getRedirect()) { $response->setRedirect($redirect['location'], $redirect['status_code']); return $response; }
// ... the remainder of the handling
}
If the original action method sets a redirect via redirectToRoute
or redirectToUri
, the information is stored in the $view
variable. In the above code, we've implemented the default redirect behavior: the redirect is set on the Response
object and returned.
Let's change the behavior and return a JSON-encoded array instead of redirecting. This may be more advantageous if the response is being returned to client-side Javascript code:
public function handleJson(DefaultView $view, Request $request, Response $response) { if ($redirect = $view->getRedirect()) { $json = json_encode(array('redirect' => $redirect['location'])); $response->setContent($json);
return $response;
}
// ... the remainder of the handling
}
In this case, if the request format is JSON, a JSON-encoded array will be returned with a status code of 200. Your client-side Javascript can handle the redirect however you choose.
Overall, I think this is a nice layer, but I'm afraid that it does too much out-of-the-box. See my notes below.
- This seems to render the
core.view
event either useless or "second-class". That irks me for some reason. - Seems strange that I say I want to render a template, but then because my request format is json, my variables are just json'ed up and the template is never called (and no error). I don't like this.
- I don't like that I can now, by default, only use HTML, XML or JSON. If I try something else, it's going to throw an Exception.
- The reset() seems strange. Why do this? - what's wrong with me using my view layer multiple times (apart from probably not really making a ton of sense). It seems like if you want your object to behave like this, everything being reset should just be passed to the handle() method so that the whole process doesn't mutate the view.
- In the controller, why make the handle() method have a different order of arguments than the renderView() method?
- This really doesn't work well (at least for the purposes of documentation) if we keep automatically set the format in the template.format.twig. It just seems like we've got two totally different solutions for multiple formats. So, if this is merged, that's gotta go imho.
- Unfortunate that I need several lines of code to show how you'd render a template through the view layer. That's one of the advantageous of the templating layer - I can show you how to use it directly in just one line (i.e. it documents really well).
- Why would I pass a non-array as a parameter (e.g. a DOMDocument, or object)? Doesn't this make it impossible for other formats to work? For example, if I pass an object as the parameter, I can use the view for the JSON format, but not for XML or HTML (both transform functions can't handle an object). I'm having trouble seeing the use-case for this.
- It seems inconsistent (though I empirically understand why) that the
globalParameters
are used only for the HTML format. - What about the DIC extension and tests?
- I do really like the custom handlers.