objects namespaces and application architecture

Dependency Injection

Dependency injection means giving an object the collaborators it needs instead of letting it create or fetch them secretly.

In PHP applications, this usually means constructor injection: a service receives its repository, mailer, logger, clock, HTTP client, or formatter through __construct(). The result is code with visible dependencies that is easier to test and replace.

Inject A Collaborator Through The Constructor

PHP example
<?php

declare(strict_types=1);

final class MoneyFormatter
{
    public function formatPennies(int $pennies): string
    {
        return '£' . number_format($pennies / 100, 2);
    }
}

final class InvoicePresenter
{
    public function __construct(private MoneyFormatter $formatter)
    {
    }

    public function totalLabel(int $totalPennies): string
    {
        return 'Total: ' . $this->formatter->formatPennies($totalPennies);
    }
}

$presenter = new InvoicePresenter(new MoneyFormatter());

echo $presenter->totalLabel(2499) . PHP_EOL;

// Prints:
// Total: £24.99

The presenter does not create its formatter internally. The dependency is supplied from the outside.

Depend On Interfaces At Boundaries

Interfaces are useful when the dependency talks to the outside world or may have multiple implementations.

PHP example
<?php

declare(strict_types=1);

interface Mailer
{
    public function send(string $to, string $subject): void;
}

final class EchoMailer implements Mailer
{
    public function send(string $to, string $subject): void
    {
        echo 'Sending ' . $subject . ' to ' . $to . PHP_EOL;
    }
}

final class RegisterUser
{
    public function __construct(private Mailer $mailer)
    {
    }

    public function handle(string $email): void
    {
        $this->mailer->send($email, 'Welcome');
    }
}

(new RegisterUser(new EchoMailer()))->handle('nia@example.com');

// Prints:
// Sending Welcome to nia@example.com

RegisterUser does not know whether the mailer uses SMTP, an API, a queue, or a fake implementation.

Injection Makes Tests Easier

A test can pass a recording fake instead of a real external service.

PHP example
<?php

declare(strict_types=1);

interface Clock
{
    public function now(): DateTimeImmutable;
}

final class FixedClock implements Clock
{
    public function now(): DateTimeImmutable
    {
        return new DateTimeImmutable('2026-05-20 09:00:00');
    }
}

final class TrialEndsAt
{
    public function __construct(private Clock $clock)
    {
    }

    public function afterDays(int $days): string
    {
        return $this->clock->now()->modify('+' . $days . ' days')->format('Y-m-d');
    }
}

echo (new TrialEndsAt(new FixedClock()))->afterDays(14) . PHP_EOL;

// Prints:
// 2026-06-03

The calculation is deterministic because the clock is injected.

Avoid Hidden Dependencies

Static calls, global variables, and service locators can hide what a class needs.

PHP example
<?php

declare(strict_types=1);

interface ReferenceGenerator
{
    public function orderReference(int $id): string;
}

final class SequentialReferenceGenerator implements ReferenceGenerator
{
    public function orderReference(int $id): string
    {
        return 'ORD-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT);
    }
}

final class OrderCreatedMessage
{
    public function __construct(private ReferenceGenerator $references)
    {
    }

    public function forOrder(int $id): string
    {
        return 'Created ' . $this->references->orderReference($id);
    }
}

echo (new OrderCreatedMessage(new SequentialReferenceGenerator()))->forOrder(42) . PHP_EOL;

// Prints:
// Created ORD-000042

The dependency is visible in the constructor, so a reviewer can see what the class needs.

Do Not Inject Plain Values Automatically

Inject services and collaborators. Plain values such as strings, IDs, and totals often belong in method arguments or value objects.

PHP example
<?php

declare(strict_types=1);

final class PasswordHasher
{
    public function hash(string $plainPassword): string
    {
        return password_hash($plainPassword, PASSWORD_DEFAULT);
    }
}

$hasher = new PasswordHasher();

echo password_verify('secret', $hasher->hash('secret')) ? 'verified' : 'failed';
echo PHP_EOL;

// Prints:
// verified

The hasher service is reusable. The password value is passed to the method at the time it is needed.

What To Remember

Dependency injection makes collaborators explicit. Prefer constructor injection for required services, depend on interfaces at external boundaries, use fakes in tests, and avoid hidden static or global dependencies that make code harder to reason about.

Practice

Task: Inject A Mailer

Create a user registration service that receives its mailer dependency.

Requirements

  • Use declare(strict_types=1);.
  • Create a Mailer interface.
  • Create a RecordingMailer implementation.
  • Create a RegisterUser class that receives Mailer through the constructor.
  • Validate the email address in RegisterUser.
  • Send a welcome email through the injected mailer.
  • Print how many messages were recorded.
  • Show one invalid email by catching the exception.
  • Include the expected output as comments in the same PHP code block.

The registration service should not create the mailer internally.

Show solution
PHP example
<?php

declare(strict_types=1);

interface Mailer
{
    public function send(string $to, string $subject): void;
}

final class RecordingMailer implements Mailer
{
    public array $messages = [];

    public function send(string $to, string $subject): void
    {
        $this->messages[] = [$to, $subject];
    }
}

final class RegisterUser
{
    public function __construct(private Mailer $mailer)
    {
    }

    public function handle(string $email): void
    {
        if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException('Email address is invalid.');
        }

        $this->mailer->send(strtolower($email), 'Welcome');
    }
}

$mailer = new RecordingMailer();
$registerUser = new RegisterUser($mailer);
$registerUser->handle('NIA@example.com');

echo count($mailer->messages) . ' message recorded' . PHP_EOL;

try {
    $registerUser->handle('not-an-email');
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// 1 message recorded
// Email address is invalid.

RegisterUser receives its mailer from the outside. The example uses a recording implementation, but production code could inject an SMTP, API, or queue-backed mailer with the same interface.