Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Slim Bridge Laravel Package

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.

Deep Wiki
Context7

Getting Started

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.
  • Your definitions file (config/container.php) — this is where interface bindings, config values, and factories live.
  • Slim 4 routing/middleware docs — the bridge only changes how callables are resolved, not how routes work.
  • PHP-DI autowiring docs — most controllers and middleware need no explicit definition at all.

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.

Implementation Patterns

Build the container once and pass it in

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());

Controllers: class names and Class:method

Both 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.

Middleware: also container-resolved

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.

A practical definitions file

<?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.

Middleware ordering and error handling

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.

Route closures are still useful for small things

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.

Testing without a web server

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.

Gotchas and Tips

  • 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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons