I'll Be Backend
Design patterns · PHP

Dependency injection: stop creating collaborators inside

Run the same class with different collaborators without editing the class itself.

14 steps4 chapters~6 min at 1.25×#dependency injection#constructor injection#composition root#decoupling
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the PHP video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–5

Refactor

Steps 6–10

Payoff

Steps 11–12

Recap

Steps 13–14

The code: before and after

Before

declare(strict_types=1);

final readonly class RateCard
{
    public function __construct(private int $perHour) {}

    public function price(int $hours): int
    {
        return $hours * $this->perHour;
    }
}

final readonly class ReceiptPrinter
{
    public function __construct(private string $branch) {}

    public function print(int $bikeId, int $euros): void
    {
        echo "{$this->branch}: bike $bikeId, $euros EUR\n";
    }
}

final class ReturnDesk
{
    private readonly RateCard $rates;
    private readonly ReceiptPrinter $printer;

    public function __construct()
    {
        $this->rates = new RateCard(3);
        $this->printer = new ReceiptPrinter('City Center');
    }

    public function checkIn(int $bikeId, int $hours): void
    {
        $euros = $this->rates->price($hours);
        $this->printer->print($bikeId, $euros);
    }
}

(new ReturnDesk())->checkIn(7, 3);

After

declare(strict_types=1);

final readonly class RateCard
{
    public function __construct(private int $perHour) {}

    public function price(int $hours): int
    {
        return $hours * $this->perHour;
    }
}

final readonly class ReceiptPrinter
{
    public function __construct(private string $branch) {}

    public function print(int $bikeId, int $euros): void
    {
        echo "{$this->branch}: bike $bikeId, $euros EUR\n";
    }
}

final class ReturnDesk
{
    public function __construct(
        private readonly RateCard $rates,
        private readonly ReceiptPrinter $printer,
    ) {}

    public function checkIn(int $bikeId, int $hours): void
    {
        $euros = $this->rates->price($hours);
        $this->printer->print($bikeId, $euros);
    }
}

function openDesk(int $perHour, string $branch): ReturnDesk
{
    $rates = new RateCard($perHour);
    $printer = new ReceiptPrinter($branch);
    return new ReturnDesk($rates, $printer);
}

openDesk(3, 'City Center')->checkIn(7, 3);
openDesk(5, 'Airport')->checkIn(7, 3);

Transcript, step by step

Problem

Step 1: A desk that builds its collaborators Before

This is a bike rental return desk, and Rate Card holds the euros per hour and multiplies them by the hours. Receipt Printer holds a branch name and echoes the branch, the bike and the euros. Return Desk creates both in its own constructor, with three euros and City Center written right there. Check in asks the rate card for the price and tells the printer to print it. At the bottom, the script in rentals dot P H P checks in bike seven after three hours. So the receipt reads City Center, bike seven, nine euros. With one branch, a desk that builds its own rate card and printer was the shortest path, and callers knew nothing.

Step 2: An airport branch Change request

Now a second branch opens at the airport, and it charges five euros an hour and prints Airport on its receipts. Both desks run in the same program, so the quick way gives Return Desk's constructor a branch name. Inside, an if builds the airport rate card and printer, and the else keeps the city ones. PHPStan is happy, and the script checks in bike seven at each desk. The receipts say nine euros at City Center, then fifteen at Airport.

Step 3: The desk picks its own collaborators Code smell

Return Desk calls the rate card and the printer to do part of its job, so each one is a collaborator. Return Desk holds the check-in rule, yet it also picks its rate card and printer, because it creates its collaborators inside. It picks the branch here, builds the airport collaborators here, and builds the city collaborators here. Check in didn't change, yet Return Desk was edited for a change that's only about prices and names. And a third branch means one more elseif, in the same constructor.

Step 4: The else hides a typo Code smell

Worse, here the script passes Airport with a capital A. What does that desk charge? Three euros an hour, the city rate, because this comparison is case-sensitive and the else catches it.

Step 5: Dependency injection Principle

The desk should get its rate card and printer from whoever builds it. A class that receives its collaborators from outside, instead of creating them, uses dependency injection. In plain words, whoever builds the desk hands it the rate card and the printer, and the desk only uses them.

Refactor

Step 6: Refactor first Back it out

So we take the branch parameter and the if back out, and the desk serves City Center alone again. We refactor first in small steps with the same receipt each time, and the airport comes back at the end.

Step 7: Pass the rate card in Change Function Declaration

Return Desk's constructor gets a Rate Card parameter and stores it, so it no longer creates one. This move is Change Function Declaration, and when a constructor gains a collaborator this way, it's called constructor injection. The parameter type tells PHPStan and the reader that every desk needs a Rate Card. The line that builds the rate card with three moved into the script, which passes it in, so the receipt stays.

Step 8: Pass the printer in Change Function Declaration

The same constructor injection again for the printer, so the constructor takes a Receipt Printer too. With constructor promotion, each parameter's modifiers make it a property too, so the property lines and assignments are gone. Now the constructor only stores what it's given, so it names no rate and no branch. The script builds the printer with City Center and passes both collaborators in, and the receipt hasn't changed.

Step 9: One function opens a desk Extract Function

The airport would need a copy of the script's three build lines, so we extract them into a function, open desk. It takes the rate and the branch name, builds both collaborators from them, and returns a new desk. A function whose only job is to build and return an object is called a factory function. The script calls open desk with three and City Center, and the receipt is still the same. Each new setting, such as a currency, adds one more parameter to open desk.

Step 10: One place wires the collaborators Composition root

Something still has to create the objects and wire them, so open desk and the script's last line do it. The one place that builds and wires every object is called the composition root. It's the only code that knows the rate and the branch name. Return Desk, meanwhile, keeps the check-in rule and nothing else.

Payoff

Step 11: The airport, take two Payoff

So the airport request from the start can come back in now. The script calls open desk with five and Airport, and checks in bike seven for three hours. It prints nine euros at City Center and fifteen at Airport, the same two receipts as the quick version.

Step 12: The desk did not change Payoff

Return Desk didn't change at all to add the airport. Where the if stood, the constructor only stores its collaborators, and both branches' collaborators are built here, from the arguments. Return Desk keeps the check-in rule and the caller chooses the collaborators, so a new branch is one more call. And a misspelled branch name can't fall into the wrong prices, because there's no name left to compare. You might think injection needs an interface. It doesn't, because both collaborators stay concrete final classes here, and only their creation moved. But now the composition root has to know every collaborator, and it builds each desk by hand.

Recap

Step 13: Where we started, where we ended Before / after

On the left, Return Desk created its rate card and printer itself, with three and City Center written in its constructor. So a second branch meant editing the desk, even though its check-in rule stayed the same. On the right, the desk receives both collaborators as promoted parameters and only stores them. And open desk, in the composition root, builds each branch's collaborators from its arguments.

Step 14: When to reach for it Takeaways

Reach for it when one class must run with different collaborators in the same program, like two branches or two currencies. Skip it when a class has one setup that no caller will ever change. Skip it too when the collaborator is a plain array it fills, because passing it in only moves the creation outward. Spot it by a constructor that builds configured objects with values written inside, or takes a flag to pick them. And PHP's own Limit Iterator works this way, because it takes the iterator it reads from as a constructor argument.