Skip to content

PHP Tutorial Advanced OOP

Dependency Injection Basics

Lesson 219 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

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

index.php
PHP
<?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();
Output
OUTPUT
[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

index.php
PHP
<?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";
Output
OUTPUT
[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

  1. Flexible. Swap the logger without touching OrderService.
  2. Testable. In a test, pass a fake that records messages.
  3. Honest. The constructor lists everything the class needs. No hidden dependencies.

A test double

index.php
PHP
<?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];
Output
OUTPUT
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.

index.php
PHP
<?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 new for a service inside another class.
  • Avoid global access to services (Logger::get(), global $db).
info
Why this matters in Laravel
In Laravel, you write public function __construct(private Mailer $mailer) and the framework supplies the object. That is dependency injection with a container doing the wiring.