Pragmatism in the real world

Accessing services in Slim 3

One of the changes between Slim Framework 2 and 3 is that the application singleton has gone.

In Slim 2, you could do this:

$app = \Slim\Slim::getInstance();
// do something with $app

In general, you didn’t need access to $app itself, but rather you wanted access to something that the app knows about, such as a database adapter, or the router for access to the urlFor method to create a URL to a route.

With Slim 3, there is no getInstance() on App, so you need to inject the instances of whatever you need where ever you need them.

Setup the container

Let’s start by setting up the container with a fictional mapper:

// create container and configure it
$settings = require 'settings.php';
$container = new \Slim\Container($settings);

$container['pdo'] = function ($container) {
    $cfg = $container->get('settings')['db'];
    return new \PDO($cfg['dsn'], $cfg['user'], $cfg['password']);
};

$container['books'] = function ($container) {
    return new BookMapper($container->get('pdo'));
};

// create app instance
$app = new \Slim\App($container);

This is standard Slim 3 initialisation code, to which we have added a DI entry called books which depends on a PDO instance, so we have also registered that to allow the container to create the BookMapper when we ask for it.

How would we get at books within our application?

Route callables

Let’s start by looking at the two common ways to write a route callable.

A route callable closure:

If you use a closure, then Slim 3 binds the container instance to $this.

