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.

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.