Objects Namespaces And Application Architecture

Interfaces

An interface defines a contract that implementing classes must satisfy. It describes public behavior without providing the normal method implementation. Code can then depend on what an object can do rather than on one concrete class.

Interfaces are especially valuable at boundaries that may have several implementations: payment providers, storage, mail delivery, queues, clocks, logging, search engines, and external APIs. They also let an application expose a narrow internal contract instead of allowing callers to depend on every public method of a large library class.

The goal is not to put an interface in front of every class. The goal is to make important dependencies explicit and replaceable where that improves the design.

Define And Implement A Contract

An interface contains method signatures. A class uses implements and must provide compatible public methods.

PHP example
<?php

declare(strict_types=1);

interface Notifier
{
    public function send(string $recipient, string $message): void;
}

final 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

Interface methods are public. An implementation cannot make send() protected or private because callers using the Notifier contract are promised public access.

The signature is part of the promise: parameter types, return type, by-reference markers, variadic behavior, and required versus optional parameters must remain compatible. Parameter names also matter to callers using named arguments, so implementations should normally retain the interface's names.

Depend On The Interface At The Boundary

A class gains flexibility when it asks for the contract it uses rather than a specific implementation.

PHP example
<?php

declare(strict_types=1);

interface Logger
{
    public function info(string $message): void;
}

final class EchoLogger implements Logger
{
    public function info(string $message): void
    {
        echo '[info] ' . $message . PHP_EOL;
    }
}

final class ProductImporter
{
    public function __construct(private Logger $logger)
    {
    }

    public function import(): void
    {
        $this->logger->info('Import started');
    }
}

(new ProductImporter(new EchoLogger()))->import();

// Prints:
// [info] Import started

ProductImporter knows that it can call info(). It does not know whether the logger writes to standard output, a file, an observability service, or an in-memory test recorder.

This does not mean every call should receive an interface type. If a service requires behavior unique to one concrete domain class, that class may be the honest dependency. Interfaces are strongest where the abstraction represents a real capability or boundary.

Several Implementations Can Share One Contract

A single contract lets different objects participate in the same workflow.

PHP example
<?php

declare(strict_types=1);

interface ReferenceFormatter
{
    public function format(int $id): string;
}

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

final class InvoiceReferenceFormatter implements ReferenceFormatter
{
    public function format(int $id): string
    {
        return 'INV-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT);
    }
}

$formatters = [
    new OrderReferenceFormatter(),
    new InvoiceReferenceFormatter(),
];

foreach ($formatters as $formatter) {
    echo $formatter->format(42) . PHP_EOL;
}

// Prints:
// ORD-000042
// INV-000042

The loop uses only the interface. This is polymorphism: different object types can be used through a shared contract while preserving their own implementation.

Substitution must be behavioral, not merely syntactic. If one formatter throws for every positive ID while another formats it, both classes may satisfy PHP's method signature but not the expectation of callers. Document meaningful preconditions, results, and failure behavior.

One Class Can Implement Several Interfaces

PHP classes have one parent class at most, but they can implement multiple interfaces. This lets a class advertise several independent capabilities.

PHP example
<?php

declare(strict_types=1);

interface RendersReport
{
    public function render(): string;
}

interface NamesDownload
{
    public function filename(): string;
}

final class CsvSalesReport implements RendersReport, NamesDownload
{
    public function render(): string
    {
        return "month,total\nJune,129900";
    }

    public function filename(): string
    {
        return 'sales.csv';
    }
}

$report = new CsvSalesReport();

echo $report->filename() . PHP_EOL;
echo $report->render() . PHP_EOL;

Small capability interfaces allow a caller to request only what it needs. A renderer can accept RendersReport; a download controller may need both rendering and naming. Composite type declarations can express the latter directly, while a named combined interface can make a frequently used combination easier to read.

Interfaces Can Extend Interfaces

An interface may extend one or more other interfaces. The child contract includes all inherited requirements.

PHP example
<?php

declare(strict_types=1);

interface ReadsCache
{
    public function get(string $key): mixed;
}

interface WritesCache
{
    public function put(string $key, mixed $value): void;
}

interface CacheStore extends ReadsCache, WritesCache
{
    public function forget(string $key): void;
}

final class ArrayCache implements CacheStore
{
    private array $values = [];

    public function get(string $key): mixed
    {
        return $this->values[$key] ?? null;
    }

    public function put(string $key, mixed $value): void
    {
        $this->values[$key] = $value;
    }

    public function forget(string $key): void
    {
        unset($this->values[$key]);
    }
}

This arrangement supports narrow dependencies. A query service that only reads can ask for ReadsCache, while cache administration can ask for CacheStore.

