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

Abstract Classes and Contracts

Provide base implementations that force subclasses to complete.

40m
0 problems
Topic Progress 0%

A Class You Cannot new: Half-Implemented Blueprints

An Exporter family shares a write-and-flush pipeline, but each format — CSV, JSON, XLSX — computes its rows differently. An abstract class is the mechanism for "this much is finished, this much is yours":

abstract class Exporter
{
    public function __construct(private string $target) {}

    abstract protected function renderRow(array $record): string;

    abstract protected function header(): string;

    public function export(array $records): string
    {
        $out = $this->header();
        foreach ($records as $record) {
            $out .= $this->renderRow($record) . "\n";
        }
        return $out;
    }
}

The abstract keyword does two things. On the class: it forbids instantiation — new Exporter('/tmp/out.csv') throws Error: Cannot instantiate abstract class Exporter. On a method: it declares the signature and omits the body; there is no {} at all, just the semicolon. Everything else is ordinary: export() is a concrete method shipping complete, shared behavior, and $this->renderRow() inside it calls whichever implementation the concrete child provides.

The contract is enforced at the child's declaration, not at runtime. A child must implement every abstract method, with a compatible signature, before it can ever be instantiated:

final class CsvExporter extends Exporter
{
    protected function renderRow(array $record): string
    {
        return implode(',', array_map(fn($v) => (string) $v, $record));
    }

    protected function header(): string
    {
        return implode(',', array_keys(['id' => 0, 'name' => 0]));
    }
}
// missing one → Fatal error: Class CsvExporter contains 1 abstract method
//              and must therefore be declared abstract or implement the remaining method

That compile-time fatal is the point: an incomplete subtype never reaches production. Two rules apply to abstract methods themselves. They may be protected or public but never private — a private method the child is required to implement would be a method no one can legally write. And an abstract method can never also be final or static: those combinations contradict "the child must provide this".

Visibility and invariants carry over from ordinary inheritance. A child implementing renderRow() may call the parent's protected members, but not its private ones (see Public, Private, and Protected Members); the constructor is still the child's responsibility to invoke via parent::__construct(...) if it declares its own. What abstract adds to Inheritance and the extends Keyword is a formal division of labor: the parent owns the skeleton — the order of operations in export() — and the child owns the steps the skeleton cannot know. That division has a name and a discipline, and it is the subject of the next chapter.

Abstract classes also carry ordinary class features into the contract, which is their quiet advantage. A constructor with promoted protected properties gives every child shared state initialized in one place; concrete helper methods give shared behavior; constants give contract-level defaults. None of that is available to an interface, and all of it composes with the abstract steps: the child's renderRow() sees the same protected target every sibling sees, so the family stays coherent without repeating the plumbing. One caution as hierarchies grow: an abstract class is still a parent, and every child couples to its protected surface — so keep that surface as small as the template requires, mark finished behavior final, and remember from Inheritance and the extends Keyword that a child declaring its own constructor owes the parent an explicit parent::__construct call.

Everything abstract classes do, they do on top of ordinary inheritance — the extends rule, protected seams, and explicit parent calls from Static Properties and Methods and the foundations phase all apply unchanged; the abstract keyword only adds the unfinished steps and the no-instantiation rule.

Template Method: Fixed Steps, Overridable Hooks

The export() method in the previous chapter is a template method: an algorithm whose overall shape is fixed while selected steps are delegated to subclasses. The pattern pays for itself the moment the shared skeleton carries logic you do not want duplicated — here, iteration and newline handling; in real systems, retry loops, transaction boundaries, or validation order:

abstract class Notifier
{
    final public function send(Notification $n): bool
    {
        $body = $this->format($n);              // step 1: delegated
        $ok = $this->deliver($body, $n->to());  // step 2: delegated
        $this->afterSend($n, $ok);              // step 3: hook, default no-op
        return $ok;
    }

    abstract protected function format(Notification $n): string;

    abstract protected function deliver(string $body, string $to): bool;

    protected function afterSend(Notification $n, bool $ok): void
    {
        // optional override — most notifiers ignore this
    }
}

Notice final public function send(). Marking the template method final is the pattern's enforcement mechanism: subclasses must not re-derive the sequence, because the whole value of the pattern is that step 1→2→3 happens identically everywhere. Specialization is confined to the abstract steps and the optional hook. If a subclass needs a different sequence, it does not override send() — it belongs under a different template, or the template was designed wrong.

