---
title: 'Inheritance and Abstract Classes'
source: 'https://academia.sh/en/courses/php-object-model/inheritance-and-abstract-classes'
course: 'Object-Oriented PHP and Standards'
language: en
updated: '2026-10-21T05:03:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that an abstract class cannot be instantiated and a final method cannot be overridden'
    - "Measures that an internal change in the parent class silently shifts the subclass's counter"
    - 'Verifies that building the same counter with composition keeps the change in place'
---

# Inheritance and Abstract Classes

A change inside a parent class shifting the result of a subclass implicitly, an added abstract method stopping the subclasses, and the same counter built with composition leaving the same change in place.

In Classes and Objects a name inside a class changed and the consumer was unaffected, because visibility stood in between. With inheritance nothing stands in between: the subclass calls the parent's methods and protected properties directly. So the question changes. When the inside of the parent class changes, what does the subclass experience?

## Building a Subclass

Inheritance is a class taking over the properties and methods of another class. The class that takes over is the subclass, the class taken from is the parent class. In PHP the `extends` keyword declares this. The `protected` visibility opens a member to the class itself and its subclasses and keeps it closed to the outside. If a subclass writes a method under the same name, it overrides the method; the parent's version is still reachable with `parent::`.

```php
<?php
// ItemList.php — a parent class that carries a protected property and three methods
class ItemList
{
    protected array $items = [];

    public function add(string $item): void
    {
        $this->items[] = $item;
    }

    public function addAll(array $items): void
    {
        foreach ($items as $item) {
            $this->add($item);
        }
    }

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

```php
<?php
// CountingList.php — a subclass that overrides the add() method
class CountingList extends ItemList
{
    public int $counter = 0;

    public function add(string $item): void
    {
        $this->counter++;
        parent::add($item);
    }
}
```

```php
<?php
// list_usage.php — the subclass uses the parent's method; the protected property is closed from outside
require 'ItemList.php';
require 'CountingList.php';

$list = new CountingList();
$list->addAll(['a', 'b', 'c']);
echo $list->counter, ' ', $list->count(), "\n";

try {
    echo count($list->items);
} catch (Error $error) {
    echo get_class($error), "\n";
}
```

```
3 3
Error
```

**OM5.** `addAll()` was not written in the subclass, it was inherited; `add()` was called for each item and `CountingList::add()` raised the counter on every call: `3 3`. The `protected` property was open to the subclass but stayed closed to the outside. What stands out here is that the subclass's counter depends on an internal detail of the parent: `addAll()` calling `add()` one by one.

## Abstract Classes and final

An abstract class cannot be instantiated and can carry methods without a body; subclasses must write those methods. The class below declares one method without a body and supplies one method with a body.

```php
<?php
// Report.php — a method without a body and a method with a body that depends on it
abstract class Report
{
    abstract public function title(): string;

    public function render(): string
    {
        return strtoupper($this->title());
    }
}
```

```php
<?php
// Daily.php — a concrete subclass that writes the abstract method
class Daily extends Report
{
    public function title(): string
    {
        return 'daily';
    }
}
```

```php
<?php
// report_usage.php — the concrete subclass is built, the abstract class cannot be
require 'Report.php';
require 'Daily.php';

echo (new Daily())->render(), "\n";

try {
    new Report();
} catch (Error $error) {
    echo get_class($error), "\n";
}
```

```
DAILY
Error
```

If a method is marked `final`, subclasses cannot override it. The file stops as soon as it is loaded. The script below writes two classes, one of which overrides a `final` method, into a file and prints the exit code.

```bash
# final.sh — a class that overrides a final method cannot be loaded
cat > Fixed.php <<'PHP'
<?php
class Base
{
    final public function identity(): string { return 'base'; }
}
class Child extends Base
{
    public function identity(): string { return 'child'; }
}
PHP
php -d display_errors=0 Fixed.php; echo "exit=$?"
```

```
exit=255
```

**OM6.** `new Daily()` ran and `Report::render()` used the subclass's `title()` method. `new Report()` and the file that overrides a `final` method, however, gave a signal: the first threw `Error`, the second stopped with exit 255. The abstract class and `final` are the two ends of inheritance: one imposes an obligation to write on the subclass, the other takes away the authority to write.

## The Parent's Constructor

If a subclass writes its own constructor, the parent's constructor does not run on its own. For the state the parent builds to be ready, `parent::__construct()` is called explicitly. If the call is forgotten, the parent's properties are left unset.

```php
<?php
// Person.php — a parent constructor, a subclass that calls it and one that forgets
class Person
{
    public function __construct(protected string $name) {}

    public function greet(): string
    {
        return 'hello ' . $this->name;
    }
}

class Employee extends Person
{
    public function __construct(string $name, private int $number)
    {
        parent::__construct($name);
    }

    public function card(): string
    {
        return $this->greet() . ' #' . $this->number;
    }
}

class Forgetful extends Person
{
    public function __construct(private int $number) {}

    public function read(): string
    {
        return $this->name;
    }
}

