Slim 3 routing best practices and organization

Hi,

i’m just a beginner with Slim 3 and i want to implement a REST API.

I have some questions about routes because i’m planning a lot of routes and i’m a little bit lost with this.

I have:

$app->group('/v1', function () {

    $this->group('/auth', function() {
        $this->map(['GET','POST'], '/login/', 'App\controllers\AuthController:login');
        $this->map(['GET','POST'], '/logout/', 'App\controllers\AuthController:logout');
    });

    $this->group('/data', function() {
        $this->map(['GET'], '/login/', 'App\controllers\DataController:getData');
        $this->map(['POST'], '/login/', 'App\controllers\DataController:setData');

        $this->map(['GET'], '/login/', 'App\controllers\DataController:getSubData');
    });

});

For me is very annoying because I have more than 30 routes and create in same place and one by one calling method is very disorganized.

In other frameworks like Silex I can create routes in Controller Constructors:

$app->group('/v1', function () {

    $this->group('/auth', 'App\controllers\AuthController');

});

class AuthController {

    public function __construct( $app ) {

        $app->map(['GET','POST'], '/login/', 'App\controllers\AuthController:login');
        $app->map(['GET','POST'], '/logout/', 'App\controllers\AuthController:logout');
    }
}

$container['App\controllers\AuthController'] = function($c) use ($app) {
    return new App\controllers\AuthController( $app );
}

Something like this will help to organize routes inside the controllers and the groups in a separate file.
I tried this but not work for me. Is something similar posible?

Thanks
Victor

What end result are you trying to get (which full routes linked to which callable)? You have multiple login routes?

The full routes are (some):

(AuthController)
/v1/auth/login/
/v1/auth/logout/
/v1/auth/signup/

(EventController)
/v1/event[/{id}]/
/v1/event/{id}/facility/
/v1/events[/{page}]/
/v1/event[/{id}]/set-extra/
/v1/event[/{id}]/get-extra/

(FacilityController)
/v1/facilities/[/{page}]/
/v1/facility[/{id}]/
/v1/facility/{id}/events/

(TasksController)
/v1/tasks/executeCron
/v1/tasks/alotofhere1
/v1/tasks/alotofhere2
/v1/tasks/alotofhere3

There is some, but the real questios is exist a simple way to organize route creation

Thanks

You can use nested grouping like this to prevent repeating the url:

$app->group('/v1', function () {
    $this->group('/auth', function () {
        $this->map(['GET', 'POST'], '/login', 'App\controllers\AuthController:login');
        $this->map(['GET', 'POST'], '/logout', 'App\controllers\AuthController:logout');
        $this->map(['GET', 'POST'], '/signup', 'App\controllers\AuthController:signup');
    });

    $this->group('/events', function () {
        $this->get('', 'App\controllers\EventController:getEvents');
        $this->post('', 'App\controllers\EventController:createEvent');

        $this->group('/{eventId}', function () {
            $this->get('', 'App\controllers\EventController:getEvent');
            $this->put('', 'App\controllers\EventController:updateEvent');
            $this->delete('', 'App\controllers\EventController:deleteEvent');            
        });
    });
});
1 Like

I want to avoid nested and move to controller constructor, like I said in first post.

Is this posible?

You could do that by passing the Slim application instance into the controller and declare some routes in there. But I wouldn’t recommend that, since this means that the controller has to be initialized even if its not gonna be used.

Or maybe you can put the route registration in a static method of the controller. This will still include the file, but not create an instance.

$app = new App();

new Controller($app);

 // or within a group
$app->group('/v1', function () {
    new Controller($this);
});

class Controller {
    public function __construct($app) {
        // $app->get(...);
    }
}

// or static

Controller::registerRoutes($app);

class Controller {
    public static function registerRoutes($app) {
        // $app->get(...);
    }
}

Hi JoeBengalen,

I appreciate your help but like i said in first post this i tried this and don’t work.

Any other person in the house that know a solution?

Thanks

It does, just tried it myself …
Example below dump the registered routes, which included the one set from the controller


<?php