The hook deserves a note. afterSend() has a default empty body, so it is neither abstract nor abstract-class-only; it is a concrete method most children leave alone and some extend — a notification logger, say. That graduated contract (two required steps, one optional extension point) is something interfaces cannot express without forcing every implementer to declare the optional method too.

The pattern's traps are inherited from overriding, plus one of its own. Because dispatch is dynamic, calling an abstract protected method from the parent's concrete code is safe only because the child must have implemented it — the compiler guaranteed it. The template's own trap is the fragile base class: if send() calls format() and subclasses also call format() internally expecting pristine input, the parent's later edits ripple into children invisibly. Keep the delegated steps small, document their preconditions, and never call overridable steps from the constructor. As always, the substituted subclass must honor the parent's behavioral contract — LSP from Overriding That Breaks Substitutability applies with full force; a deliver() that silently returns true without sending breaks every caller of the template. When the family shares no code at all — only a vocabulary of method signatures — an abstract class is overkill, and the cheaper, multiple-implementation tool takes over: Contracts with Interfaces.

A word on naming the steps, because the pattern lives or dies by its vocabulary. Abstract methods read as verbs of the specific work — renderRow, deliver, format — while the template reads as the story: send, export, handle. When the names tell the same story at both levels, subclasses become readable plug-ins: a reviewer sees handle() and knows the sequence, sees format() and knows the slot. When names drift — doWork, process2, helper — the pattern degrades into indirection with no payoff. There is also a testing consequence worth noting in interviews: the template's fixed sequence gets tested once against a fake subclass that records its step calls, and each new concrete subclass then needs tests only for its own steps — the skeleton is already covered.

Abstract Class or Interface? Choosing Where to Put the Contract

Both mechanisms answer "what must a class provide", and both are checked at compile time. The choice is decided by three questions: does the contract come with code? does it come with state? does the family need to also implement other contracts?

// Abstract class: contract + shared implementation + (optional) shared state
abstract class PaymentGateway
{
    protected array $config;                     // shared state

    public function __construct(array $config) { $this->config = $config; }

    public function fee(float $amount): float    // shared behavior
    {
        return $amount * ($this->config['rate'] ?? 0.029);
    }

    abstract public function charge(float $amount): Receipt;   // the required step
}

// Interface: contract only — signatures, no code, no state
interface Refundable
{
    public function refund(string $transactionId): Receipt;
}

PaymentGateway earns its abstract-class status twice: fee() is real code every subclass would otherwise copy, and $config is state the family shares. An interface cannot hold either — interfaces may declare only method signatures (plus constants), never properties, never method bodies. PHP has no default methods for interfaces the way Java 8 does; if you find yourself wanting a body in a contract, that is the abstract class telling you what it is.

The third question is where abstract classes show their cost. A class extends exactly one parent, so abstract class PaymentGateway consumes the inheritance slot entirely:

final class StripeGateway extends PaymentGateway implements Refundable, Loggable
{
    public function charge(float $amount): Receipt { /* ... */ }
    public function refund(string $transactionId): Receipt { /* ... */ }
    public function logChannel(): string { return 'payments'; }
}

Interfaces do not compete for that slot — a class may implements any number of them, and an interface may itself extends several interfaces. So the family vocabulary (charge, refund, log) belongs in interfaces, and the one true is-a spine with shared code belongs in the single abstract parent. This split is the standard shape of framework APIs you already use: ArrayAccess, Countable, Iterator, JsonSerializable are all interfaces precisely because the engine must accept objects from totally unrelated hierarchies.

Practical decision rules, distilled: abstract class when you have shared code, shared constructor logic, or protected state the children build on — and the family is a genuine single lineage. Interface when you have a pure signature contract, when unrelated classes must satisfy it, when you need more than one contract per class, or when you are defining a capability for objects you will never construct yourself (think Stringable, which any class with __toString automatically satisfies since PHP 8.0). Neither is "better OOP"; they occupy different slots — code-plus-lineage versus pure contract — and mature codebases use both, with traits covering the remaining axis of reuse: shared methods across classes that share no lineage at all, which is where Reusing Behavior with Traits begins.

The decision also has a maintenance dimension interviewers appreciate. An abstract parent's shared code is edited in one place and ships to every child on the next release — powerful when the change is a fix, dangerous when it changes behavior under children nobody retested. Interfaces carry no such leverage and therefore no such risk: implementing classes own their code outright. That trade — centralize behavior for DRYness and accept the coupling, or decentralize and accept the duplication — is the real question hiding under 'abstract or interface', and the mature answer is usually both: one abstract spine for the lineage, several interfaces for the capabilities, and a bias toward interfaces at package boundaries where third parties must satisfy the contract.

