Skip to content
intermediate Phase 5 · Interfaces, Traits, Enums, and Advanced Classes

Contracts with Interfaces

Specify method signatures that implementing classes must provide.

40m
0 problems
Topic Progress 0%

implements: Declaring What a Class Promises

Your logging code must accept anything that can write a line: a file logger, a syslog adapter, a test array logger. None of them share a parent — file and syslog come from different libraries — yet all of them must satisfy some checkable promise. An interface is that promise, and it contains nothing but signatures:

interface Logger
{
    public function log(string $level, string $message): void;
}

final class FileLogger implements Logger
{
    public function __construct(private string $path) {}

    public function log(string $level, string $message): void
    {
        file_put_contents(
            $this->path,
            sprintf("[%s] %s: %s\n", date('c'), $level, $message),
            FILE_APPEND,
        );
    }
}

final class ArrayLogger implements Logger
{
    public array $records = [];

    public function log(string $level, string $message): void
    {
        $this->records[] = [$level, $message];
    }
}

implements Logger is a compile-time pledge: this class provides log(string $level, string $message): void with a compatible signature. Miss it and PHP fatals at class declaration — Class ArrayLogger contains 1 abstract method and must therefore be declared abstract or implement the remaining method — the same enforcement abstract methods get, but here with zero code attached.

What an interface may contain in PHP 8, precisely: method signatures (implicitly public — writing public is required by convention and by older tooling, but there is no way to declare a non-public interface method), and constants. No properties, no method bodies, no constructors. That last absence is the practical difference from the abstract classes you just met: if you catch yourself wanting a body, you want an abstract class; if two unrelated classes must satisfy the contract, only an interface fits, because a class extends exactly one parent but may implement any number of interfaces.

The implementer's obligation is signature-compatibility, and PHP 8 checks it at compile time: parameters equal or wider than the contract (contravariant), return equal or narrower (covariant). Widening visibility is allowed (protected in an abstract parent becoming public here), narrowing is a fatal. One naming note for the roadmap ahead: interfaces have no required I-prefix convention; the PHP-ecosystem norm is a noun or capability name — Logger, Countable, JsonSerializable, Stringable — and Stringable is worth a look: since PHP 8.0, any class defining __toString() satisfies it automatically, the engine's one built-in example of a contract with no implements keyword. Ordinary contracts still need the explicit pledge — and once classes pledge to them, call sites can stop caring who wrote the class.

A historical note explains a PHP quirk you will meet in old code: interfaces have never carried method bodies, and PHP never adopted default methods, so designs that wanted shared behavior split into abstract classes, or into the pre-5.4 pattern of an interface plus duplicating implementers. Traits closed that gap in 2014, and the modern three-way split is stable: interfaces for the promise, abstract classes for the spine, traits for the sideways body. One more enforcement detail: since interface methods are implicitly public, an implementer declaring the matching method protected is a fatal 'must be public' error, not a variance question — visibility on implementations may widen but never narrow, exactly like ordinary overrides. And a class that implements an interface while extending an abstract parent may satisfy the contract with methods from either source, which is how template methods and interfaces interlock in framework base classes.

The line between the two mechanisms starts at Abstract Classes and Contracts: an interface is what remains of an abstract class once you remove its code, its state, and its claim on the single inheritance slot.

One Class, Many Contracts: implements, extends, and Interface Constants

The superpower interfaces add over inheritance is multiplicity. A payment class is simultaneously a refundable, a loggable, and a JSON-serializable object, with each concern declared independently:

interface Refundable
{
    public function refund(string $transactionId): string;
}

interface Loggable
{
    public function logChannel(): string;
}

final class StripePayment implements Refundable, Loggable, \JsonSerializable
{
    public function refund(string $transactionId): string { /* ... */ }
    public function logChannel(): string { return 'payments'; }
    public function jsonSerialize(): array { return ['tx' => $this->refund('')); }
}

Three contracts, one inheritance slot untouched. Each interface is a separate, small promise — the interface-segregation principle in syntax: callers depend on Refundable and get exactly the method they know about, instead of one fat contract that forces every implementer to stub methods it does not need. Splitting contracts is cheap and always legal; merging them is the move that hurts later.

Interfaces themselves compose, through extends — and unlike classes, an interface may extend several:

interface Readable   { public function read(): string; }
interface Writable   { public function write(string $data): void; }

interface Stream extends Readable, Writable
{
    public function close(): void;
}

final class MemoryStream implements Stream
{
    public function read(): string { /* ... */ }
    public function write(string $data): void { /* ... */ }
    public function close(): void { /* ... */ }
}

