Peter Hoffmann

Class-Based Views with Silex

Silex-View is an implementation of class based views similar to django class based views and flask pluggable views for the php microframework silex.

This post is from 2013. Some of the content may be outdated.

Silex-View is an implementation of class-based views similar to Django class-based views and Flask pluggable views for the PHP microframework silex.

In Silex, you attach closures to routes. The following is a simple example.

$app->get('/blog/show/{id}', function (Application $app, Request $request, $id) {
    ...
});

Silex injects the $app and $request variables based on type hints. When the route matches, the closure is called, and $app and $request are bound to your Silex application and the current request. Routing variables like the $id parameter can be added to the function definition as well.

This is a nice and quick way to build small applications. However, in my opinion, putting your controller logic in a closure leads to tightly coupled code that is difficult to test.

The silex documentation shows how to put your controllers in classes:

$app->get('/', 'Igorw\Foo::bar');

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;

namespace Igorw
{
    class Foo
    {
        public function bar(Request $request, Application $app)
        {
            ...
        }
    }
}

This approach is much better. Now you can test your controller class with mocked $request and $application objects. As a bonus, your routing definitions are small and clean, and your controllers are separated from your routing code and can be reused.

There are two things I don't like:

Silex-View has a simple BaseView class that you can inherit from:

use SilexView\BaseView;

class MyView extends BaseView
{
    private $greeting;

    function __construct($greeting){
        $this->greeting = $greeting;
    }

    function get($request, $app){
        return $this->greeting.' '.$request->get('name');
    }
}

and use it in your routing definition:

$app->get('/hello/{name}', MyView::asView('hello'));

BaseView::asView() is a static method that returns a closure which will be called when the route matches:

class BaseView
{
    public static function asView()
    {
        $classname = get_called_class();
        $args = func_get_args();
        return function(\Symfony\Component\HttpFoundation\Request $request,
                        \Silex\Application $app) use ($classname, $args){
            $cls = new \ReflectionClass($classname);
            $instance = $cls->newInstanceArgs($args);
            return $instance->dispatch($request, $app);
        };
...

All arguments passed to the asView function will be forwarded to the constructor of your inherited controller class. Inspired by the Django class-based views, the BaseView class dispatches the request based on the HTTP method of the request. So a GET request will be passed to the get(..) method, and a POST request to the post(...) method of your controller class. With this convention, it is very easy and clean to build REST controllers.

class BaseView
{
    ...
    protected $http_method_names = array('get', 'post', 'put', 'delete', 'head', 'options', 'trace');
    public function dispatch($request, $app)
    {
        $method = strtolower($request->getMethod());
        // If no HEAD method is defined, use GET
        if ("head" === $method && ! method_exists($this, "head"))
            $method = "get";
        if (! (in_array($method, $this->http_method_names) &&
               method_exists($this, $method)))
            return $this->httpMethodNotAllowed($method);
        return $this->$method($request, $app);
    }

The TemplateView class is a shortcut for a GET request controller that should be rendered by a template. All you have to do is create a subclass and implement the getContextData() function, which should return an array of arguments needed in your Twig template.

class MyTemplate extends TemplateView
{
    function getContextData($request, $app)
    {
        return array('name' => "Joe");
    }
}

The implementation is as follows:

class TemplateView extends BaseView
{

    /*
     * Get the template name for the view.
     * Default implementation is to use the class name without namespace.
     */
    function getTemplateName(){
        $cls = explode('\\', get_class($this));
        return end($cls).'.twig';

    }

    function get($request, $app)
    {
        return $app["twig"]->render($this->getTemplateName(),
                                    $this->getContextData($request, $app));
    }

    function getContextData($request, $app)
    {
    }
}

The Django and Flask versions of class-based views are much more mature, so there is a lot of room for improvement. I welcome your comments and thoughts.