Giorgio's Blog

Dependency Injection with Slim and PHP-DI

If you're building a web application, chances are it uses a framework. There are many with all sorts of features -- for PHP, Symfony and Laravel immediately spring to mind. Depending on what you're building, these projects offer a large number of features. On the other hand, maybe you don't like (or need) the features they've provided for you. Sure, in most instances you can turn these off and integrate another solution. This can get messy though.

What's the alternative? Smaller frameworks have emerged that have a focus on just the core features you need to get up and running. For PHP, I'm a big fan of the Slim Framework for this purpose, and it is great for rapidly building smaller applications. Today, I'll show you how to combine Slim with another project, PHP-DI, that makes building Slim apps even better.

So now that we know what Slim is, what is PHP-DI? Simply put, PHP-DI is a dependency injection container for PHP with support for several frameworks, including Slim. Slim gives us a container out-of-the-box, but PHP-DI provides us with something Slim's does not: autowiring. Rather than building up our application's services by hand, with autowiring we can have the container do that for us, as needed.

To start, we'll need to pull in a few composer dependencies. Slim and the Slim bridge for PHP-DI. From your project directory, run composer require slim/slim php-di/slim-bridge to grab Slim and PHP-DI. To instantiate Slim with our preferred container, create an index.php file in your project's src directory with the following:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

$app = new \DI\Bridge\Slim\App;
$app->run();
index.php

This isn't very interesting. All we're doing here is creating the application, and immediately running it. No routes have been defined, so if you were to run this in your browser, you'd get a generic 404 error from Slim. Let's fix that. Update your index.php with the following:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use \Psr\Http\Message\RequestInterface;
use \Psr\Http\Message\ResponseInterface;

$app = new \DI\Bridge\Slim\App;

$app->get('/', function (RequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('Hello world!');

    return $response;
});

$app->run();
index.php, now with a basic route

This is better -- we've got Hello World in our Browser! Relish the fact that this simple task has been performed by virtually every programmer around the world. Let's dig into it. We've added a few use statements to keep our route binding as tidy as possible. Note lines 10 through 13 -- here we've told Slim that for HTTP GET requests to the document root, run this anonymous function we've provided. The function takes two arguments, our HTTP Request, and an HTTP response. We then use the response object to write the text "Hello World!" and pass it back to Slim, which fires it off to your browser.

For small tasks, this is fine. Anything more interesting and the function-bound-to-route idea starts to get unwieldy though. Sure, we could break out the routes into separate files, but wouldn't it be great if we could break things down into smaller units that were composed for our routes? Let's do just that. First, we'll need to update our composer.json to tell it where to find the class we'll write. Edit it to look like the following:

{
    "require": {
        "slim/slim": "^3.8",
        "php-di/slim-bridge": "^1.0"
    },
    "autoload": {
        "psr-4": {
            "": "./src"
        }
    }
}
composer.json

Composer gives us an autoloader for the packages we pull in, but we can also tell it to handle our code too! Here we add an autoload section, and tell composer to look for classes without a namespace in our src directory. With this in-place, we need to regenerate our autoloader. Run composer dumpautoload. We're now ready to make a few changes to the code. Edit your index.php once more:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

$app = new \DI\Bridge\Slim\App;

$app->get('/', TodoController::class . '::indexAction');

$app->run();</code>
index.php, simple again

We've replaced the anonymous function with a string that evaluates to TodoController::indexAction. PHP-DI knows to turn these strings into a request for the class TodoController, and to execute its method indexAction. We don't have this class yet, so let's add it now:

<?php

use \Psr\Http\Message\RequestInterface;
use \Psr\Http\Message\ResponseInterface;

class TodoController
{
    public function indexAction(RequestInterface $request, ResponseInterface $response)
    {
        $response->getBody()->write('Hello world!');

        return $response;
    }
}
TodoController.php

If you access the route now, you should be greeted with the same Hello World! we had before. This is a very simple example, so it may not seem worth the effort to build out your routes in this way. Suppose we want to use more than just Slim, however. Mustache is a popular template engine for many languages, let's add it to our application. Tell composer to pull in Mustache by running composer require mustache/mustache.

How do we use Mustache with our controller? We could create an instance inside the indexAction, but what if we have more than just that one method? How about we instantiate it inside the constructor:

<?php

// use statements here..

class TodoController
{
    private $mustache;

    public function __construct()
    {
        $this->mustache = new Mustache_Engine;
    }

    // rest of our methods here..

}
Mustache in TodoController.php's constructor

This works, but we've created a dependency on Mustache that isn't clear without examining the code. What if we decide to switch to Twig in a newer version? We'll have our work cut out for us tracking down instances of Mustache! Instead of creating the instance inside the constructor, let's inject the dependency by moving it out of the constructor block and into the argument list, like so:

<?php

// use statements here..

class TodoController
{
    private $mustache;

    public function __construct(Mustache_Engine $mustache)
    {
        $this->mustache = $mustache;
    }

    // rest of our methods here..

}
Injecting the Mustache dependency

When our container sees that TodoController needs a Mustache_Engine, it tries to create one to fulfill the dependency. Behold, the power of autowiring! Let's update our indexAction method to take advantage of Mustache:

<?php

use \Psr\Http\Message\RequestInterface;
use \Psr\Http\Message\ResponseInterface;

class TodoController
{
    private $mustache;

    public function __construct(Mustache_Engine $mustache)
    {
        $this->mustache = $mustache;
    }

    public function indexAction(RequestInterface $request, ResponseInterface $response)
    {
        $response->getBody()->write(
            $this->mustache->render('Hello {{ greetee }}', [
                'greetee' => 'World!'
            ])
        );

        return $response;
    }
}
Template-based rendering of our simple response.

While we're at it, Hello World is sort of boring. Let's allow the user to supply a name for the greeting. We can add route parameters to our methods by appending them as arguments after the Request and Response objects:

<?php

use \Psr\Http\Message\RequestInterface;
use \Psr\Http\Message\ResponseInterface;

class TodoController
{
    private $mustache;

    public function __construct(Mustache_Engine $mustache)
    {
        $this->mustache = $mustache;
    }

    public function indexAction(RequestInterface $request, ResponseInterface $response, $greetee = null)
    {
        $response->getBody()->write(
            $this->mustache->render('Hello {{ greetee }}', [
                'greetee' => $greetee ?? 'World!'
            ])
        );

        return $response;
    }
}
Route parameters in Controller methods

On line 15 we've added an argument greetee to our controller's indexAction method. We've also provided a default of null in case the greetee argument isn't provided. Then, on line 19 we use the null-coalesce operator to provide our original parameter. We're almost ready to use this extra parameter, we just need to tell Slim about it in our routing file, index.php:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

$app = new \DI\Bridge\Slim\App;

$app->get('/{greetee}', TodoController::class . '::indexAction');

$app->run();
index.php, now with an optional greetee parameter

With our updated route, we can now make a GET request such as /Gio and you should receive a response of 'Hello Gio!'. Awesome, isn't it?


We've only scratched the surface of what you can do with Slim and PHP-DI though. For instance, using Mustache_Engine could be handled better -- an interface that Mustache_Engine satisfies through an adapter would probably be a more SOLID approach, for instance.

Perhaps an article for another day 😀. Until then, be sure to check the docs on PHP-DI and Slim for more awesome ways to use these tools.