Implementing Stream means satisfying all three contracts — the compiler checks the union. This is how the standard library builds families: Countable, ArrayAccess, Iterator, IteratorAggregate, JsonSerializable are all plain interfaces, and a class adopts any subset its domain justifies. Note what never appears on an interface: final methods (no bodies to protect), properties, or a constructor.

Interfaces may also carry constants, which implementers and callers read through the interface name:

interface Cache
{
    public const DEFAULT_TTL = 300;
    public function get(string $key): mixed;
}

echo Cache::DEFAULT_TTL;                  // 300

Use them for contract-level defaults and enumerated values that belong to the vocabulary itself; put anything configurable into constructor injection instead, because an interface constant cannot vary per instance.

The failure mode multiplicity prevents is equally worth naming: the base-class lottery. With one abstract parent, a JsonCache extends ArrayStore design forces you to choose which capability owns the inheritance slot, and every later need reshuffles the hierarchy. implements makes adding a capability a one-line, backward-compatible edit. The discipline that keeps the resulting call sites honest — hinting the contract instead of the concrete class, from parameters through return types to test doubles — is the subject of the next chapter.

Interface constants deserve a realistic caveat before you overuse them: they are compile-time values visible through the interface name, so they cannot vary per implementation or per instance — fine for a DEFAULT_TTL or a fixed set of result codes, wrong for anything configurable. Modern code reaching for a fixed value set should usually reach for an enum from Backed Enums for Fixed Values instead, which adds types, matching, and methods where a constant offers only a name. Teams also build capability ladders with interface inheritance deliberately: a broad Readable for minimal adopters, a ReadableAndWritable extending it for the richer slot, and consumers hinted at the lowest rung that gives them what they need — the interface-segregation principle expressed as package structure rather than as a slogan.

Programming to Interfaces: Hints, Returns, and Test Doubles

An interface earns its keep at the call sites. The rule is simple and total: type against the smallest contract that expresses the need.

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

    public function build(array $rows): \JsonSerializable
    {
        $this->logger->log('info', 'report started');
        return new ReportPayload($rows);
    }
}

$svc = new ReportService(new FileLogger('/var/log/app.log'));

ReportService now knows two names from the outside world — Logger and JsonSerializable — and nothing about files, syslog, or the payload's class. Swap FileLogger for ArrayLogger and the service compiles unchanged. This is dependency injection in its smallest possible frame: constructor takes the contract, stores it, uses only contract members. Calling ->flush() — a method the File logger happens to have but the contract never promised — is a fatal error even when the runtime object could do it, and that is correct: the contract is the only thing ReportService may rely on.

Return types join in the same way, usually covariantly. A factory promises the contract, hiding which implementer it chose:

function makeLogger(string $channel): Logger
{
    return match ($channel) {
        'test'  => new ArrayLogger(),
        default => new FileLogger("/var/log/$channel.log"),
    };
}

Callers get a Logger; the concrete pick is an implementation detail that can change without breaking anyone. And because the hints are checked at compile time, the whole scheme fails loudly: passing an ArrayLogger where Refundable is promised throws TypeError: Argument #1 must be of type Refundable, ArrayLogger given — the contract boundary enforced by the engine, not by convention.

The payoff shows up in tests, where a hand-written double beats a mocking library for small contracts:
n

final class SpyLogger implements Logger
{
    public array $calls = [];

    public function log(string $level, string $message): void
    {
        $this->calls[] = [$level, $message];
    }
}

$spy = new SpyLogger();
(new ReportService($spy))->build($rows);
assert(count($spy->calls) === 1);   // the service logged, without touching a disk

No filesystem, no process globals, no sleeps — the interface is the seam a test seizes. That is the argument to give in reviews: the concrete class is an option the wiring decides, the interface is the decision the code makes. Keep interfaces small so doubles stay trivial, and treat instanceof on interfaces (if ($x instanceof Refundable)) as legitimate — it asks about a capability, not a class name. Two remaining reuse tools complete the toolbox for classes that share neither lineage nor contract: methods that travel sideways between unrelated classes, which is what Reusing Behavior with Traits is for.

The runtime reflection side completes the picture for framework authors. class_implements($obj) lists the contracts a value satisfies, and $obj instanceof SomeInterface powers container resolution and capability checks across package boundaries. PSR-3, PSR-7, and PSR-11 are the proof at ecosystem scale: dozens of packages interoperate because they all pledge the same small signatures, and an application swaps implementations by changing one binding. When you design your own package's interfaces, borrow that lesson — small, single-purpose, named for the capability — and let abstract classes stay private implementation detail. The interface is the part of your design the world is allowed to depend on, which is also the part that must never leak a concrete class into a signature.

