In a previous post, we discussed creating your Silex controllers as Services, in which we created a nice, clean, easily tested controller, with few dependencies and collaborators. But I cheated a little, by returning a JSON response. Supposing we want to render some HTML, do we want to inject the template engine in to the controller? Should the controller be responsible for knowing how to render the template? I'm not sure, but if I can have it not do it with minimal fuss, I think I'd rather it not. The full stack framework has the @Template annotation, which allows developers to assign a template to a controller and then simply return an array. If they can do it in the full stack framework, we can do it in Silex.
<?php
/**
* What we're aiming for
*/
$app->get("/posts/{post}", "posts.controller:viewAction")
->convert("post", $app['findOr404Factory']("Demo\Entity\Post"))
->template("post/edit.html.twig");
FindOr404Factory
This is really simple, if you're using doctrine, it simply creates closures for us that attempt to find an instance of the class we provide with the Id of the first argument, pretty much like the @ParamConverter annotation, without as much flexibility.
<?php
$app['findOr404Factory'] = $app->protect(function($type, $message = null) use ($app) {
if (null === $message) {
$lastNamespace = strrpos($type, "\\");
if ($lastNamespace !== false) {
$message = substr($type, strrpos($type, "\\") + 1) . " Not Found";
} else {
$message = "$type Not Found";
}
}
return function($id) use ($app, $type, $message) {
$obj = $app['em']->getRepository($type)->find($id);
if (null === $obj) {
return $app->abort(404, $message);
}
return $obj;
};
});
Custom Route Class
In order to have our nice template helper, we need to tell Silex to use a custom Route Class. This is pretty easy, but needs to be done before declaring any routes.
<?php
class CustomRoute extends Route
{
public function template($path)
{
$this->setOption('_template', $path);
return $this;
}
}
/** ... */
$app['route_class'] = 'CustomRoute';
TemplateRenderingListener
Out of the box, Silex comes with a listener that will attempt to convert
anything you return from a controller (except for a Response
object) to a
string, and create a new Response with it. We need one of these, that will take
an array and render a template if the route has one.
<?php
class TemplateRenderingListener implements EventSubscriberInterface
{
protected $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$response = $event->getControllerResult();
if (!is_array($response)) {
return;
}
$request = $event->getRequest();
$routeName = $request->attributes->get('_route');
if (!$route = $this->app['routes']->get($routeName)) {
return;
}
if (!$template = $route->getOption('_template')) {
return;
}
$output = $this->app['twig']->render($template, $response);
$event->setResponse(new Response($output));
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::VIEW => array('onKernelView', -10),
);
}
}
/** ... */
$app['dispatcher']->addSubscriber(new TemplateRenderingListener($app));
Job's a good'un. The code can be seen together in this gist, I haven't got a full working example I can publish at this time, but I can assure you it works! Hopefully you can see from this post how flexible Silex is, but also how much of a great reference the full stack framework is for whenever you want to do something new with your Silex boilerplate.
What we've ended up with is a PostController::viewAction
method that doesn't
care where the Post
comes from and doesn't care how it's presented. All it
care's about is the fact that the Post is being viewed, and what behaviour it
should apply to the application domain, based on that fact. This soon goes to
pot when you want to start redirecting to generated urls, persisting to the
database etc but hey, one step at a time. Even then, it wouldn't take much to add a
route helper that listens for a true boolean response and redirects to a given
url. Same goes for a helper that would automatically flush Doctrine's Unit Of
Work to the database, I'd be a little concious of doing that, but you
get the idea.
One thing I would like to explore is applying security to the routes and their parameters, much like you can with the JMSSecurityExtraBundle, but that's for another day. I think the convert methods are applied literally last thing before executing the controller, so that might prove tricky.
I also should write about why I use Silex instead of the full stack framework...