echo (new Employee('ali', 7))->card(), "\n";

try {
    echo (new Forgetful(1))->read();
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
hello ali #7
Error
```

Because `Employee` called the parent's constructor, `name` was set. Because `Forgetful` did not, `name` was never written and reading it raised `Error`. In inheritance the constructor chain works only if it is written.

## Signatures When Overriding and a final Class

When a subclass overrides a method, it has to stay compatible with the parent's signature; code that holds the parent in a typed variable must be able to make the same call with the subclass. Adding a required parameter breaks that compatibility. If a class is not to be extended at all, it is marked `final`; the file that extends it stops as soon as it is loaded.

```bash
# signature.sh — adding a required parameter and extending a final class make the declaration impossible
cat > Signature.php <<'PHP'
<?php
class Base { public function add(string $item): void {} }
class Child extends Base { public function add(string $item, int $quantity): void {} }
PHP
cat > ClosedClass.php <<'PHP'
<?php
final class Closed {}
class Opener extends Closed {}
PHP
php -d display_errors=0 Signature.php; echo "signature exit=$?"
php -d display_errors=0 ClosedClass.php; echo "final exit=$?"
```

```
signature exit=255
final exit=255
```

In both cases the error came not during execution but during the class declaration. A `final` method takes one behavior away from subclasses, a `final` class takes all of it.

## When Inheritance Fits

Inheritance fits when there is an "is a" relationship between two classes: an `Employee` is a `Person`, a `Daily` is a `Report`. Inheritance built only to share code ties the subclass to all of the parent's methods and to every future change in them. This is exactly what is measured throughout this lesson: when the inside of the parent changes, the subclasses tied to it are affected. If the shared code describes a capability rather than a relationship, an interface or a trait creates fewer ties; they are the next two lessons.

## The Measurement Core

The measurement in this lesson uses the file `reach.php`; this is the core explained line by line in Classes and Objects, and it is rewritten in every lesson's working directory.

```php
<?php
// reach.php — helpers that measure where a change reaches

function write_files(string $dir, array $files): void
{
    foreach ($files as $name => $content) {
        if ($content === null) {
            continue; // null: the file was deleted
        }
        $path = "$dir/$name";
        if (!is_dir(dirname($path))) {
            mkdir(dirname($path), 0777, true);
        }
        file_put_contents($path, $content);
    }
}

function read_files(string $dir): array
{
    $files = [];
    $walker = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($walker as $file) {
        $name = substr($file->getPathname(), strlen($dir) + 1);
        $files[$name] = file_get_contents($file->getPathname());
    }
    return $files;
}

function run(string $dir): array
{
    $command = 'cd ' . escapeshellarg($dir) . ' && php -d display_errors=0 main.php 2>/dev/null';
    exec($command, $lines, $code);
    return [$code, implode("\n", $lines)];
}

function measure(string $name, array $base, array $change, array $seams = []): string
{
    if (array_key_exists('main.php', $change)) {
        throw new LogicException("$name: the consumer is not edited");
    }
    write_files("measure/$name/before", $base);
    write_files("measure/$name/after", array_merge($base, $change));
    [$codeBefore, $outputBefore] = run("measure/$name/before");
    [$codeAfter, $outputAfter] = run("measure/$name/after");
    if ($codeBefore !== 0) {
        throw new LogicException("$name: the baseline did not run");
    }
    $reach = match (true) {
        $codeAfter !== 0 => 'breaking',
        $outputBefore !== $outputAfter => 'implicit',
        array_intersect(array_keys($change), $seams) !== [] => 'at-seam',
        default => 'in-place',
    };
    printf(
        "%-18s files=%d  exit=%-3d output %-7s -> %s\n",
        $name,
        count($change),
        $codeAfter,
        $outputBefore === $outputAfter ? 'same' : 'changed',
        $reach
    );
    return $reach;
}
```

## The Parent's Inside Changes

The first observation uses the `ItemList` and `CountingList` pair above. The parent's `addAll()` method no longer calls `add()` one by one; it merges the array in one go with `array_merge()`. The subclass and the consumer `main.php` stay the same. The contract the parent offers to the outside does not change either: the two methods carry the same name and signature.

```php
<?php
// inheritance.php — the parent no longer calls the single-add method in bulk add
require_once 'reach.php';

measure('parent-class', [
    'item_list.php' => <<<'PHP'
        <?php
        class ItemList
        {
            protected array $items = [];
            public function add(string $item): void { $this->items[] = $item; }
            public function addAll(array $items): void
            {
                foreach ($items as $item) {
                    $this->add($item);
                }
            }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'item_list.php';
        class CountingList extends ItemList
        {
            public int $counter = 0;
            public function add(string $item): void { $this->counter++; parent::add($item); }
        }
        $list = new CountingList();
        $list->addAll(['a', 'b', 'c']);
        echo $list->counter, "\n";
        PHP,
], [
    'item_list.php' => <<<'PHP'
        <?php
        class ItemList
        {
            protected array $items = [];
            public function add(string $item): void { $this->items[] = $item; }
            public function addAll(array $items): void
            {
                $this->items = array_merge($this->items, $items);
            }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/parent-class/$stage");
    echo $stage, ': ', $output, "\n";
}
```

```
parent-class       files=1  exit=0   output changed -> implicit
before: 3
after: 0
```

**OM7.** The exit code is 0, but the output changed: the counter went from `3` to `0`. The subclass had overridden only the `add()` method; since the parent no longer calls `add()`, the subclass's method never ran. There is no error and no warning in the consumer, only a wrong number. This is the fragile base class problem: the subclass trusted not the parent's contract but its inner workings, and the inner workings changed. The lesson's reach is **implicit**. In Object-Oriented Java what was measured was which type chooses the method that gets called; here what is measured is whether a change in the parent reaches the subclass, and it did.

## Adding an Abstract Method

The opposing measurement moves the change from the inside of the parent to its contract. A new `abstract` method is added to an abstract class. A subclass written earlier, like `Daily`, does not know this method.

```php
<?php
// abstract.php — a new abstract method is added to the abstract parent
require_once 'reach.php';

measure('abstract-method', [
    'report.php' => <<<'PHP'
        <?php
        abstract class Report
        {
            abstract public function title(): string;
            public function render(): string { return strtoupper($this->title()); }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'report.php';
        class Daily extends Report
        {
            public function title(): string { return 'daily'; }
        }
        echo (new Daily())->render(), "\n";
        PHP,
], [
    'report.php' => <<<'PHP'
        <?php
        abstract class Report
        {
            abstract public function title(): string;
            abstract public function footer(): string;
            public function render(): string { return strtoupper($this->title()); }
        }
        PHP,
]);
```

```
abstract-method    files=1  exit=255 output changed -> breaking
```

**OM8.** The same consumer, the same subclass, the same single-file change; this time the consumer stopped. An abstract method tells a subclass "you must write this", and when the subclass does not write it, the class cannot be declared. The exit code is not 0, the reach is **breaking**. Because the contract has no body, the language catches the change; there was no contract that would catch an internal change in the parent.

## The Bounding Measurement

Is the cause of the implicit result inheritance itself, or the counter being tied to the inside of the parent? When the same counter is built with composition, the measurement separates the two. In composition the class does not extend another class, it carries an instance of it and counts in its own two methods. The change made in the parent is the same as before: `addAll()` no longer calls `add()`.

```php
<?php
// composition.php — the counter is built with composition, not inheritance; the same parent change
require_once 'reach.php';

measure('composition', [
    'item_list.php' => <<<'PHP'
        <?php
        class ItemList
        {
            protected array $items = [];
            public function add(string $item): void { $this->items[] = $item; }
            public function addAll(array $items): void
            {
                foreach ($items as $item) {
                    $this->add($item);
                }
            }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'item_list.php';
        class CountingList
        {
            public int $counter = 0;
            private ItemList $inner;
            public function __construct() { $this->inner = new ItemList(); }
            public function add(string $item): void { $this->counter++; $this->inner->add($item); }
            public function addAll(array $items): void { $this->counter += count($items); $this->inner->addAll($items); }
        }
        $list = new CountingList();
        $list->addAll(['a', 'b', 'c']);
        echo $list->counter, "\n";
        PHP,
], [
    'item_list.php' => <<<'PHP'
        <?php
        class ItemList
        {
            protected array $items = [];
            public function add(string $item): void { $this->items[] = $item; }
            public function addAll(array $items): void
            {
                $this->items = array_merge($this->items, $items);
            }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/composition/$stage");
    echo $stage, ': ', $output, "\n";
}
```

```
composition        files=1  exit=0   output same    -> in-place
before: 3
after: 3
```

**OM9.** The parent's change is the same, the consumer is the same; the result stayed `3`. In composition the counter does not look at which method of the parent calls which: it counts in its own two methods and reaches the parent only through the contract, that is, the names and signatures of the two methods. The same internal change was implicit in inheritance and stayed in place in composition. The distinction measured is one of design, not of language: inheritance ties the subclass to the inside of the parent, composition does not. The cost comes from the same place: `CountingList` is no longer an `ItemList`.

## Summary

- `extends` builds a subclass, `protected` opens a member to the subclass while keeping it closed to the outside, and `parent::` reaches the parent's method.
- An abstract class cannot be instantiated and obliges subclasses to write the methods without a body; a `final` method cannot be overridden.
- When the parent stopped calling `add()` in bulk add, the subclass's counter dropped from `3` to `0`; the reach of this lesson is **implicit**.
- Adding an `abstract` method to the abstract class stopped the same consumer, a **breaking** reach; because the contract has no body, the language catches it.
- When the counter was built with composition, the same parent change stayed in place: `3`.

## Next Step

The abstract method brought a contract without a body, and adding a method to the contract stopped the subclass. The Interfaces lesson separates this contract from inheritance: when a method is added to an interface, what do the classes that implement it experience?
