php-di/slim-bridge
Integrates the PHP-DI dependency injection container with Slim, letting you use autowiring, constructor injection, and PSR-11 services in Slim applications. Configure PHP-DI once and resolve controllers, middleware, and dependencies automatically.
The bridge is a thin but important integration layer: it makes Slim 4 resolve controllers and middleware through a PHP-DI container, instead of only using the container as a passive service locator.
Install:
composer require php-di/slim-bridge
This pulls in PHP-DI, Slim 4, and slim/psr7 (used for the default PSR-7 factories). The only API most projects need is DI\Bridge\Slim\Bridge::create().
Minimal entry point:
<?php
// public/index.php
use DI\Bridge\Slim\Bridge;
use DI\ContainerBuilder;
require __DIR__ . '/../vendor/autoload.php';
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/../config/container.php');
$container = $builder->build();
$app = Bridge::create($container);
$app->get('/', \App\Controller\HomeController::class);
$app->run();
Where to look first:
DI\Bridge\Slim\Bridge::create() — the single integration point.config/container.php) — this is where interface bindings, config values, and factories live.A good first use case is replacing manual object construction in route closures. Before:
$app->get('/users', function ($request, $response) {
$repository = new SqlUserRepository(new PDO($_ENV['DB_DSN']));
// ...
});
After:
final class ListUsersController
{
public function __construct(private UserRepository $users) {}
public function __invoke(Request $request, Response $response): Response
{
$response->getBody()->write(json_encode($this->users->all()));
return $response->withHeader('Content-Type', 'application/json');
}
}
$app->get('/users', ListUsersController::class);
PHP-DI autowires the controller; if UserRepository is an interface, bind it once in config/container.php.
Use Bridge::create($container) rather than Bridge::create() as soon as you have any configuration. Bridge::create() with no argument creates a default container that already contains Slim's PSR-7 factories, but you cannot add definitions to it afterwards.
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/../config/container.php');
$builder->enableCompilation(__DIR__ . '/../var/cache/php-di'); // production only
$app = Bridge::create($builder->build());
Class:methodBoth common forms are resolved through the container:
// Invokable controller
$app->get('/users', ListUsersController::class);
// Controller with a specific method
$app->get('/users/{id}', 'App\Controller\UserController:show');
Constructor injection works in both cases:
final class UserController
{
public function __construct(
private UserRepository $users,
private ResponseFactoryInterface $responseFactory,
) {}
public function show(Request $request, Response $response, array $args): Response
{
$user = $this->users->find((int) $args['id']);
$response->getBody()->write(json_encode($user));
return $response->withHeader('Content-Type', 'application/json');
}
}
Prefer injecting dependencies over fetching them from the container inside the controller. If you need response creation outside the method signature, inject ResponseFactoryInterface.
Global middleware:
final class RequireAuth implements MiddlewareInterface
{
public function __construct(private TokenVerifier $verifier) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (! $this->verifier->isValid($request->getHeaderLine('Authorization'))) {
throw new HttpUnauthorizedException($request);
}
return $handler->handle($request);
}
}
$app->add(RequireAuth::class);
Per-route or per-group middleware works the same way:
$app->get('/admin', AdminController::class)->add(RequireAuth::class);
$app->group('/api', function (RouteCollectorProxy $group) {
$group->get('/users', ListUsersController::class);
$group->post('/users', CreateUserController::class);
})->add(RequireAuth::class);
The class string is resolved lazily at request time, so missing definitions fail during the request, not during boot.
<?php
// config/container.php
use function DI\autowire;
use function DI\env;
use function DI\get;
return [
// Interface bindings
UserRepository::class => autowire(SqlUserRepository::class),
TokenVerifier::class => autowire(JwtTokenVerifier::class),
// Config values
'db.dsn' => env('DB_DSN'),
'mailer.dsn' => env('MAILER_DSN'),
// Explicit construction when scalars/config are involved
Mailer::class => autowire(SmtpMailer::class)
->constructorParameter('dsn', get('mailer.dsn')),
PDO::class => autowire()->constructor(get('db.dsn')),
];
This keeps controllers and middleware free of environment lookups and static factories.
Slim middleware added later wraps middleware added earlier. The standard, effective order is:
$app->addBodyParsingMiddleware();
$app->addRoutingMiddleware();
$errorMiddleware = $app->addErrorMiddleware(
displayErrorDetails: $isDev,
logErrors: true,
logErrorDetails: true,
);
$errorMiddleware->setDefaultErrorHandler(
fn (ServerRequestInterface $request, Throwable $e) => $container
->get(App\Error\ErrorHandler::class)
->handle($request, $e)
);
Adding error middleware last makes it outermost, so it catches routing exceptions and errors thrown by inner middleware.
Closures are not container-resolved, but they are convenient for health checks or wiring:
$app->get('/health', function (Request $request, Response $response) use ($container) {
$response->getBody()->write(json_encode([
'db' => $container->get(PDO::class) instanceof PDO,
]));
return $response->withHeader('Content-Type', 'application/json');
});
For anything with more than one dependency or real logic, move it to a controller class.
Slim's handle() method makes integration tests straightforward:
$app = Bridge::create($container);
$app->get('/users', ListUsersController::class);
$request = (new ServerRequestFactory())
->createServerRequest('GET', '/users');
$response = $app->handle($request);
$this->assertSame(200, $response->getStatusCode());
Build a fresh container per test, or override specific services with $container->set(...) before handle() when you need fakes. Avoid sharing a compiled container between tests unless the cached state is what you are testing.
Do not use AppFactory::create() directly if you want controller injection. AppFactory::createFromContainer($container) gives Slim a container, but the bridge supplies the callable resolver that actually fetches controller and middleware classes from PHP-DI. Use Bridge::create($container).
Bridge::create() without arguments hides the container. It creates a usable default container with Slim's PSR-7 factories, but you cannot add definitions afterwards. If you need bindings, build the container yourself and pass it in.
Closures bypass the container. A route defined as function (...) { new Service(...) } gets no autowiring. Class strings (Controller::class, 'Controller:method', Middleware::class) are the container-resolved forms.
Autowiring fails on interfaces and scalars. Bind interfaces explicitly (UserRepository::class => autowire(SqlUserRepository::class)). For scalar constructor arguments, use DI\env(), DI\get('config.key'), or constructorParameter().
Middleware resolution failures happen at request time. Because middleware class strings are resolved lazily, a missing binding can produce a confusing runtime error rather than a boot error. Keep definitions for middleware in the same file as controllers and check $container->has() when debugging.
Custom error handlers are not auto-wired by Slim. setDefaultErrorHandler() accepts a callable or an ErrorHandlerInterface, so resolve the handler from the container inside a closure, as shown above. Do not pass a bare class string unless the class is invokable.
Error middleware ordering matters. Add it last (outermost) so it catches routing errors and exceptions from other middleware. The familiar sequence is body parsing, routing, then error middleware.
Keep slim/psr7 (or another PSR-7 implementation) installed. The default container relies on it for ResponseFactoryInterface and StreamFactoryInterface. If you replace the container, make sure those factories are still defined or resolvable; otherwise Slim cannot create responses.
Avoid injecting Slim\App into controllers or middleware. It creates circular wiring and makes testing harder. Inject ResponseFactoryInterface, RouteParserInterface, or UrlGeneratorInterface instead when you need URL generation or response creation.
Compilation is production-only. $builder->enableCompilation($dir) gives a worthwhile performance boost, but the cache directory must be writable and must be cleared after definition changes. Keep compilation disabled locally; stale compiled containers are a common source of "why is my new binding ignored?" confusion.
Container mutations should happen before the app handles requests. $container->set() is useful in tests and for narrow runtime overrides, but production configuration belongs in ContainerBuilder definitions. Do not treat the built container as a mutable service registry.
Version check: this bridge targets Slim 4 and PHP-DI 7. The old Slim 3 DI\Bridge\Slim\App approach (extending an app class) is not the API here. If you are following a Slim 3 tutorial, stop and use Bridge::create() instead.
Debugging container errors: PHP-DI exceptions typically include the resolution chain, which is usually enough to identify the missing scalar or interface. If you use PHP-DI attributes such as #[Inject], verify attributes are enabled on the builder and that the class is actually being resolved through the container.
How can I help you explore Laravel packages today?