Dependency Inversion in Practice
Wire adapters to the core with dependency inversion.
Dependency Inversion in Practice is a free PHP Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Inversion vs Injection
People conflate two distinct ideas. Dependency Injection is a technique: pass collaborators in rather than constructing them. Dependency Inversion (the D in SOLID) is a principle: high-level policy and low-level detail both depend on an abstraction, and the abstraction is owned by the high-level module. This lesson is about making the second one real — wiring concrete adapters to a core that defines the interfaces.
The Principle, Precisely
DIP states:
- High-level modules should not depend on low-level modules. Both depend on abstractions.
- Abstractions should not depend on details. Details depend on abstractions.
The subtle part: the interface belongs to the consumer, not the implementer. Your domain declares PaymentGateway; the Stripe adapter conforms to it — not the other way around.
Define the Abstraction in the Core
Put the interface next to the code that needs it, expressed in domain terms. No Stripe types leak in.
<?php
// src/Billing/Application/Port/PaymentGateway.php
interface PaymentGateway
{
public function charge(Money $amount, CardToken $token): ChargeId;
}
final class Money {
public function __construct(public readonly int $cents, public readonly string $currency) {}
}
final class CardToken { public function __construct(public readonly string $value) {} }
final class ChargeId { public function __construct(public readonly string $value) {} }Conform an Adapter to It
The infrastructure adapter implements the core's interface and translates to the vendor SDK. The dependency arrow points from the adapter (detail) to the abstraction (policy) — inversion achieved.
<?php
final class StripePaymentGateway implements PaymentGateway
{
public function __construct(private \Stripe\StripeClient $stripe) {}
public function charge(Money $amount, CardToken $token): ChargeId {
$intent = $this->stripe->paymentIntents->create([
'amount' => $amount->cents,
'currency' => strtolower($amount->currency),
'payment_method' => $token->value,
'confirm' => true,
]);
return new ChargeId($intent->id);
}
}Constructor Injection Is the Default
Inject through the constructor and type-hint the abstraction. Dependencies become explicit, immutable, and impossible to forget. Avoid setter and property injection for required collaborators — they allow half-built objects.
<?php
final class CheckoutService
{
public function __construct(
private PaymentGateway $payments, // abstraction, not StripeClient
private OrderRepository $orders,
) {}
public function pay(OrderId $id, CardToken $token): ChargeId {
$order = $this->orders->get($id);
$charge = $this->payments->charge($order->total(), $token);
$order->markPaid($charge);
$this->orders->save($order);
return $charge;
}
}The Composition Root
All concrete wiring happens in exactly one place — the composition root — as close to main()/the entrypoint as possible. Nothing else news-up infrastructure. This is the only spot that knows Stripe exists.
<?php
// public/index.php — composition root
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET'));
$gateway = new StripePaymentGateway($stripe);
$orders = new PdoOrderRepository(new PDO(getenv('DB_DSN')));
$checkout = new CheckoutService($gateway, $orders);
// Everything below depends only on abstractions
$controller = new CheckoutController($checkout);Wiring with a DI Container
For non-trivial apps, a container (PHP-DI, Symfony) automates wiring. The key move is binding interfaces to implementations. Autowiring resolves constructors by type; you only declare the interface→class map.
<?php
use function DI\autowire;
use function DI\get;
return [
PaymentGateway::class => autowire(StripePaymentGateway::class),
OrderRepository::class => autowire(PdoOrderRepository::class),
\Stripe\StripeClient::class => fn() => new \Stripe\StripeClient(getenv('STRIPE_SECRET')),
PDO::class => fn() => new PDO(getenv('DB_DSN')),
];Swapping Adapters Proves the Point
Because the core depends only on PaymentGateway, switching providers or testing offline is a one-line wiring change. Here is a fake used in a unit test — the CheckoutService is unchanged and never imports Stripe.
<?php
final class FakeGateway implements PaymentGateway {
public array $charges = [];
public function charge(Money $a, CardToken $t): ChargeId {
$this->charges[] = $a;
return new ChargeId('ch_test_' . count($this->charges));
}
}
$fake = new FakeGateway();
$id = $fake->charge(new Money(2500, 'EUR'), new CardToken('tok_visa'));
echo $id->value, ' charges=', count($fake->charges), PHP_EOL; // ch_test_1 charges=1Avoid the Service Locator Trap
Injecting the container itself and pulling dependencies inside methods is the service locator anti-pattern. It hides dependencies, defeats type-checking, and re-couples your code to the container. Inject what you need explicitly.
<?php
// ANTI-PATTERN: hidden dependencies, container leaks everywhere
final class BadCheckout {
public function __construct(private ContainerInterface $c) {}
public function pay($id, $token) {
$gateway = $this->c->get(PaymentGateway::class); // hidden!
// ...
}
}Where the Container May Appear
The container is legitimate in exactly one layer: the composition root and the framework glue around it (e.g., a controller factory). Your domain and application classes must remain container-agnostic — they receive plain objects via constructors and could be instantiated by hand. A good test: could you wire the whole app in a single PHP file with no container? If yes, your dependencies are honest.
Lazy Wiring Without Service Location
Sometimes a dependency is expensive to build or only needed conditionally. Resist injecting the container — inject a factory closure instead. The dependency stays explicit and typed, while construction is deferred until actually used.
<?php
final class ReportService
{
/** @param Closure():PaymentGateway $gatewayFactory */
public function __construct(private Closure $gatewayFactory) {}
public function refundIfNeeded(bool $needed): void {
if (!$needed) return;
$gateway = ($this->gatewayFactory)(); // built only when required
// $gateway->charge(...) etc.
}
}
// Composition root supplies the factory, not the container
$svc = new ReportService(fn() => new StripePaymentGateway($stripe ?? null));
echo 'lazy dependency wired', PHP_EOL;Quick Check
Who should own the PaymentGateway interface?
Recap
You wired adapters to the core the right way:
- Inversion ≠ injection: injection is the mechanism, inversion is owning the abstraction in the consumer.
- The core defines the interface; infrastructure adapters conform to it.
- Use constructor injection of abstractions; do all concrete wiring in a single composition root (or container config that binds interface→class).
- Swapping adapters and faking in tests becomes a one-line change.
- Avoid the service locator anti-pattern — keep the container at the edge only.
Frequently asked questions
Is the “Dependency Inversion in Practice” lesson free?
Yes — the full text of “Dependency Inversion in Practice” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.
What will I learn in “Dependency Inversion in Practice”?
Wire adapters to the core with dependency inversion. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PHP Academy?
No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Dependency Inversion in Practice” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PHP Academy lesson?
Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- From Layered to Clean Architecture
- Ports and Adapters Explained
- Use Cases and Application Services
- Dependency Inversion in Practice