Abstract Classes vs. Interfaces: Making the Right Design Choice
AI generated
<?php
8.4
PHP 8.4 · Abstract Classes · Interfaces · Design
Abstract Classes vs. Interfaces
making the right design choice

An abstract class and an interface appear at first glance to solve a similar problem, namely defining a contract that concrete classes must follow. The decisive difference lies in what each construct actually brings along: an interface is a pure contract with zero implementation, an abstract class, on the other hand, can contain constructors, shared state, and concrete methods that every subclass inherits automatically. This article uses a running report generator example to show when each tool is the right choice, why PHP's single inheritance restriction shapes this decision decisively, and how both concepts work together cleanly in a single class hierarchy.

13 min read Template Method · abstract · implements PHP 8.4 · OOP fundamentals

1. The core distinction: contract vs. partial implementation

An interface in PHP defines exclusively which methods a class must offer, without a single line of implementation, let alone a property or a constructor, being allowed to be part of the interface. An abstract class, on the other hand, may do both at the same time: it can provide concrete methods with complete code and simultaneously mark individual methods as abstract, which every concrete subclass must implement itself. This combination of finished and still open code is exactly the point where an abstract class fundamentally differs from an interface.

The difference is clearest when several subclasses need the same partial logic. If the same task were solved with an interface alone, every single implementing class would have to write the same logic again, because an interface structurally offers no room for shared code. An abstract class solves exactly this problem by implementing the shared part once, centrally, and delegating only the variable part to the subclasses.


<?php

declare(strict_types=1);

namespace Reporting;

// Abstract class: shared constant logic PLUS an open contract in one construct
abstract class AbstractReportGenerator
{
    public function renderHeader(string $title): string
    {
        return sprintf("=== %s ===\n", strtoupper($title));
    }

    // Every concrete subclass must supply its own data source
    abstract public function fetchData(): array;
}

// The equivalent interface-only version would lose renderHeader() entirely:
// every implementing class would have to duplicate that formatting logic.
interface ReportGeneratorInterface
{
    public function fetchData(): array;
}

This counter-example shows the price of a pure interface: renderHeader() would have to be written separately in every single implementing class, with the risk that the formatting eventually drifts apart between classes. The abstract class prevents exactly that, because the shared code exists in exactly one place and is inherited from there.

2. Constructors and shared state in abstract classes

A structural advantage of an abstract class over an interface is the ability to declare its own constructor. This constructor can, using constructor property promotion and readonly properties, enforce exactly the invariants every subclass must satisfy, without the subclass itself having to do anything beyond calling parent::__construct(). An interface structurally cannot do this, because it cannot prescribe a constructor signature at all, let alone execute constructor logic.

The following example extends the report generator with a constructor that fixes a timestamp and a report name as shared state, which every concrete report can equally use without recomputing it itself.


<?php

declare(strict_types=1);

namespace Reporting;

abstract class AbstractReportGenerator
{
    public readonly \DateTimeImmutable $generatedAt;

    public function __construct(
        private readonly string $reportName,
    ) {
        // Shared invariant: every report knows exactly when it was created
        $this->generatedAt = new \DateTimeImmutable();
    }

    public function renderHeader(): string
    {
        return sprintf(
            "=== %s (generated %s) ===\n",
            strtoupper($this->reportName),
            $this->generatedAt->format('Y-m-d H:i:s')
        );
    }

    abstract public function fetchData(): array;
}

final class SalesReportGenerator extends AbstractReportGenerator
{
    public function __construct(private readonly \PDO $db)
    {
        parent::__construct('Sales Report'); // must call parent constructor
    }

    public function fetchData(): array
    {
        return $this->db->query('SELECT * FROM sales')->fetchAll();
    }
}

Every subclass of AbstractReportGenerator receives generatedAt automatically, without duplicating the logic itself, but must explicitly pass the constructor call through. This exact combination of enforced constructor call and shared state is structurally unreachable with a pure interface, because an interface has no room for property declarations or constructor logic.

3. The Template Method pattern as a textbook example

The classic textbook case for an abstract class is the Template Method pattern. The base class defines a method marked final that fixes the entire flow of an algorithm in a set order, but calls, at specific points, hook methods declared abstract that every subclass must fill individually. The structure of the flow stays guaranteed identical for all subclasses, while the domain content varies exactly at the intended points.

This guarantee is the actual value of the pattern: no subclass author can accidentally forget to render the header before the data, because the order is hard-wired in the final method and cannot be overridden by any subclass.


<?php

declare(strict_types=1);

namespace Reporting;

abstract class AbstractReportGenerator
{
    // final: the overall algorithm structure is fixed for every subclass
    final public function generate(): string
    {
        $output = $this->renderHeader();

        foreach ($this->fetchData() as $row) {
            $output .= $this->formatRow($row);
        }

        return $output;
    }

    abstract protected function renderHeader(): string;
    abstract protected function fetchData(): array;
    abstract protected function formatRow(array $row): string;
}

final class InventoryReportGenerator extends AbstractReportGenerator
{
    public function __construct(private readonly \PDO $db)
    {
    }

