Slim Framework: routes and controllers

ffflabs

Originally, my Slim Framework app had the classic structure

(index.php)

<?php
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) {
    echo "Hello, $name";
});
$app->run();

But as I added more routes and groups of routes, I moved to a controller based approach:

index.php

<?php
$app = new \Slim\Slim();
$app->get('/hello/:name', 'HelloController::hello');
$app->run();

HelloController.php

<?php
class HelloController {
    public static function hello($name) {
        echo "Hello, $name";
    }
}

This works, and it had been helpful to organize my app structure, while at the same time lets me build unit tests for each controler method.

However, I'm not sure this is the right way. I feel like I'm mocking Silex's mount method on a sui generis basis, and that can't be good. Using the $app context inside each Controller method requires me to use \Slim\Slim::getInstance(), which seems less efficient than just using $app like a closure can.

So... is there a solution allowing for both efficiency and order, or does efficiency come at the cost of route/closure nightmare?

ffflabs

I guess I can share what I did with you guys. I noticed that every route method in Slim\Slim at some point called the method mapRoute

(I changed the indentation of the official source code for clarity)

Slim.php

 protected function mapRoute($args)
    {
        $pattern = array_shift($args);
        $callable = array_pop($args);

        $route = new \Slim\Route(
              $pattern, 
              $callable, 
              $this->settings['routes.case_sensitive']
        );
        $this->router->map($route);
        if (count($args) > 0) {
            $route->setMiddleware($args);
        }

        return $route;
    }

In turn, the Slim\Route constructor called setCallable

Route.php

public function setCallable($callable)
{
    $matches = [];
    $app = $this->app;
    if (
         is_string($callable) && 
         preg_match(
           '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!', 
           $callable, 
           $matches
         )
       ) {
            $class = $matches[1];
            $method = $matches[2];
            $callable = function () use ($class, $method) {
                static $obj = null;
                if ($obj === null) {
                    $obj = new $class;
                }
                return call_user_func_array([$obj, $method], func_get_args());
            };
        }

        if (!is_callable($callable)) {
            throw new \InvalidArgumentException('Route callable must be callable');
        }

        $this->callable = $callable;
    }

Which is basically

  • If $callable is a string and (mind the single colon) has the format ClassName:method then it's non static, so Slim will instantiate the class and then call the method on it.
  • If it's not callable, then throw an exception (reasonable enough)
  • Otherwise, whatever it is (ClassName::staticMethod, closure, function name) it will be used as-is.

ClassName should be the FQCN, so it's more like \MyProject\Controllers\ClassName.

The point where the controller (or whatever) is instantiated was a good opportunity to inject the App instance. So, for starters, I overrode mapRoute to inject the app instance to it:

\Util\MySlim

 protected function mapRoute($args)
    {
        $pattern = array_shift($args);
        $callable = array_pop($args);

        $route = new \Util\MyRoute(
            $this, // <-- now my routes have a reference to the App
            $pattern, 
            $callable, 
            $this->settings['routes.case_sensitive']
        );
        $this->router->map($route);
        if (count($args) > 0) {
            $route->setMiddleware($args);
        }

        return $route;
    }

So basically \Util\MyRoute is \Slim\Route with an extra parameter in its constructor that I store as $this->app

At this point, getCallable can inject the app into every controller that needs to be instantiated

\Util\MyRoute.php

public function setCallable($callable)
{
    $matches = [];
    $app = $this->app;
    if (
       is_string($callable) && 
       preg_match(
          '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!', 
          $callable, 
          $matches
          )
       ) {
        $class = $matches[1];
        $method = $matches[2];

        $callable = function () use ($app, $class, $method) {
            static $obj = null;
            if ($obj === null) {
                $obj = new $class($app); // <--- now they have the App too!!
            }
            return call_user_func_array([$obj, $method], func_get_args());
        };
    }

    if (!is_callable($callable)) {
        throw new \InvalidArgumentException('Route callable must be callable');
    }

    $this->callable = $callable;
}

So there it is. Using this two classes I can have $app injected into whatever Controller I declare on the route, as long as I use a single colon to separate controller from method. Using paamayim nekudotayim will call the method as static and therefore will throw an error if I try to access $this->app inside it.

I ran tests using blackfire.io and... the performance gain is negligible.

Pros:

  • this saves me the pain of calling $app = \Slim\Slim::getInstance() on every static method call accounting for about 100 lines of text overall.
  • it opens the way for further optimization by making every controller inherit from an abstract controller class, which in turn wraps the app methods into convenience methods.
  • it made me understand Slim's request and response lifecycle a little better.

