objects namespaces and application architecture
Interfaces
Interfaces are one of the main tools for dependency boundaries in PHP. They let code depend on behaviour instead of a specific concrete class, which is useful for storage, email, payments, queues, clocks, logging, and external APIs.
Define A Contract
An interface contains method signatures. A class uses implements to promise it provides those methods.
<?php
declare(strict_types=1);
interface Notifier
{
public function send(string $recipient, string $message): void;
}
class EmailNotifier implements Notifier
{
public function send(string $recipient, string $message): void
{
echo 'Email to ' . $recipient . ': ' . $message . PHP_EOL;
}
}
$notifier = new EmailNotifier();
$notifier->send('nia@example.com', 'Welcome');
// Prints:
// Email to nia@example.com: Welcome
The interface does not care whether the message is sent by email, SMS, a queue, or a test double.
Depend On The Interface
Functions and classes can ask for the interface type.
<?php
declare(strict_types=1);
interface Logger
{
public function info(string $message): void;
}
class EchoLogger implements Logger
{
public function info(string $message): void
{
echo '[info] ' . $message . PHP_EOL;
}
}
function importProducts(Logger $logger): void
{
$logger->info('Import started');
}
importProducts(new EchoLogger());
// Prints:
// [info] Import started
importProducts() does not know or care which logger implementation it receives.
Multiple Implementations Can Share One Contract
Different classes can implement the same interface.
<?php
declare(strict_types=1);
interface ReferenceFormatter
{
public function format(int $id): string;
}
class OrderReferenceFormatter implements ReferenceFormatter
{
public function format(int $id): string
{
return 'ORD-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT);
}
}
class InvoiceReferenceFormatter implements ReferenceFormatter
{
public function format(int $id): string
{
return 'INV-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT);
}
}
foreach ([new OrderReferenceFormatter(), new InvoiceReferenceFormatter()] as $formatter) {
echo $formatter->format(42) . PHP_EOL;
}
// Prints:
// ORD-000042
// INV-000042
The calling loop only relies on the format() method.
Interfaces Help Testing
A test can provide a simple implementation without sending real email or calling a real API.
<?php
declare(strict_types=1);
interface Mailer
{
public function send(string $to, string $subject): void;
}
class RecordingMailer implements Mailer
{
public array $sent = [];
public function send(string $to, string $subject): void
{
$this->sent[] = [$to, $subject];
}
}
$mailer = new RecordingMailer();
$mailer->send('nia@example.com', 'Welcome');
echo count($mailer->sent) . PHP_EOL;
// Prints:
// 1
This kind of fake implementation is often clearer than trying to mock a concrete mailer class.
Do Not Create Interfaces For Everything
An interface is useful when there are multiple implementations, a boundary to an external system, or a real need to substitute behaviour.
<?php
declare(strict_types=1);
final class SlugGenerator
{
public function fromTitle(string $title): string
{
return trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($title)) ?? '', '-');
}
}
echo (new SlugGenerator())->fromTitle('New Product') . PHP_EOL;
// Prints:
// new-product
This small deterministic class may not need an interface until there is a reason.
What To Remember
Interfaces define what behaviour is required. They are strongest at boundaries where implementations may vary: databases, filesystems, mailers, queues, payment providers, clocks, and APIs. Avoid adding interfaces automatically when one concrete class is enough.
Practice
Task: Swap Notification Implementations
Create an interface for sending notifications and two implementations.
Requirements
- Use
declare(strict_types=1);. - Create a
Notifierinterface with asend()method. - Create an
EmailNotifierimplementation. - Create a
RecordingNotifierimplementation for tests or local checks. - Write a function that accepts
Notifier. - Call that function once with each implementation.
- Print enough output to prove both implementations were used.
- Include the expected output as comments in the same PHP code block.
The function should depend on the interface, not a concrete notifier class.
Show solution
<?php
declare(strict_types=1);
interface Notifier
{
public function send(string $recipient, string $message): void;
}
class EmailNotifier implements Notifier
{
public function send(string $recipient, string $message): void
{
echo 'Email to ' . $recipient . ': ' . $message . PHP_EOL;
}
}
class RecordingNotifier implements Notifier
{
public array $messages = [];
public function send(string $recipient, string $message): void
{
$this->messages[] = [$recipient, $message];
echo 'Recorded message for ' . $recipient . PHP_EOL;
}
}
function sendWelcomeMessage(Notifier $notifier, string $recipient): void
{
$notifier->send($recipient, 'Welcome');
}
sendWelcomeMessage(new EmailNotifier(), 'nia@example.com');
sendWelcomeMessage(new RecordingNotifier(), 'lee@example.com');
// Prints:
// Email to nia@example.com: Welcome
// Recorded message for lee@example.com
sendWelcomeMessage() depends on the Notifier interface. That lets production code use one implementation and tests or local checks use another.