PHP cookbook · Intermediate

Wire a PHP app with a DI container

Stop creating dependencies with `new` all over the code and make classes testable.

Approach

Use constructor injection everywhere and let a PSR-11 container (PHP-DI, Symfony DependencyInjection) build objects through autowiring. Bind interfaces to implementations in one place (the composition root) and keep $container->get() out of business code.

Solution

<?php
declare(strict_types=1);

use DI\ContainerBuilder;
use function DI\autowire;
use function DI\factory;

$builder = new ContainerBuilder();
$builder->enableCompilation(__DIR__ . '/var/cache');           // fast in production
$builder->addDefinitions([
    PDO::class => factory(fn () => createPdo()),
    OrderRepository::class => autowire(PdoOrderRepository::class),
    PaymentGateway::class => autowire(StripeGateway::class)->constructorParameter('apiKey', getenv('STRIPE_KEY')),
    Psr\Log\LoggerInterface::class => factory(fn () => createLogger()),
]);
$container = $builder->build();

// Composition root (front controller / router) only:
$controller = $container->get(CheckoutController::class);
$response = $controller->handle($request);

// In tests, no container needed:
$service = new CheckoutService(new InMemoryOrderRepository(), new FakePaymentGateway());

Pitfalls

  • Injecting the container into services (service locator).
  • Global singletons via static methods instead of container-managed shared instances.
  • Runtime reflection autowiring without compilation in production.

Variations

  • Symfony: autowiring and autoconfiguration in services.yaml.
  • Laravel: service providers and contextual binding.

Further reading: PHP-DI: Getting started

Reference: https://php-di.org/doc/getting-started.html

More PHP recipes