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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 Notifier interface with a send() method.
  • Create an EmailNotifier implementation.
  • Create a RecordingNotifier implementation 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 example
<?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.