sábado, 16 de agosto de 2014

Improving the behaviour of SlimFramework with a routing manager

After Symfony, Slim is my favourite framework to develop for the web. It's a lightweight framework that allows me to express myself creating for the web.



But I have to recognize the majestuosity of Symfony: the extensive use of best practices, design patterns, the quantity of bundles and the extension of its community.

Often I need to solve some simple designs and Symfony seems a very big solution. And the fact to host in a shared, normal hosting limits the requirements of the framework I can use.

The fact is that Symfony has influence over me and I am use to see the declarations of routes in controllers as annotations.

The rest of the content of this post wants to illustrate how to emulate this behaviour in SlimFramework.

Lets see how we declare a route in Symfony using annotations:


class DemoController extends Controller
{
    /**
     * @Route("/", name="home_index")
     */
    public function indexAction()
    {
        // here the code that serves this route
    }
}

Now, lets analyse how the front controller in Slim prepare the environment and read the route declarations.


$app = new Slim();

$app->get("/", function(){
  // here the code that servers this route
})->name("home");

$app->run();

In order to emulate the annotations’ behaviour, we need to prepare a base controller and to create classes that declare inside the route's endpoints as a public methods.

The idea is to achieve this point:


class MySlimDemoController extends Controller
{
    /**
     * @Route("/")
     * @Name("home_index")
     */
    public function indexAction()
    {
        // here the code that serves this route
    }
}

To deal with this way we need to process the php file that contains the class before to pass the control to slim (remember: $app->run()).

And convert the annotations into sentences that instructs Slim to respond in each route, as we have seen above.

Then, we need to read the Controller class, process the annotations and generate the sentences.

To speed up the process and do not  read and process each time the contents of the Controller class in each request cycle we need to see how Symfony solves the same problem: caching the results to improve the subsequents requests.

Then, we need to read the modification time of the Controller class and the same for the cached file, if dates are different the routing manager process the annotations again to generate the sentences for Slim and to save this sentences in the cached file, if the dates are identical, we only need to read the cached file in the normal flow of the program to inform Slim the routes we want.

You can get the SlimRoutingManager in github, and I prepared a sample to illustrate its use. You can see the sample in action here or download the sample from github.

The example is really very simple. Do not expect a full application, it is only to illustrate how to integrate SlimRoutingManager.

To integrate this improvement in a project based on SlimFramework or to start one new basically you need to add this line to your composer.json:


{
   "require":{
        // ... more lines
        "jlaso/slim-routing-manager": "*"
   }
}


And in the front controller, before to invoke $app->run() add these lines:

use JLaso\SlimRoutingManager\RoutingCacheManager;

new RoutingCacheManager(
    array(
        'cache'      => __DIR__ . '/cache/routing',
        'controller' => __DIR__ . '/app/controller',
    )
);

//$app->run();

Obviously the configuration of RoutingCacheManager allows to identify the folder that we want to use as a cache and which folder/s we want to process. The 'controller' index allows to identify a single path or an array of paths, in order to have a very compact sentence.

In this example the distinct controllers that attend the routes are placed in app/controller

Screen Shot 2014-08-15 at 09.17.28 am
We can see below the simple controller that serves the home of the page:


use Slim\Slim;
use JLaso\SlimRoutingManager\Controller\Controller;

class FrontendController extends Controller
{
    /**
     * @Route('/')
     * @Name('home.index')
     */
    public function indexAction()
    {
        /** @var Slim $slim */
        $slim = $this->getSlim();

        $slim->response()->body('this is the home of this site');
    }

}

Normally I use Twig to solve the "V" in the MVC pattern, but in order to lightweight the presentation and mainly to uncouple this SlimRoutingManager the response is generated as a raw text in this sample.

Once you visit the home url in your browser you can see the content of the cache/routing folder and if you open the cached version of FrontendController you can see the work that SlimRoutingManager did.


$app = Slim\Slim::getInstance();

$app->map("/", "FrontendController::___indexAction")->via("GET")->name("home.index");

Basically SlimRoutingManager has processed the different methods in FrontendController class that have route annotations (one in this case) and has generated the sentence that Slim understands.

In this way we can code with annotations that are more intuitive and we have almost the same speed because the Slim sentence is cached.

4 comentarios:

  1. well.. like your approach, but how are you going to deal with this:

    $app->get('/data/user-locations/:domain/:start/:end/:type', function ( $domain, $start, $end, $type ) use ($app) {

    });

    whereas the variables originating from the URL need to be passed to Controller?

    :) Any solution for this?

    ResponderEliminar
  2. Hi, thank you for you comment.

    I think that is the same: you need to declare the controller (a public static method with the same signature).


    /**
    * @Route("/data/user-locations/:domain/:start/:end/:type")
    */
    public static function dataUserLocationsAction($domain, $start, $end, $type){ ... }


    In this new project I've used this component, you can see in action in http://pingpongserver.ahiroo.com

    Let me know if this work, and if not I will tray and I'll publish an update.

    ResponderEliminar
  3. Hi! I found the solution :) It was pretty obvious... the map which is generated passes the variables to the controller.. so the whole thing would look Controller wise as follows:

    class GroupsEditController extends Controller
    {

    const currentpage = 'banners';

    protected $slimInstance;

    /**
    * @Route('/groups/edit/:id')
    * @Name('groups.edit.index')
    */
    public function indexAction( $id )


    so in this example ID is passed into the indexAction as a plain variable.

    The other alternative is extending the controller to grab the variable from Slims request object....

    :)

    ResponderEliminar
  4. Very well Andreas, thank you for reading my blog and thank you very much for your comments.

    ResponderEliminar