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
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
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
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
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
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
Mailerinterface. - Create a
RecordingMailerimplementation. - Create a
RegisterUserclass that receivesMailerthrough 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
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.