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
Source: Object Inheritance
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.
Source: Signature compatibility rules
#[\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');
Source: The Override attribute
final
final prevents extension or overriding:
-
final classcannot be extended; -
final public functioncannot be overridden; -
final public const(8.1) cannot be redefined; -
finalproperties and hooks (8.4) cannot be redeclared; promoted constructor properties can befinalsince 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.
Source: Class Abstraction
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.
Source: Object Interfaces
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 ( |
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-onlyset-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.
Source: Covariance and Contravariance
The predefined interfaces
PHP’s core defines interfaces that hook objects into language features:
| Interface | Enables |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
marker for objects with |
|
custom |
|
the legacy serialisation interface: deprecated since 8.1, use |
|
implemented automatically by enums (see Enumerations) |
|
base of |
<?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
Source: Object Interfaces
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.