Cons:

  • performance gains are negligible
  • you have to convert all your routes to use a single colon instead of paamayin, and all your controller methods from static to dynamic.
  • inheritance from Slim base classes might break when they roll out v 3.0.0

Epilogue: (4 years later)

In Slim v3 they removed the static accessor. In turn, the controllers are instantiated with the app's container, if you use the same convention FQCN\ClassName:method. Also, the method receives the request, response and $args from the route. Such DI, much IoC. I like it a lot.

Looking back on my approach for Slim 2, it broke the most basic principle of drop in replacement (Liskov Substitution).

class Route extends \Slim\Route
{
  protected $app;
  public function __construct($app, $pattern, $callable, $caseSensitive = true) {
   ...
   }
}

It should have been

class Route extends \Slim\Route
{
  protected $app;
  public function __construct($pattern, $callable, $caseSensitive = true, $app = null) {
   ...
   }
}

So it wouldn't break the contract and could be used transparently.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

From Dev

Play Framework, REST, Routes, and Controllers

From Dev

Slim framework - cannot interpret routes with dot

From Dev

Pass 'use' objects to Routes with Slim Framework

From Dev

Nginx config for Slim Framework API routes

From Dev

Slim framework - cannot interpret routes with dot

From Dev

Slim Framework -Single class instance for all routes

From Dev

Pass 'use' objects to Routes with Slim Framework

From Dev

Nginx config for Slim Framework API routes

From Dev

Slim Framework PHP - block routes access to users in some conditions

From Dev

PHP - Slim Framework: Best practice with a lot of code inside routes closures

From Dev

How to get Slim PHP Framework routes in a subdirectory on nginx

From Dev

How to get Slim PHP Framework routes in a subdirectory on nginx

From Dev

Slim Framework PHP - block routes access to users in some conditions

From Dev

Slim Framework: How to catch multiple fully OPTIONAL routes?

From Dev

how to load custom classes using slim middleware in slim framework from a folder to routes

From Dev

How to refactor long php file of routes (I'm using Slim Framework)

From Dev

ActiveAdmin: routes for overridden controllers

From Dev

Ember Controllers and Routes

From Dev

Ember controllers in nested routes

From Dev

Sub Domain Routes and Controllers

From Dev

Slim framework configuration outside of Slim

From Dev

Multiple Slim routes with the same signature

From Dev

Namespacing controllers within routes file

From Dev

How to map routes to controllers in Sinatra?

From Dev

Sitecore, custom MVC controllers and routes

From Dev

Connecting routes, views and controllers (Rails)

From Dev

AngularJS: Best practices for routes and controllers

From Dev

Slim Framework redirect with params

From Dev

Slim Framework and Eloquent ORM

Related Related

  1. 1

    Play Framework, REST, Routes, and Controllers

  2. 2

    Slim framework - cannot interpret routes with dot

  3. 3

    Pass 'use' objects to Routes with Slim Framework

  4. 4

    Nginx config for Slim Framework API routes

  5. 5

    Slim framework - cannot interpret routes with dot

  6. 6

    Slim Framework -Single class instance for all routes

  7. 7

    Pass 'use' objects to Routes with Slim Framework

  8. 8

    Nginx config for Slim Framework API routes

  9. 9

    Slim Framework PHP - block routes access to users in some conditions

  10. 10

    PHP - Slim Framework: Best practice with a lot of code inside routes closures

  11. 11

    How to get Slim PHP Framework routes in a subdirectory on nginx

  12. 12

    How to get Slim PHP Framework routes in a subdirectory on nginx

  13. 13

    Slim Framework PHP - block routes access to users in some conditions

  14. 14

    Slim Framework: How to catch multiple fully OPTIONAL routes?

  15. 15

    how to load custom classes using slim middleware in slim framework from a folder to routes

  16. 16

    How to refactor long php file of routes (I'm using Slim Framework)

  17. 17

    ActiveAdmin: routes for overridden controllers

  18. 18

    Ember Controllers and Routes

  19. 19

    Ember controllers in nested routes

  20. 20

    Sub Domain Routes and Controllers

  21. 21

    Slim framework configuration outside of Slim

  22. 22

    Multiple Slim routes with the same signature

  23. 23

    Namespacing controllers within routes file

  24. 24

    How to map routes to controllers in Sinatra?

  25. 25

    Sitecore, custom MVC controllers and routes

  26. 26

    Connecting routes, views and controllers (Rails)

  27. 27

    AngularJS: Best practices for routes and controllers

  28. 28

    Slim Framework redirect with params

  29. 29

    Slim Framework and Eloquent ORM

HotTag

Archive