use Slim\App;
use Slim\Route;

require 'vendor/autoload.php';

$app = new App();

$app->group('/v1', function () {
    new Controller($this);
});

$routes = $app->getContainer()->get('router')->getRoutes();
$mapped = array_map(function (Route $route) {
    return [
        'method' => $route->getMethods(),
        'pattern' => $route->getPattern(),
    ];
}, $routes);

var_dump($mapped);

class Controller {
    public function __construct($app)
    {
        $app->get('/test', 'callable');
    }
}

As @JoeBengalen says, putting route definitions in controller constructors isn’t very efficient as you end up instantiating objects that are then not used. However, if that’s what you want to do, this is how you do it:

$app->group('/v1', function () {
    $this->group('/auth', function() {
        new App\Controllers\AuthController($this);
    });

    $this->group('/data', function() {
        new App\Controllers\DataController($this);
    });

});

with a controller looking like this:

class AuthController
{
    public function __construct($app)
    {
        $app->map(['GET','POST'], '/login/', [$this, 'login']);
        $app->map(['GET','POST'], '/logout/', [$this, 'logout']);
    }

    public function login($request, $response, $args) {
        // do stuff & return $response
    }
    public function logout($request, $response, $args) {
        // do stuff & return $response
    }
}

When you say it doesn’t work, what error are you getting?

1 Like

In my topic “Middleware dynamic loading - code consultation appreciated” (I wonder why nobody has even a word of comment) I have developed more conventional approach for Slim with routing and controllers calling - works fine even for HMVC pattern that I prefere - meaning separate controllers for main sections of a page or modals (I hate templates :slight_smile: )

Thanks akrabat,

I was trying more modern approach and i think that not work using containers (that’s what i’m trying):

$app->group('/v1', function () {

    $this->group('/auth', 'App\controllers\AuthController');

});

$container['App\controllers\AuthController'] = function ($c) use ($app) {
      return new \App\controllers\AuthController($app);
};

This don’t work for me.
Creating routes in controllers ensure to me that i’m not adding more code/routes to memory and this are a little bit more efficient. If you don’t access route, the controller aren’t created in memory and the rest of the routes either.

Thanks

In what way does it not work?

Hi akrabat,

sorry but i’m really busy :tired_face:

My real example:

/*** CONTAINER ****/
$container['App\controllers\CountriesController'] = function ($c) use ($app) {
    // logger will be removed soon because $app have container
    // but helps to explain my problem
    return new \App\controllers\CountriesController($app, $c->get('logger'));
};

/*** ROUTES ****/
$app->group('/v1/{lang}', function () {
     $this->group('/countries', 'App\controllers\CountriesController');
});

/*** CONTROLLERS OR ACTION ****/
namespace App\controllers;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
 
class CountriesController extends BaseController {
     public function __construct( $app, LoggerInterface $logger )
     {
             parent::__construct( $app, $logger );
     
             $app->get('', [$this, 'getCountries']);
             $app->get('/tryme', [$this, 'getCountries']);
     }  

     public function getCountries(Request $request, Response $response, $args) {
         $response->withJson('200', ['success' => true]);
     }

}

//basecontroller
namespace App\controllers;

use Psr\Log\LoggerInterface;

abstract class BaseController
{     
       /**
        * @var $app \Slim\App
        */
       protected $app;

       /**
        * @var $logger \Psr\Log\LoggerInterface
        */
       protected $logger;

       public function __construct( $app, LoggerInterface $logger )
       {
                $this->app              = $app;
                $this->logger           = $logger;
       }
}

And Errors:
2016/04/13 15:39:29 [error] 30307#0: *355 FastCGI sent in stderr: “PHP message: PHP Catchable fatal error: Argument 2 passed to App\controllers\CountriesController::__construct() must be an instance of App\controllers\LoggerInterface, instance of Monolog\Logger given, called in /var/sites/xxxxx/app/dependencies.php on line 82 and defined in /var/sites/xxxxx/app/src/controllers/CountriesController.php on line 15” while reading response header from upstream, client: 192.168.56.1, server: xxxxx, request: “GET /v1/es/countries/tryme HTTP/1.1”, upstream: “fastcgi://unix:/var/run/php5-fpm.sock:”, host: “xxxxx”

