Silex Route Helpers for a Cleaner Architecture

27 Nov 2012

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.

This example is a little contrived, as the controller we end up with actually does nothing, so much so, that I've not even printed the method here. A better example would have the controller performing some sort of application specific logic, such as emailing the author, or updating some statistics, or whatever else you can think of.
<?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...

php silex symfony clean featured

Maintainable PHP Apps with Silex & BDD

I'm currently writing a book, Maintainable PHP Apps with Silex & BDD, leave me your email address and I'll keep you up to date on my progress.

Twitter Icon If you liked this post, you should follow me on twitter here
blog comments powered by Disqus

About

Photo of Dave Marshall

Dave Marshall has been building web applications with various technologies since around 2004. Dave is a TDD enthusiast, blogs quite regularly at davedevelopment.co.uk and has recently increased his efforts to give back further, by contributing to OS projects such as Silex and Mockery

Read more about Dave

Maintainable PHP Apps with Silex & BDD

I'm currently writing a book, Maintainable PHP Apps with Silex & BDD, leave me your email address and I'll keep you up to date on my progress.

Follow Dave: