Inheritance, Interfaces and Abstract Classes

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. PHP has single class inheritance, any number of interfaces per class, abstract classes, and traits for horizontal reuse. This page covers the first three and the rules that tie them together.

Inheritance with extends

A class extends at most one parent. It inherits all public and protected members (private ones exist in the object but are not accessible to the child), can add new members and can override methods, properties and constants. parent::method() calls the parent’s implementation, and parent::__construct() must be called explicitly when the child defines its own constructor.

<?php

declare(strict_types=1);

class Item
{
    public function __construct(
        public readonly string $id,
        public readonly string $title,
    ) {}

    public function label(): string
    {
        return "{$this->title} ({$this->id})";
    }

    public function loanDays(): int
    {
        return 21;
    }
}

final class Dvd extends Item
{
    public function __construct(string $id, string $title, public readonly int $minutes)
    {
        parent::__construct($id, $title);       // not called automatically
    }

    #[\Override]                                 // 8.3: assert that this really overrides a parent method
    public function loanDays(): int
    {
        return 7;
    }

    #[\Override]
    public function label(): string
    {
        return 'DVD: ' . parent::label() . ", {$this->minutes} min";
    }
}

$items = [new Item('B-1', 'Kindred'), new Dvd('D-1', 'Solaris', 167)];
foreach ($items as $item) {
    printf("%-40s %d days\n", $item->label(), $item->loanDays());   // polymorphism: the object's class decides
}
Kindred (B-1)                            21 days
DVD: Solaris (D-1), 167 min              7 days

Signature compatibility

An overriding method must be compatible with the method it overrides, so that a child object can be used wherever the parent is expected (the Liskov substitution principle): same or wider parameter types, same or narrower return type, no additional required parameters, and the same or wider visibility. Violations are fatal errors at compile time. Constructors are exempt, unless the parent constructor is abstract or comes from an interface.

#[\Override]

Adding the #[\Override] attribute (8.3) to a method makes PHP check, at compile time, that a parent class or interface really declares a method with that name. Without it, renaming a parent method silently turns the child’s "override" into an unrelated new method. Since PHP 8.5 the attribute works on properties, and upcoming in PHP 8.6 on class constants.

<?php

declare(strict_types=1);

class Notifier
{
    public function send(string $message): void { echo "base: {$message}\n"; }
}

final class EmailNotifier extends Notifier
{
    #[\Override]
    public function send(string $message): void { echo "email: {$message}\n"; }

    // #[\Override]
    // public function sned(string $message): void {}
    // Fatal error: EmailNotifier::sned() has #[\Override] attribute, but no matching parent method exists
}

(new EmailNotifier())->send('Your loan is due');

final

final prevents extension or overriding:

  • final class cannot be extended;

  • final public function cannot be overridden;

  • final public const (8.1) cannot be redefined;

  • final properties and hooks (8.4) cannot be redeclared; promoted constructor properties can be final since 8.5.

Making classes final by default is a widely followed practice: it keeps the public API small, makes refactoring safe, and pushes reuse towards composition. Open a class for extension deliberately, with protected extension points documented as such.

Source: Final Keyword

Abstract classes

An abstract class cannot be instantiated; it may declare abstract methods (signature without body) and, since 8.4, abstract properties, which every concrete subclass must implement. It is the right tool when subclasses share state and implementation, and the parent defines a skeleton (the template method pattern).

<?php

declare(strict_types=1);

abstract class Report
{
    final public function render(): string             // template method: fixed skeleton
    {
        return $this->header() . "\n" . implode("\n", $this->rows()) . "\n";
    }

    protected function header(): string { return strtoupper(static::TITLE); }

    /** @return list<string> */
    abstract protected function rows(): array;          // step implemented by subclasses
}

final class OverdueReport extends Report
{
    public const TITLE = 'Overdue loans';

    public function __construct(private array $loans) {}

    #[\Override]
    protected function rows(): array
    {
        return array_map(fn (array $l): string => "{$l['title']}: {$l['days']} days late", $this->loans);
    }
}

echo (new OverdueReport([['title' => 'Kindred', 'days' => 3]]))->render();
// OVERDUE LOANS
// Kindred: 3 days late

A concrete class that leaves an abstract method unimplemented is a compile-time fatal error ("Class X contains 1 abstract method and must therefore be declared abstract or implement the remaining methods"), and new Report() throws Error: Cannot instantiate abstract class Report.

Interfaces

An interface declares what a class can do, without any implementation: public method signatures, constants and, since 8.4, properties (see Property Hooks). A class can implements several interfaces, and an interface can extends several others. Interfaces are the main tool for dependency inversion: code depends on Clock, Notifier or PSR interfaces, and implementations are swapped in production and tests.

<?php

declare(strict_types=1);

interface Lendable
{
    public const int DEFAULT_DAYS = 21;             // interface constants (typed since 8.3)

    public string $title { get; }                   // 8.4: interface property

    public function loanDays(): int;
}

interface Reservable
{
    public function reserve(string $memberId): void;
}

interface Circulating extends Lendable, Reservable {}   // interfaces can extend several interfaces

final class Book implements Circulating, Stringable
{
    private array $queue = [];

    public function __construct(public readonly string $title) {}

    public function loanDays(): int { return self::DEFAULT_DAYS; }

    public function reserve(string $memberId): void { $this->queue[] = $memberId; }

    public function __toString(): string { return $this->title; }
}

function describe(Lendable&Reservable $item): string        // intersection type of two interfaces
{
    return "{$item->title}: {$item->loanDays()} days";
}

$book = new Book('Kindred');
echo describe($book), "\n";                              // Kindred: 21 days
var_dump($book instanceof Lendable, $book instanceof Circulating);   // bool(true) bool(true)

Interface constants could not be overridden by implementing classes before 8.1; since 8.1 they can, unless declared final.

Abstract class or interface?

Interface Abstract class

State (properties with storage)

no (only property contracts)

yes

Method implementations

no

yes

How many per class

any number

one

Constructor

may declare one (rarely useful)

yes

Visibility of members

public only

any

Use for

a capability or contract (Countable, Notifier)

a partial implementation shared by a family

A common combination is an interface for the contract plus an abstract base class implementing the boring parts: consumers type against the interface, implementers may extend the base class or not.

Covariance and contravariance

PHP checks variance when a method is overridden or an interface method is implemented:

  • Return types are covariant: a child may return a more specific type (Item → Book, iterable → array, mixed → string, anything → never).

  • Parameter types are contravariant: a child may accept a more general type (Book → Item, int → int|string, anything → mixed), or drop the type entirely.

  • Property types are invariant: they must stay identical (except that a read-only get-only interface property can be narrowed, and a write-only set-only one widened).

<?php

declare(strict_types=1);

class Item {}
final class Book extends Item {}

interface ItemFactory
{
    public function create(string $id): Item;
}

final class BookFactory implements ItemFactory
{
    public function create(string|int $id): Book       // wider parameter, narrower return: allowed
    {
        return new Book();
    }
}

interface ItemSink
{
    public function accept(Book $item): void;
}

final class AnythingSink implements ItemSink
{
    public function accept(Item $item): void {}          // contravariant parameter: allowed
}

var_dump((new BookFactory())->create(42) instanceof Book);   // bool(true)
// The reverse (parameter Item -> Book, or return Book -> Item) is a fatal error at compile time.

The predefined interfaces

PHP’s core defines interfaces that hook objects into language features:

Interface Enables

Traversable

foreach (marker interface; implement Iterator or IteratorAggregate instead)

Iterator

foreach with explicit current(), key(), next(), rewind(), valid()

IteratorAggregate

foreach by returning an iterator (often a generator) from getIterator()

ArrayAccess

$obj[$key], isset($obj[$key]), unset($obj[$key])

Countable

count($obj)

Stringable

marker for objects with __toString(); implemented automatically since 8.0

JsonSerializable

custom json_encode() output via jsonSerialize()

Serializable

the legacy serialisation interface: deprecated since 8.1, use __serialize()/__unserialize()

UnitEnum, BackedEnum

implemented automatically by enums (see Enumerations)

Throwable

base of Exception and Error; cannot be implemented directly by user classes

<?php

declare(strict_types=1);

/** @implements IteratorAggregate<string, string> @implements ArrayAccess<string, string> */
final class Catalogue implements IteratorAggregate, ArrayAccess, Countable, JsonSerializable
{
    /** @var array<string, string> isbn => title */
    private array $titles = [];

    public function getIterator(): Generator { yield from $this->titles; }

    public function offsetExists(mixed $isbn): bool { return isset($this->titles[$isbn]); }
    public function offsetGet(mixed $isbn): string { return $this->titles[$isbn] ?? throw new OutOfBoundsException("No {$isbn}"); }
    public function offsetSet(mixed $isbn, mixed $title): void { $this->titles[$isbn] = $title; }
    public function offsetUnset(mixed $isbn): void { unset($this->titles[$isbn]); }

    public function count(): int { return count($this->titles); }

    public function jsonSerialize(): array { return ['count' => count($this), 'titles' => array_values($this->titles)]; }
}

$c = new Catalogue();
$c['978-0-00-000001-1'] = 'Kindred';
$c['978-0-00-000003-5'] = 'Solaris';
unset($c['978-0-00-000003-5']);

foreach ($c as $isbn => $title) {
    echo "{$isbn} {$title}\n";
}
echo count($c), ' ', json_encode($c), "\n";                   // 1 {"count":1,"titles":["Kindred"]}
var_dump(isset($c['nope']));                                  // bool(false)

Composition over inheritance

Inheritance couples a child to every detail of its parent ("fragile base class"), allows only one parent, and fixes behaviour at class-definition time. Composition — an object holding collaborators behind interfaces — avoids all three:

<?php

declare(strict_types=1);

interface FinePolicy { public function fineFor(int $daysLate): int; }

final class FlatFine implements FinePolicy
{
    public function fineFor(int $daysLate): int { return $daysLate * 25; }
}

final class CappedFine implements FinePolicy                 // a decorator: wraps another policy
{
    public function __construct(private FinePolicy $inner, private int $cap) {}
    public function fineFor(int $daysLate): int { return min($this->cap, $this->inner->fineFor($daysLate)); }
}

final class ReturnDesk                                        // composed, not inherited
{
    public function __construct(private FinePolicy $fines) {}
    public function checkIn(int $daysLate): string { return sprintf('Fine: %.2f EUR', $this->fines->fineFor($daysLate) / 100); }
}

echo (new ReturnDesk(new FlatFine()))->checkIn(60), "\n";                     // Fine: 15.00 EUR
echo (new ReturnDesk(new CappedFine(new FlatFine(), 1000)))->checkIn(60), "\n";   // Fine: 10.00 EUR

Use inheritance for genuine "is-a" relationships within a closed family you control, and abstract base classes when the template-method pattern fits; use interfaces and composition for everything else. Laravel’s service container (Service Container, Providers and Facades) is built on wiring interfaces to implementations.

Where older books and tutorials are outdated

  • #[\Override] (8.3, properties in 8.5) is absent from older books.

  • Interface properties and abstract properties (8.4) did not exist; interfaces required getX() methods.

  • Variance: before PHP 7.4, return types had to match exactly; books for PHP 7.0-7.3 state that covariance is not supported.

  • Serializable (Beak ch. 5) is deprecated since 8.1.

  • Final constants (8.1) and typed interface constants (8.3) are new.