2016/04/13 15:39:34 [error] 30307#0: *355 FastCGI sent in stderr: “PHP message: PHP Catchable fatal error: Argument 2 passed to App\controllers\CountriesController::__construct() must be an instance of App\controllers\LoggerInterface, instance of Monolog\Logger given, called in /var/sites/xxxxx/app/dependencies.php on line 82 and defined in /var/sites/xxxxx/app/src/controllers/CountriesController.php on line 15” while reading response header from upstream, client: 192.168.56.1, server: xxxxx, request: “GET /v1/es/countries HTTP/1.1”, upstream: “fastcgi://unix:/var/run/php5-fpm.sock:”, host: “xxxxx”

Maybe the container isn’t well defined?

Thanks a lot

Add use Psr\Log\LoggerInterface; to the CountriesController.

That will solve the error you have. But it will not make your routing work as you intended, because those wont get registered without actually instantiating the CountriesController.

Thanks Joe, I need Holydays. I was just remember that before new routes the error was other, just this one:

2016/04/13 16:12:11 [error] 30307#0: *385 FastCGI sent in stderr: "PHP message: PHP Fatal error:  Uncaught exception 'RuntimeException' with message 'App\controllers\CountriesController is not resolvable' in /var/sites/xxxxx/vendor/slim/slim/Slim/CallableResolver.php:82
Stack trace:
#0 /var/sites/xxxxx/vendor/slim/slim/Slim/CallableResolverAwareTrait.php(45): Slim\CallableResolver->resolve('App\\controllers...')
#1 /var/sites/xxxxx/vendor/slim/slim/Slim/RouteGroup.php(40): Slim\Routable->resolveCallable('App\\controllers...')
#2 /var/sites/xxxxx/vendor/slim/slim/Slim/App.php(271): Slim\RouteGroup->__invoke(Object(Slim\App))
#3 /var/sites/xxxxx/app/routes.php(7): Slim\App->group('/countries', 'App\\controllers...')
#4 /var/sites/xxxxx/vendor/slim/slim/Slim/RouteGroup.php(45): Closure->{closure}()
#5 /var/sites/xxxxx/vendor/slim/slim/Slim/App.php(271): Slim\RouteGroup->__invoke(Object(Slim\App))
#6 /var/sites/xxxxx/app/routes.php(9): Slim\App->group('/v1/{lang}', Object(Closure))
#7 /var/sites/xxxxx/public/index.php(30): requ" while reading response header from upstream, client: 192.168.56.1, server: xxxxx, request: "GET /v1/es/countries HTTP/1.1", upstream: "fastcgi://unix:/var/run/php5-fpm.sock:", host: xxxxx

Thanks,

Thats weird, because in the error you showed before the CoutriesController seemed to be resolved just fine. Did you change anything else?

No Joe, just added:

use Psr\Log\LoggerInterface;

Thanks,

Rename getCountries to __invoke

Added __invoke:

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;

public function __invoke(Request $request, Response $response, $args) { //line 25
    $response->withJson('200', ['success' => true]);
}

2016/04/13 16:31:39 [error] 30307#0: *393 FastCGI sent in stderr: “PHP message: PHP Catchable fatal error: Argument 1 passed to App\controllers\CountriesController::__invoke() must be an instance of Psr\Http\Message\ServerRequestInterface, none given, called in /var/sites/xxx/vendor/slim/slim/Slim/RouteGroup.php on line 45 and defined in /var/sites/xxx/app/src/controllers/CountriesController.php on line 25” while reading response header from upstream, client: 192.168.56.1, server: xxx, request: “GET /v1/es/countries HTTP/1.1”, upstream: “fastcgi://unix:/var/run/php5-fpm.sock:”, host: “xxx”

Thanks,

It’s complicated because the $app variable is different when arrive to Controller constructor, and isn’t returned to normal thread execution and new paths are lost.