    protected function renderHeader(): string
    {
        return "=== INVENTORY REPORT ===\n";
    }

    protected function fetchData(): array
    {
        return $this->db->query('SELECT sku, qty FROM inventory')->fetchAll();
    }

    protected function formatRow(array $row): string
    {
        return sprintf("%s: %d units\n", $row['sku'], $row['qty']);
    }
}

InventoryReportGenerator only needs to implement the three domain-variable methods, while generate() itself can never be overridden, because it is final. An interface could prescribe the same contract for fetchData() and formatRow() too, but the fixed flow order in generate() would then have to be rebuilt independently, and potentially inconsistently, in every implementing class.

4. The single inheritance limitation

PHP allows a class to extend exactly one abstract class, but to implement any number of interfaces at the same time. This asymmetry is not an arbitrary language decision, it is a deliberate avoidance of the classic diamond problem that can occur in languages with true multiple inheritance of implementation: if two base classes implement the same method differently, it would not be clear which version a shared subclass should inherit.

This restriction has direct consequences for design. Anyone trying to mix two completely independent behavior bundles into a single class via two abstract base classes at once immediately hits the language boundary. The practical consequence: as soon as more than one behavior dimension is needed, the path goes through interfaces plus composition, not through a second abstract base class. A deep chain of abstract classes that tries to bundle more and more behavior into a single inheritance line becomes increasingly inflexible as it grows.

5. Interfaces as pure contracts in detail

An interface may declare constants, but no properties and no method implementation. A class may implement any number of interfaces at the same time, as long as it provides a concrete implementation for every single method from every interface. This exact property makes interfaces the right tool when several, domain-wise completely independent types are meant to satisfy the same contract without having to share a common ancestor.

For testing and loose coupling this is exactly decisive: an interface like ReportGeneratorInterface can be replaced by a test double in a unit test without the test needing to know anything about a concrete class hierarchy. Requiring an abstract class as a dependency, on the other hand, would mean a test double would have to inherit the same inheritance line, unnecessarily strengthening coupling to the concrete implementation.


<?php

declare(strict_types=1);

namespace Reporting;

interface ExportableInterface
{
    public function toCsv(): string;
}

interface SchedulableInterface
{
    public function nextRunAt(): \DateTimeImmutable;
}

// One class can implement as many independent interfaces as needed
final class SalesReportGenerator extends AbstractReportGenerator implements
    ExportableInterface,
    SchedulableInterface
{
    public function toCsv(): string
    {
        // ...
        return '';
    }

    public function nextRunAt(): \DateTimeImmutable
    {
        return new \DateTimeImmutable('tomorrow 06:00');
    }
}

6. Combining both deliberately

In real codebases the choice is rarely either-or. A class can extend an abstract class that takes over part of the work while implementing an interface that describes the externally visible contract at the same time. The abstract class can even already concretely pre-implement part of the interface methods and leave only the rest as abstract, so every subclass only has to fill in the domain-specific part.


<?php

declare(strict_types=1);

namespace Reporting;

interface ReportGeneratorInterface
{
    public function generate(): string;
    public function fetchData(): array;
}

// Abstract class implements PART of the interface, leaves the rest open
abstract class AbstractReportGenerator implements ReportGeneratorInterface
{
    final public function generate(): string
    {
        $output = "=== Report ===\n";

        foreach ($this->fetchData() as $row) {
            $output .= json_encode($row) . "\n";
        }

        return $output;
    }

    // fetchData() remains abstract, ReportGeneratorInterface is not fully satisfied here
    abstract public function fetchData(): array;
}

final class UserReportGenerator extends AbstractReportGenerator
{
    public function fetchData(): array
    {
        return [['id' => 1, 'name' => 'Ada']];
    }
}

// Both relationships hold at the same time
$report = new UserReportGenerator();
var_dump($report instanceof ReportGeneratorInterface); // true
var_dump($report instanceof AbstractReportGenerator);  // true

This three-way relationship, a class extends an abstract class and thereby indirectly implements an interface, combines the benefits of both concepts: the shared implementation from the abstract class and the loose coupling through the interface type, which other code can program against without knowing the concrete inheritance line.

7. Common design mistakes

A recurring mistake is using an abstract class purely as a container for constants, without it actually bringing shared implementation or state. In this case an interface with constants, or, since PHP 8.1, an enum, would be the more appropriate choice, because both avoid forcing an unnecessary inheritance relationship that can later lead to unexpected coupling.

The reverse mistake occurs when an interface is used where shared implementation is actually needed. The result is almost always identical code that exists separately in every implementing class and must be maintained in multiple places at once on any change, with the corresponding risk that one place gets forgotten. A third, more subtle mistake is the "fragile base class" problem: the deeper a chain of abstract classes becomes, the greater the risk that a change to a shared method changes the behavior of all subclasses at once and unintentionally, far away from where the change was actually meant to apply.

8. A practical decision heuristic

