01 · THE IDEAREAD → TRY → LEARN
The idea
Most objects need help from other objects: a formatter, a logger, a pricing rule. There are two ways to get that help. The class can create the helper itself, or it can be given the helper. Giving it the helper is called dependency injection. It sounds grand, but it is just an ordinary constructor argument.
The PHP manual describes the constructor as the method called on each newly created object, suited to any initialization the object needs before use. A collaborator is exactly that kind of need. If the object cannot work without it, require it in the constructor.
Why not create it inside?
Suppose a class runs new UpperFormatter() inside one of its methods. That class is now welded to one formatter. You cannot change the behavior without editing the class, and you cannot replace the formatter with a simple fake while checking the result. When the dependency arrives from outside, the class only needs to know what the collaborator can do, not which concrete class it is.
That "what it can do" is usually expressed as an interface. The consumer type-hints the interface, and any class implementing it can be passed in.
A first worked example
Here Greeter depends on a Formatter. It uses constructor property promotion (available since PHP 8.0): a modifier such as private on a constructor parameter declares the property and assigns it in one step.
interface Formatter
{
public function format(string $text): string;
}
final class UpperFormatter implements Formatter
{
public function format(string $text): string
{
return strtoupper($text);
}
}
final class Greeter
{
public function __construct(private Formatter $formatter)
{
}
public function greet(string $name): string
{
return $this->formatter->format("Hello, " . $name);
}
}
$greeter = new Greeter(new UpperFormatter());
echo json_encode($greeter->greet("Ada"), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);"HELLO, ADA"
Greeter never mentions UpperFormatter. Pass a different Formatter and the greeting changes without touching Greeter.
Fakes that record what happened
Because the collaborator is passed in, you can pass a small in-memory stand-in and inspect it afterward. This logger keeps its messages in an array instead of writing anywhere.
interface Logger
{
public function log(string $message): void;
}
final class MemoryLogger implements Logger
{
private array $messages = [];
public function log(string $message): void
{
$this->messages[] = $message;
}
public function all(): array
{
return $this->messages;
}
}
final class Signup
{
public function __construct(private Logger $logger)
{
}
public function register(string $email): bool
{
$this->logger->log("registered:" . $email);
return true;
}
}
$logger = new MemoryLogger();
$signup = new Signup($logger);
$signup->register("a@example.test");
$signup->register("b@example.test");
echo json_encode($logger->all(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);["registered:a@example.test","registered:b@example.test"]
The test code holds a reference to the same MemoryLogger that Signup received, so it can read what Signup did.
Optional dependencies with a default
Since PHP 8.1 an object may be used as a default parameter value through new in an initializer. That lets a dependency be optional while still being replaceable.
interface Tax
{
public function apply(int $amount): int;
}
final class NoTax implements Tax
{
public function apply(int $amount): int
{
return $amount;
}
}
final class FlatTax implements Tax
{
public function __construct(private int $extra)
{
}
public function apply(int $amount): int
{
return $amount + $this->extra;
}
}
final class Invoice
{
public function __construct(private Tax $tax = new NoTax())
{
}
public function total(int $amount): int
{
return $this->tax->apply($amount);
}
}
echo json_encode([
(new Invoice())->total(100),
(new Invoice(new FlatTax(15)))->total(100),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);[100,115]
The manual restricts what may appear in such an initializer: no dynamic class names, no anonymous classes and no argument unpacking.
Common mistakes
- Calling
newon a concrete class inside a method, which hides the dependency. - Type-hinting a concrete class when an interface would do, which blocks substitutes.
- Injecting too much. If a constructor takes seven collaborators, the class probably does too many jobs.
- Forgetting that a child class with its own constructor must call
parent::__construct()itself if it needs the parent's setup.
Exercise contract
Write checkoutTotal(array $prices, int $percentOff): int. The prices are integer cents. Inside the function, build a Checkout that receives a PriceRule through its constructor, using a PercentOff rule. Checkout::total() must sum the prices first, then pass the sum to the rule. PercentOff returns intdiv($cents * (100 - $percent), 100), which rounds down. An empty list gives 0. Rounding once on the sum matters: two items of 999 cents at 50% off total 999, not 998.
Recap
Pass collaborators in through the constructor, type them by interface, and let promotion keep the code short. The class stays focused, and in-memory fakes make results easy to check.
Functions · Arrays · Types
- Pass a deterministic in-memory collaborator through the constructor
- Implement and test a deterministic PHP 8.4 function