Quiz

1. Which member can an interface legally declare in PHP 8?

Question 1 options

2. A class implements Refundable and Loggable but is missing Refundable::refund(). What happens?

Question 2 options

3. What does `interface Stream extends Readable, Writable` mean for a class that implements Stream?

Question 3 options

4. Why type a service's constructor parameter as Logger instead of FileLogger?

Question 4 options

Flashcards

Question

What may an interface contain in PHP 8, and what is forbidden?

Answer

Method signatures (implicitly public) and constants. Forbidden: properties, method bodies (no default methods), constructors, and non-public methods. Wanting a body means you want an abstract class; wanting signatures only means interface.

Question

implements vs extends for interfaces — the multiplicity rules

Answer

A class extends one class but implements any number of interfaces. An interface itself may extend MULTIPLE interfaces (class inheritance cannot). Implementing a child interface obligates the class to every method in the whole merged contract, checked at compile time.

Question

Signature compatibility rules for interface implementers

Answer

Parameters equal or wider (contravariant), returns equal or narrower (covariant); visibility may widen but never narrow. Violations are a compile-time fatal 'must be compatible', so a wrong pledge never reaches runtime.

Question

What is 'programming to interfaces', concretely?

Answer

Type parameters, return types, and properties against the smallest contract that expresses the need; call only contract members (touching a concrete-only method is a fatal error); pick the implementer in wiring/factories. The concrete class becomes an option; the interface is the decision.

Question

How do interfaces enable testing, and which instanceof use is legitimate?

Answer

A hand-written test double (SpyLogger implementing Logger) records calls without touching disk or globals — the interface is the seam tests seize. instanceof on an interface asks about a capability ('is this Refundable?') and is legitimate at boundaries; instanceof chains on concrete classes inside logic signal a missing contract.

Revision Notes

Key Takeaways

  • 1. An interface is a compile-time pledge of signatures (plus constants) — no bodies, no state — enforced fatally at class declaration
  • 2. One class implements many interfaces while extending one class; interfaces themselves extend multiple interfaces — that multiplicity is the escape from the single-inheritance lottery
  • 3. Programming to the smallest contract makes the concrete class a wiring decision, so swapping FileLogger for a SpyLogger changes nothing in the service
  • 4. Return types and factories hide the concrete pick behind the contract; TypeError enforces the boundary at every call
  • 5. instanceof on an interface asks a capability question and is fine; instanceof chains on concrete classes mean a contract is missing

Interview Tips

  • • "What can an interface contain?" — method signatures and constants only; no properties, no bodies, no constructors. Add that PHP has no default interface methods, so shared code means abstract class or trait
  • • "Interface or abstract class?" — pure signature with unrelated implementers or several contracts per class → interface; shared code/state/constructor with a single lineage → abstract class
  • • "How do interfaces help testing?" — they are the injection seam: a hand-written double implementing the contract records calls or returns canned data, so tests run without disks, networks, or globals
  • • "What is interface segregation?" — split fat contracts into small capability interfaces (Refundable, Loggable) so implementers and test doubles only touch methods they actually need; adding a small interface is a backward-compatible change

Cheat Sheet

Contracts with Interfaces

interface Logger
{
    public const DEFAULT_LEVEL = 'info';
    public function log(string $level, string $message): void;   // signatures only
}

interface Refundable { public function refund(string $txId): string; }
interface Loggable   { public function logChannel(): string; }

final class StripePayment implements Refundable, Loggable, \JsonSerializable
{
    public function refund(string $txId): string { /* ... */ }
    public function logChannel(): string { return 'payments'; }
    public function jsonSerialize(): array { return []; }
}

function pay(Refundable $p): string { return $p->refund('tx_1'); }   // contract-typed call site

Contain / forbid:

Allowed Forbidden
method signatures (implicitly public) properties
constants method bodies (no default methods)
extends multiple interfaces constructors; non-public methods

Multiplicity: class extends one class, implements many interfaces; interface extends many interfaces.

Compatibility: parameters equal/wider, returns equal/narrower, visibility may widen — else compile-time fatal.

Testing seam:

final class SpyLogger implements Logger
{
    public array $calls = [];
    public function log(string $level, string $message): void
    {
        $this->calls[] = [$level, $message];
    }
}

Hint the smallest contract; choose the implementer in wiring only.

Continue Learning