hoffmann/silex-view

Generic Views for Silex

dev-master 2014-01-05 12:08 UTC

This package is not auto-updated.

Last update: 2024-03-16 11:15:42 UTC


README

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 a route. The following is a simple example.

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

Silex does the injection of the $app and $request variables base on type hinting. When the route matches the closure is called, and the $app and $request is bound to your silex application and the currend 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 application. But in my opinion putting your controller logic in a closure leads to tighly coupled code which 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 conrtrollers are separated from your routing code and can be reused.

There are two things I don't like:

  • You pass your controllter 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 it annoyes me that phpstrom - my ide of choice - does not recognice it, so you can't click on it or use the goto 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 as simple BaseView class which you can inherint 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 which returns a closure that 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 dispaches the request based on the htttp 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 GET request which 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 which are 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)
    {
    }
}