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.
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:
You pass your controller class as a string to your routing function. Maybe this is the PHP way of doing things, but it does not feel right to me. On top of that, it annoys me that PhpStorm — my IDE of choice — does not recognize it, so you cannot click it or use the Go to Definition shortcut.
You are not able to pass arguments to the constructor of your controller. This is a bigger obstacle for me.
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.