Do not build a deep interface hierarchy merely to mirror a class hierarchy. Extend interfaces when the child genuinely promises all parent capabilities and callers benefit from that named relationship.

Keep Interfaces Focused

A large interface forces every implementation to provide every method, even when some methods do not make sense. Consider a broad file contract with read(), write(), delete(), compress(), publish(), and makePublic(). A read-only archive would need fake or exception-throwing implementations for operations it cannot support.

Split contracts around coherent caller needs:

PHP example
<?php

declare(strict_types=1);

interface DocumentReader
{
    public function read(string $path): string;
}

interface DocumentWriter
{
    public function write(string $path, string $contents): void;
}

final class TemplateRenderer
{
    public function __construct(private DocumentReader $documents)
    {
    }

    public function render(string $path): string
    {
        return strtoupper($this->documents->read($path));
    }
}

TemplateRenderer cannot delete or overwrite documents because its dependency does not expose those operations. A narrow interface improves understanding and limits accidental capability, not only testability.

Avoid reducing every interface to a single method by rule. Cohesion matters more than method count. A transaction interface with begin(), commit(), and rollBack() describes one coherent responsibility.

Interfaces Support Purpose-Built Test Doubles

Tests can provide a small implementation that records interactions without performing the external action.

PHP example
<?php

declare(strict_types=1);

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

final class RecordingMailer implements Mailer
{
    /** @var list<array{to: string, subject: string}> */
    public array $sent = [];

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

$mailer = new RecordingMailer();
$mailer->send('nia@example.com', 'Welcome');

echo $mailer->sent[0]['subject'] . PHP_EOL;

// Prints:
// Welcome

A handwritten fake or recorder is often clearer than a complex mocking setup. It can implement realistic behavior, remain reusable across tests, and fail in a way developers can inspect.

Do not weaken the production contract solely to make tests easier. Tests and production should agree on the same behavior. If the interface is painful to fake, it may expose too many responsibilities or low-level details.

Choose Where The Interface Lives

A useful dependency interface often belongs near the application code that consumes it, because that consumer defines what it needs. An order service might own a PaymentAuthorizer contract, while infrastructure provides a Stripe-backed implementation.

This protects application code from a vendor SDK's much larger API. The infrastructure adapter translates the application's stable contract into provider-specific calls and translates provider errors back into application-level outcomes.

In other cases, a reusable package intentionally publishes interfaces for implementers. Ownership depends on who defines the abstraction. The key is to avoid copying a third-party class method-for-method into an interface that adds no stable application meaning.

Evolving An Interface Can Break Implementations

Adding a required method to an existing interface is a backward-compatibility break for every implementing class. This includes implementations in other repositories and private test doubles that the interface author cannot see.

If only some implementations support a new capability, create a separate interface rather than expanding the original one:

PHP example
<?php

declare(strict_types=1);

interface ChargesPayments
{
    public function charge(int $amountPennies): string;
}

interface RefundsPayments
{
    public function refund(string $paymentId, int $amountPennies): void;
}

A provider can implement one or both. Existing charge-only providers remain valid. Callers that issue refunds explicitly require RefundsPayments.

Changing parameter or return types also affects compatibility. Implementations may accept broader parameter types and return narrower result types within PHP's variance rules, but an interface update must still preserve what existing callers and implementers were promised.

Interface Constants And Property Requirements

Interfaces may declare constants shared by implementations. Use them for values that are genuinely part of the public contract, not as a substitute for configuration.

PHP 8.4 added support for declaring hooked properties in interfaces. An interface can require that an implementing object expose readable or writable property behavior without prescribing storage. This is an advanced tool and ties the package to PHP 8.4 or later. Methods remain the more portable and familiar contract for packages supporting older runtimes.

The dedicated Property Hooks lesson covers that syntax and its design tradeoffs. Do not rewrite a clear method-based interface merely because a newer syntax exists.

Interfaces, Abstract Classes, And Traits

An interface defines a public contract and supports multiple implementation. It does not provide ordinary shared method bodies or instance state.

An abstract class can provide state, protected helpers, constructor behavior, and partial implementation, but a class can extend only one parent. Use it when implementations genuinely share a base model, not only because several classes have similarly named methods.

A trait reuses method or property implementation inside classes. It does not by itself create a substitutable public type. A class can use a trait without promising any particular interface to callers.

These tools can work together: classes may implement an interface, extend a meaningful abstract base, and use focused traits. Choose each tool for its actual role rather than treating them as interchangeable code-reuse mechanisms.

When An Interface Is Unnecessary

A small deterministic class such as a slug formatter may have one implementation, no external side effects, and no meaningful alternate behavior. Adding SlugFormatterInterface immediately can create naming and navigation overhead without improving a boundary.

Introduce an interface when at least one concrete reason exists:

  • the application has multiple valid implementations
  • code crosses an external or infrastructure boundary
  • a consumer needs a narrower contract than a vendor class exposes
  • independent packages need a shared extension point
  • testing needs controlled boundary behavior
  • architectural ownership requires dependency inversion

"Everything must have an interface" is not a design principle. Honest, focused dependencies are.

What You Should Be Able To Do

After this lesson, you should be able to declare an interface, implement it with compatible public methods, type a dependency against the contract, and use several implementations polymorphically. You should understand how interface extension and multiple interfaces compose capabilities.

You should also be able to split an oversized interface around consumer needs, create a purpose-built test implementation, distinguish interfaces from abstract classes and traits, and recognise that adding a method to a published interface can break every existing implementer.

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.

Practice: Segregate Inventory Capabilities

Replace one broad inventory dependency with reader and writer contracts matched to actual callers.

Task

Create:

  • an InventoryReader interface with stockFor(string $sku): int
  • an InventoryWriter interface with setStock(string $sku, int $quantity): void
  • an InventoryStore interface extending both
  • an ArrayInventory implementation of InventoryStore
  • a StockReporter that depends only on InventoryReader

Seed two SKU quantities, then use StockReporter to print them.

Check Your Work

Confirm that StockReporter cannot call setStock() through its declared dependency. Explain how the narrow contract reduces accidental capability even though the runtime object also supports writes.

Show solution

The concrete store supports both operations, while the reporter receives only the read contract it needs.

PHP example
<?php

declare(strict_types=1);

interface InventoryReader
{
    public function stockFor(string $sku): int;
}

interface InventoryWriter
{
    public function setStock(string $sku, int $quantity): void;
}

interface InventoryStore extends InventoryReader, InventoryWriter
{
}

final class ArrayInventory implements InventoryStore
{
    /** @var array<string, int> */
    private array $stock = [];

    public function stockFor(string $sku): int
    {
        return $this->stock[$sku] ?? 0;
    }

    public function setStock(string $sku, int $quantity): void
    {
        if ($quantity < 0) {
            throw new InvalidArgumentException('Stock cannot be negative.');
        }

        $this->stock[$sku] = $quantity;
    }
}

final class StockReporter
{
    public function __construct(private InventoryReader $inventory)
    {
    }

    public function line(string $sku): string
    {
        return $sku . ': ' . $this->inventory->stockFor($sku);
    }
}

$inventory = new ArrayInventory();
$inventory->setStock('KB-101', 8);
$inventory->setStock('MS-202', 14);

$reporter = new StockReporter($inventory);

echo $reporter->line('KB-101') . PHP_EOL;
echo $reporter->line('MS-202') . PHP_EOL;

// Prints:
// KB-101: 8
// MS-202: 14

StockReporter is declared against InventoryReader, so its code cannot use write methods without changing the dependency contract. The object passed at runtime may have more capabilities, but the consumer sees only what it requested.

Practice: Compose Report Contracts

Use interface extension to name a combination of report capabilities.

Task

Create:

  • a RendersReport interface with render(): string
  • a NamesDownload interface with filename(): string
  • a DownloadableReport interface extending both
  • a CsvOrderReport implementation
  • a prepareDownload() function accepting DownloadableReport

The function should return a line containing the filename and byte length of the rendered content. Print one result with expected output comments.

Check Your Work

Confirm that implementing DownloadableReport requires both inherited methods. Explain when a named combined interface is clearer than asking every caller to repeat a capability combination.

Show solution

The child interface gives a recurring combination of capabilities one domain-specific name.

PHP example
<?php

declare(strict_types=1);

interface RendersReport
{
    public function render(): string;
}

interface NamesDownload
{
    public function filename(): string;
}

interface DownloadableReport extends RendersReport, NamesDownload
{
}

final class CsvOrderReport implements DownloadableReport
{
    public function render(): string
    {
        return "order,total\n1042,2499";
    }

    public function filename(): string
    {
        return 'orders.csv';
    }
}

function prepareDownload(DownloadableReport $report): string
{
    $contents = $report->render();

    return $report->filename() . ': ' . strlen($contents) . ' bytes';
}

echo prepareDownload(new CsvOrderReport()) . PHP_EOL;

// Prints:
// orders.csv: 21 bytes

Any implementation of DownloadableReport must provide both inherited methods. The combined name is useful when the application repeatedly treats rendering and download naming as one coherent role.