$app->get('/', function($request, $response, $args) {
    $books = $this->getContainer()->get('books');
    // do something with $books and then return a response
};

There’s also a shortcut you can use as Container implements __get which proxies to the container object’s get method:

$app->get('/', function($request, $response, $args) {
    $books = $this->books; // retrieve from the container
    // do something with $books and then return a response
};

A route callable class:

If you use a class method for a route callable like this:

$app->get('/', 'HomeAction:dispatch');

Slim will look for a DI key of HomeAction, use the DI container to instantiate the class and then dispatch by calling the dispatch() method.

Hence, you should use the DI container to inject what you need into the class constructor:

class HomeAction {
    public function __construct($books)
    {
        $this->books = $books;
    }

    public function dispatch($request, $response, $args)
    {
        $books = $this->books;
        // do something with $books and then return a response
    }
}

We now need to register the class with the container:

// in index.php:
$container['HomeAction'] = function ($container) {
    return new HomeAction($container->get('books'));
};

The registered route will now work and have access to the BookMapper.

All of Slim’s services are in the container, so where I’ve used books in this example, you can access anything that Slim has registered, such as settings or router and in addition to what you have registered yourself.

Middleware

The same thing pattern works with both app and route middleware.

Middleware closure

If you use a closure in middleware, the Container is again bound to $this:

$app->add(function ($request, $response, $next) {
    $settings = $this->get('settings');
    // do something with $settings
    return $next($request, $response);
});

Middleware class

You can also use a class which works exactly as it does for a route:

$app->add('App\Middleware\AppMiddleware:run');

Slim will look for a DI key of App\Middleware\AppMiddleware, use the DI container to instantiate it and then call the run() method.

As before, inject your dependencies via the class constructor:

namespace App\Middleware;

class AppMiddleware
{
    public function __construct($settings)
    {
        $this->settings = $settings;
    }

    public function run($request, $response, $next)
    {
        $settings = $this->settings;
        // do something with $settings
        return $next($request, $response, $next);
    }
}

and register with the container:

$container['App\Middleware\AppMiddleware'] = function ($container) {
    return new App\Middleware\AppMiddleware($container->get('settings'));
};

I’ve found this pattern to be nicely consistent and easy to remember.

To sum up

Slim 3 encourages you to think a little more carefully about which dependencies you need for any given middleware or action. Due to closure binding and the built-in DI container, it’s easy to access the classes you need, when you need them.

(Note, that in development releases of Slim, the Application was bound the the route callable. This was changed by the final release of Slim 3.0 and this post has been updated to reflect this change.)

14 thoughts on “Accessing services in Slim 3

  1. Rob,

    Thank you for very useful information.

    I believe the following is not accurate in Slim 3.0 release

    If you use a closure, then Slim binds the application instance to $this.
    $app->get('/', function($request, $response, $args) {
    $books = $this->getContainer()->get('books');
    // do something with $books and then return a response
    };

    It seems that $this is an instance of Container and has no method getContainer()
    it should be

    If you use a closure, then Slim binds the container to $this.
    $app->get('/', function($request, $response, $args) {
    $books = $this->get('books');
    // do something with $books and then return a response
    };

  2. Rob,

    I find Slim and Slim 3 in particular worth a while and I hope you don't mind me giving your hard time.

    In Slim 3.0 release, after this routing
    $app->get('/', 'HomeAction:dispatch');

    an instance of HomeAction is automatically created by Slim with the container as an argument in the constructor. So, this should work:

    class HomeAction {
    public function __construct( $container)
    {
    $this->books = $container->get('whatever');
    $this->template = $container->('template');
    /// etc …
    }
    }

    It is more powerful than your example suggests as one has access to everything attached to the container inside the HomeAction class. Also, the current example will not work.

    ->-> will not work as well.

    Best wishes,
    Janusz

    1. Janusz, what you're suggesting here goes straight against the concept of dependency *injection*. I have to admit I was tempted to use the same sort of solutions, but I was 'converted' by the comments below this article:

      http://juliangut.com/blog/slim-controller#comment-2561295705

      It takes some effort to get the DI pattern in your head. Basically, you don't just pass the container around. You only inject the services that are actually neccessary in a particular place (route, controller, whatever).

  3. What happens in the case there are multiple custom classes that needs to be accessed in one of the custom class ?

    // create container and configure it
    $settings = require 'settings.php';
    $container = new \Slim\Container($settings);
    
    $container['pdo'] = function ($container) {
        $cfg = $container->get('settings')['db'];
        return new \PDO($cfg['dsn'], $cfg['user'], $cfg['password']);
    };
    
    $container['books1'] = function ($container) {
        return new Book1Mapper($container->get('pdo'));
    };
    
    
    $container['books2'] = function ($container) {
        return new Book2Mapper($container->get('pdo'));
    };
    
    
    $container['books3'] = function ($container) {
        return new Book3Mapper($container->get('pdo'));
    };
    
    // create app instance
    $app = new \Slim\App($container);
    

    What if i want to be able to access Books3Mapper instance in Book2Mapper and Book1Mapper ? How do i achieve it ?

    1. Gopal:

      $container['books4'] = function ($container) {
          $books2Mapper = $container->get('books2');
          // do something with $books2Mapper and return
          // aninstance of something
      };
      
  4. What should I do if I have variable constructor parameters and service dependencies? I know that I can create protected closures and execute them with my own parameters but protected closures don't have access to the container. Should I just pass my dependencies manually?

    http://silex.sensiolabs.org/doc/master/services.html#protected-closures

    My current method (not using slim DI) –

    $app->group('/users', function() {
    $this->post(", 'Src\Actions\Users\CreateUser')
    ->add(new Src\Middleware\AuthMiddleware('anonymous'))
    ->add(new Src\Middleware\ValidationMiddleware(['name', 'email', 'password']));

    AuthMiddleware takes the 'anonymous' parameter and requires service x,y, and z.
    ValidationMiddleware take an array of parameters and requires service x and y.

    In both cases inside of the __invoke function I'll do something linke $x = new X. Again, not the slim DI way but I'm wondering if there is a best practice to accomplish what I'm trying to do.

    Thank you for your awesome blogging about Slim. It's all been very helpful!

  5. Not sure if this is the best solution but after digging through github issues and discourse I found a way.

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

    $this->post(", 'Src\Actions\Users\CreateUser')
    ->setArguments(['state' => 'anonymous', 'validators' => ['name', 'email', 'password']])
    ->add(new Src\Middleware\AuthMiddleware())
    ->add(new Src\Middleware\ValidationMiddleware());

    The setArgument(s) methods in \Slim\Route did the trick. Now I can access/set those parameters in middleware.

    $route = $request->getAttribute('route');
    $validators = $route->getArgument('validators');

    Just a note: I couldn't get var_dump($request->getAttribute('routeInfo')[2]); to return anything. Don't know if this was axed at some point or something on my end.

    https://github.com/slimphp/Slim/issues/1505

    https://github.com/slimphp/Slim/pull/1343

    http://discourse.slimframework.com/t/environment-in-slim-3-accessing-the-slim-instance/427/2

  6. There's one thing developers should really be careful about. The documentation of Slim states that you can easily use another dependency injection container like php-di as long as it implements the Container-Interop interface.

    Well, I actually rewrote my service configuration file to php-di's format, created a php-di container based on this, injected the container into Slim's constructor… and bang, 'router' service not found. What? After digging a bit, I realize that there are many services injected by Slim *only* when it's using its default Pimple based container.

    So I wish the documentation would mention this and I really wonder what's the point of allowing different containers if we need to register default services ourselves? I know there is an external "bridge" project that fixes this problem but still…

    I also wonder why Container-Interop doesn't define a standard way to *set* services.

  7. Copy the version 2 File and paste it in same place. Then use the getInstance method. It works.It worked for me

  8. I have a route callable class, which follows your example with the same routing, a callable class with a constructor, and registering the class and passing $container->get('settings') to the new instance.
    My constructor never receives an array of settings, or a settings object, but the entire slim\container instead ?

    1. There is a mismatch between the string that you have used to register the route callable class in the container and the one in the $app->get() call.

Comments are closed.