Quiz

1. What happens when you run `new Exporter('/tmp/out.csv')` where Exporter is declared abstract?

Question 1 options

2. A child extends an abstract class but implements only one of its two abstract methods. When is this caught?

Question 2 options

3. In the template-method pattern, why is the shared entry point usually marked final?

Question 3 options

4. Which requirement forces you toward an abstract class instead of an interface?

Question 4 options

Flashcards

Question

What two things does the abstract keyword do — on a class and on a method?

Answer

On a class: forbids instantiation ('Cannot instantiate abstract class'). On a method: declares the signature with no body, forcing every concrete child to implement it. Abstract methods must be public or protected (never private) and cannot be final or static.

Question

What error do you get when a concrete child misses an abstract method, and when is it raised?

Answer

A compile-time fatal: 'Class X contains N abstract method(s) and must therefore be declared abstract or implement the remaining method'. It fires when the class is declared/loaded — never at the first call — so incomplete subtypes cannot reach production.

Question

What is a template method, and how is the pattern enforced?

Answer

A concrete method that fixes the algorithm's sequence and delegates selected steps to abstract methods and optional hooks the children provide. Enforcement: mark the template final so subclasses cannot re-sequence it; keep delegated steps small and never call overridable steps from the constructor.

Question

Abstract class vs interface — the deciding questions

Answer

Does the contract ship code or state? Then abstract class (interfaces have no bodies or properties, only signatures and constants). Is it pure signature, must unrelated classes satisfy it, or must a class satisfy several contracts? Then interface — extends one parent, implements many interfaces.

Question

What does an abstract class provide that no combination of interfaces can?

Answer

Shared implementation and shared protected state, plus a single inheritance spine: concrete methods every child inherits, a constructor that runs once through parent::__construct, and protected seams. Interfaces can pool only signatures — PHP has no default interface methods — so real shared code always means abstract class.

Revision Notes

Key Takeaways

  • 1. abstract on a class forbids instantiation; abstract on a method forbids a body — every concrete child must complete the signature
  • 2. Missing an abstract implementation is a compile-time fatal, so incomplete subtypes never run — the contract is checked at declaration
  • 3. The template method pattern locks the algorithm with final and delegates designated steps; enforce it with final, not comments
  • 4. Abstract classes carry code, state, and constructors; interfaces carry only signatures — that difference decides the choice
  • 5. One class extends one parent but implements many interfaces, so lineage goes in the abstract spine and vocabulary in interfaces

Interview Tips

  • • "What can't an abstract class do?" — be instantiated; and its abstract methods can't be private, static, or final. Follow with the compile-time enforcement: missing implementations fatal at declaration
  • • "Abstract class vs interface?" — code or state in the contract means abstract class; pure signature, unrelated implementers, or multiple contracts means interface; mention PHP has no default interface methods
  • • "How does the template method pattern work?" — final concrete method fixes the sequence, abstract methods are the required steps, concrete no-op methods are optional hooks; warn about calling overridable steps from constructors
  • • "Why do framework contracts like JsonSerializable use interfaces?" — objects from unrelated hierarchies must satisfy them, and one class may implement several; interfaces are the only mechanism that scales past single inheritance

Cheat Sheet

Abstract Classes and Contracts

abstract class Exporter
{
    public function __construct(private string $target) {}

    abstract protected function renderRow(array $record): string;  // no body

    public function export(array $records): string                 // concrete, shared
    {
        $out = '';
        foreach ($records as $r) { $out .= $this->renderRow($r) . "\n"; }
        return $out;
    }
}

final class CsvExporter extends Exporter
{
    protected function renderRow(array $record): string { return implode(',', $record); }
}

new Exporter('x');   // Error: Cannot instantiate abstract class

Rules:

Rule Why
Abstract method: public/protected only private would be unimplementable, static uncallable via $this
Never abstract final "must implement" and "must not override" contradict
Missing implementation compile-time fatal at class declaration
Child with own constructor must call parent::__construct()

Template method: concrete final entry point fixes the sequence; abstract steps + optional concrete hooks specialize it. Document step preconditions; never call overridable steps from the constructor.

Choose abstract class when: shared code, shared state, or constructor logic + a single lineage. Choose interface when: pure signatures, unrelated implementers, multiple contracts per class, or capability interfaces (Stringable).

Continue Learning

Related Reading