A dependency is something a class needs to do its job, such as a logger, a database connection or a mailer.
There are two ways to get one: create it inside the class, or have it handed in from outside. The second is dependency injection (DI), and it makes code far easier to change and test.
Without DI: the class builds its own tools
<?php
class FileLogger
{
public function log(string $m): void { echo "[file] $m\n"; }
}
class OrderService
{
private FileLogger $logger;
public function __construct()
{
$this->logger = new FileLogger(); // hard-wired
}
public function place(): void
{
$this->logger->log("Order placed");
}
}
(new OrderService())->place();
[file] Order placed
It works, but OrderService is stuck with FileLogger. You cannot swap it for another logger, and you cannot test the service without writing to a real file.
With DI: pass the dependency in
<?php
interface Logger
{
public function log(string $message): void;
}
class FileLogger implements Logger
{
public function log(string $message): void { echo "[file] $message\n"; }
}
class NullLogger implements Logger
{
public function log(string $message): void {}
}
class OrderService
{
public function __construct(private Logger $logger) {}
public function place(): void
{
$this->logger->log("Order placed");
}
}
(new OrderService(new FileLogger()))->place();
(new OrderService(new NullLogger()))->place();
echo "Second call logged nothing";
[file] Order placed
Second call logged nothing
OrderService now asks for a Logger. It does not care which. The code that creates the service chooses.
Why this is better
- Flexible. Swap the logger without touching
OrderService. - Testable. In a test, pass a fake that records messages.
- Honest. The constructor lists everything the class needs. No hidden dependencies.
A test double
<?php
interface Mailer
{
public function send(string $to, string $body): void;
}
class FakeMailer implements Mailer
{
public array $sent = [];
public function send(string $to, string $body): void
{
$this->sent[] = $to;
}
}
class Welcome
{
public function __construct(private Mailer $mailer) {}
public function run(string $email): void
{
$this->mailer->send($email, "Welcome!");
}
}
$fake = new FakeMailer();
(new Welcome($fake))->run("ana@example.com");
echo count($fake->sent) . " email(s) sent to " . $fake->sent[0];
1 email(s) sent to ana@example.com
No real email was sent, and we could still check that the code tried to send one.
Where do the objects get created?
At the edge of the program, in one place often called the composition root. Everything else just receives what it needs.
<?php
// index.php: the composition root
$logger = new FileLogger();
$mailer = new SmtpMailer($config["smtp"]);
$service = new OrderService($logger, $mailer);
$service->place();
DI containers
In large applications, a container builds the objects for you and wires the dependencies automatically. Laravel and Symfony have one. A container is a convenience on top of the idea above. You do not need it to use DI.
Rules of thumb
- Take dependencies in the constructor.
- Type them with interfaces where it makes sense.
- Do not call
newfor a service inside another class. - Avoid global access to services (
Logger::get(),global $db).
Why this matters in Laravel
public function __construct(private Mailer $mailer) and the framework supplies the object. That is dependency injection with a container doing the wiring.