A workable decision aid can be reduced to three questions. First: does concrete code or state actually need to be shared, not just a method signature? If yes, that speaks for an abstract class. Second: do several, domain-wise completely independent types need to satisfy the same contract without sharing a common ancestor? If yes, that clearly speaks for an interface. Third: will the shared behavior need to evolve independently of the individual subclasses in the future? If yes, composition, an object holds a reference to another object instead of inheriting it, is often the more robust alternative to a deep inheritance hierarchy.

Composition thereby solves a problem that neither interfaces nor abstract classes can structurally solve: the ability to swap behavior at runtime without changing the class hierarchy itself.


<?php

declare(strict_types=1);

namespace Reporting;

interface FormatterInterface
{
    public function format(array $row): string;
}

final class JsonFormatter implements FormatterInterface
{
    public function format(array $row): string
    {
        return json_encode($row) . "\n";
    }
}

// Composition: the generator HOLDS a formatter instead of inheriting one
final class ReportGenerator
{
    public function __construct(
        private readonly FormatterInterface $formatter,
    ) {
    }

    public function generate(array $rows): string
    {
        $output = '';
        foreach ($rows as $row) {
            $output .= $this->formatter->format($row);
        }

        return $output;
    }
}

// Swapping behavior at runtime, no new subclass, no abstract base class needed
$generator = new ReportGenerator(new JsonFormatter());

9. Abstract class vs. interface in direct comparison

The following table summarizes the decisive differences between an abstract class and an interface along the criteria that most often decide the right choice in practice.

Criterion Abstract class Interface
Contains implementation Yes, concrete methods possible No, signatures only
Constructor / state Yes, including properties No, no properties
Multiple relationships Only one base class (extends) Any number (implements)
Typical use case Template Method, shared base logic Loose coupling, testability, multiple contracts
Coupling to ancestor Tighter, shared inheritance line required Loose, no shared ancestor required

The table shows that the decision is rarely a matter of taste alone. As soon as state or concrete code needs to be shared, an abstract class is structurally superior. As soon as several independent types are meant to satisfy the same contract without sharing a common ancestor, an interface is the only clean solution, because PHP's single inheritance restriction rules out a second abstract base class anyway.

10. Summary

The difference between an abstract class and an interface is not a naming detail, it is a structural decision with direct consequences for maintainability and coupling. An abstract class can provide constructors, shared state, and concrete methods, making it excellently suited for the Template Method pattern, where a fixed flow structure is meant to be guaranteed. An interface deliberately stays limited to pure contract definition, thereby enabling loose coupling, testability, and the simultaneous satisfaction of several independent contracts by the same class.

PHP's single inheritance restriction makes this decision practically relevant: where more than one behavior dimension is needed, the path goes through interfaces and composition, not through a second abstract base class. Anyone who deliberately combines both concepts, an abstract class for shared base logic, an interface for the externally visible contract, gets code that both avoids duplication and stays loosely coupled.

Abstract Classes vs. Interfaces, the Essentials at a Glance

Abstract class

Constructors, shared state, and concrete methods possible. Only one base class per class allowed.

Interface

Pure contract with no implementation or state. Any number implementable at the same time.

Template Method

final method fixes the flow, abstract methods supply the domain variation.

Decision rule

Shared code: abstract class. Independent types with same contract: interface. Swappable behavior: composition.

11. FAQ: Abstract Classes vs. Interfaces

1Main difference abstract class vs. interface?
Interface: pure contract with no implementation. Abstract class: concrete methods, constructor, and shared state possible, only individual methods stay abstract.
2Can an abstract class have a constructor?
Yes. Subclasses inherit it and must call parent::__construct(). An interface cannot prescribe any constructor logic.
3Why only one abstract class but multiple interfaces?
Avoids the diamond problem of multiple inheritance. Interfaces bring no implementation, so no conflict arises with several at once.
4What is the Template Method pattern?
A final method fixes the set flow, calling abstract hook methods that every subclass implements individually.
5Can both be combined at once?
Yes, a common pattern. The abstract class implements part of the interface methods concretely, the rest stays abstract.
6When is an abstract class wrong?
When it only bundles constants without real shared implementation. Then an interface with constants or an enum fits better.
7When is an interface wrong?
When shared implementation would be needed. Every class would have to write the same logic separately, leading to duplication.
8What is the fragile base class problem?
Deep inheritance chains risk that a change to shared logic unintentionally affects all subclasses.
9When composition instead of an abstract class?
When behavior needs to be swappable at runtime. The class then holds a reference to an interface instead of a fixed inheritance.
10Can an interface contain constants?
Yes, but no properties and no method implementation. For pure constant containers often more appropriate than an abstract class.

Mironsoft

PHP development, architecture review and OOP design

Want a clean class hierarchy in your PHP project?

We help teams use abstract classes and interfaces deliberately, avoid deep inheritance chains, and cleanly separate contracts from shared implementation.

Architecture review

Checking existing class hierarchies for fragile base class risks

Refactoring

Resolving deep inheritance chains in favor of interfaces and composition

Design workshop

Template Method, contracts and composition directly on your own domain model