Dependency Injection

As soon as an app needs a database connection, a logger, or a mailer, a question comes up: where should that object be created, and how does every route handler that needs it get access to the same instance? A dependency injection (DI) container answers this — it's a central place that knows how to create and hand out shared objects, called services, instead of every piece of code creating its own fresh copy with new.

In plain terms: Rather than a route handler doing $db = new PDO(...) itself, it asks the container for 'db' and the container either builds it (the first time) or hands back the one it already built.

Slim and PSR-11

Slim doesn't force a specific container on you. It works with any container that implements PSR-11 — the standard PHP interface for containers (get() and has() methods). A very common choice in the Slim world is PHP-DI, which is easy to set up and adds some convenient extra features on top of the plain PSR-11 interface.

Registering a Service

A container is typically configured with a definitions array: keys are service names, and values are functions that know how to build each one. Here's a container holding a single 'db' service, built with PHP-DI and handed to the app.

<?php
require __DIR__ . '/../vendor/autoload.php';

$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->addDefinitions([
    'db' => function () {
        return new PDO('mysql:host=localhost;dbname=my_app', 'root', '');
    },
]);
$container = $containerBuilder->build();

\Slim\Factory\AppFactory::setContainer($container);
$app = \Slim\Factory\AppFactory::create();

Retrieving a Service in a Route Handler

Once the container is wired up, a route handler can ask for the 'db' service by name instead of building its own PDO object. When the route closure is bound to the container, $this->get() resolves a service; otherwise, the container is available through the app.

<?php
$app->get('/users', function ($request, $response) {
    $db = $this->get('db');

    $statement = $db->query('SELECT id, name FROM users');
    $users = $statement->fetchAll(PDO::FETCH_ASSOC);

    $response->getBody()->write(json_encode($users));
    return $response->withHeader('Content-Type', 'application/json');
});

Every route that needs the database asks the container for 'db' the same way — the connection details live in exactly one place, the container definition.

Note: The container also creates the same shared instance each time get('db') is called (by default), so routes reuse one connection rather than opening a new one per call.

Why This Matters

Problem it solvesHow DI helps
Scattered new PDO(...) callsConnection details live in one container definition instead of copy-pasted everywhere
Hard to testIn tests, a fake/mock service can be registered in place of the real one, without touching route code
Tight couplingRoute handlers depend on a service name, not a concrete class they must construct themselves

Dependency injection is a deep topic with plenty of nuance — auto-wiring, factories, scopes — but the core idea above (register once, retrieve by name, share the instance) is enough to start building real Slim apps that stay easy to change and test.

Next up: Put routing, middleware, and services together to build real endpoints in Building JSON APIs.