PHP::OOP_Mastery
Overview / Session 03 / Session 04
Session 04 of 10  ·  2 hrs + 15 min review

Inheritance &
Method Overriding

⏱ 2-hour session 📋 15-min review ⚠ Requires Sessions 01–03 🗂 4 topics

1 — extends: Building on Existing Classes

Inheritance lets one class inherit all the properties and methods of another. The class being inherited from is called the parent (or base/super) class. The class doing the inheriting is called the child (or derived/sub) class. In PHP, a child class is declared using the extends keyword.

The classic use-case: you have a general concept (Notification) and several specific flavours of it (EmailNotification, SmsNotification, PushNotification). The shared logic lives once in the parent; each child adds only what makes it different.

PHP — extends keyword
// Parent class — shared shape of all notifications
class Notification
{
    public function __construct(
        protected string $recipient,
        protected string $message
    ) {}

    public function getRecipient(): string { return $this->recipient; }
    public function getMessage(): string   { return $this->message; }

    public function send(): void
    {
        echo "Sending to {$this->recipient}: {$this->message}";
    }
}

// Child class — inherits everything, adds its own detail
class EmailNotification extends Notification
{
    public function __construct(
        string $recipient,
        string $message,
        private string $subject  // email-only property
    ) {
        parent::__construct($recipient, $message); // call parent constructor
    }

    public function getSubject(): string { return $this->subject; }
}

class SmsNotification extends Notification
{
    public function __construct(
        string $recipient,
        string $message,
        private string $phoneNumber
    ) {
        parent::__construct($recipient, $message);
    }
}

$email = new EmailNotification('alice', 'Hello!', 'Welcome aboard');
$email->send();        // inherited from Notification ✓
echo $email->getSubject(); // own method ✓

What is inherited?

  • All public and protected properties
  • All public and protected methods
  • private members are not inherited — the child class cannot see them at all
  • Constants (all visibilities in PHP 7.1+)

Single inheritance only

PHP supports only single inheritance — a class can extend exactly one parent. This avoids the "diamond problem" of multiple inheritance. When you need to share behaviour across unrelated class hierarchies, you use Traits (Session 07) or Interfaces (Session 05).

Calling the parent constructor

When a child class defines its own constructor, PHP does not automatically call the parent constructor. You must call parent::__construct() yourself, passing any arguments the parent needs. Forgetting this is a common source of uninitialized property bugs.

2 — Method Overriding & parent::

A child class can override any public or protected method from its parent simply by declaring a method with the same name. The child version replaces the parent version for that class and all further descendants.

PHP — Overriding send() in each child
class Notification
{
    public function __construct(
        protected string $recipient,
        protected string $message
    ) {}

    public function send(): void
    {
        echo "[Base] Notifying {$this->recipient}\n";
    }
}

class EmailNotification extends Notification
{
    public function __construct(
        string $recipient,
        string $message,
        private string $subject
    ) {
        parent::__construct($recipient, $message);
    }

    // Override: completely replace parent behaviour
    public function send(): void
    {
        echo "[Email] To: {$this->recipient} | Subject: {$this->subject}\n";
        echo "Body: {$this->message}\n";
    }
}

class SmsNotification extends Notification
{
    public function __construct(
        string $recipient,
        string $message,
        private string $phone
    ) {
        parent::__construct($recipient, $message);
    }

    // Override: extend parent behaviour with parent::
    public function send(): void
    {
        parent::send();   // run the parent version first
        echo "[SMS] Dispatched to {$this->phone}\n";
    }
}

$notifications = [
    new EmailNotification('alice', 'Hi!', 'Welcome'),
    new SmsNotification('bob',   'Hi!', '+44...'),
];

foreach ($notifications as $n) {
    $n->send();  // each calls its own version — polymorphism in action
}

Rules for valid overrides

PHP enforces a Liskov Substitution Principle-adjacent rule: an overriding method's signature must be compatible with the parent's. Specifically:

  • You cannot narrow a public method to protected or private — visibility can only widen or stay the same
  • Return types must be covariant: the child may return a more specific type (e.g., return User where parent returns Person)
  • Parameters must be contravariant: the child may accept a broader type than the parent
  • You cannot override a method marked final

The final keyword on methods

Mark a method final to prevent any child from overriding it. This is a deliberate design signal: "this is the contract; subclasses must live with it." Marking an entire class final prevents it from being extended at all — useful for value objects and security-sensitive classes.

PHP — final method and final class
class Payment
{
    // Children may not override this — the audit trail must always run
    final public function process(): void
    {
        $this->validate();
        $this->charge();
        $this->audit();   // always runs, cannot be bypassed
    }

    protected function validate(): void { /* ... */ }
    protected function charge(): void   { /* ... */ }
    private function   audit(): void    { /* log the transaction */ }
}

// Immutable value object — nobody should subclass this
final class Uuid
{
    public function __construct(public readonly string $value) {}
}

3 — The Liskov Substitution Principle (LSP)

The LSP — the "L" in SOLID — states: wherever you use a parent class, you must be able to substitute a child class without breaking the program. This sounds obvious but is violated constantly in the wild.

The key test: can every method on the child fulfil the same contract the parent promised? If a child method throws exceptions the parent never throws, silently ignores inputs the parent would act on, or returns surprising types — it violates LSP.

PHP — LSP violation vs correct design
// ─── LSP VIOLATION ────────────────────────────────────────
class Rectangle
{
    public function __construct(
        protected int $width,
        protected int $height
    ) {}

    public function setWidth(int $w): void  { $this->width  = $w; }
    public function setHeight(int $h): void { $this->height = $h; }
    public function area(): int { return $this->width * $this->height; }
}

class Square extends Rectangle
{
    // Square must keep width === height, so it overrides BOTH setters
    public function setWidth(int $w): void
    {
        $this->width = $this->height = $w; // surprise side effect!
    }
    public function setHeight(int $h): void
    {
        $this->width = $this->height = $h; // surprise side effect!
    }
}

// Code that works for Rectangle breaks silently for Square:
function testArea(Rectangle $r): void
{
    $r->setWidth(5);
    $r->setHeight(4);
    assert($r->area() === 20);  // fails for Square! area = 16 (4*4)
}

// ─── CORRECT DESIGN ───────────────────────────────────────
// Make both Rectangle and Square independent — they just share a Shape interface
interface Shape
{
    public function area(): float;
}

class Rectangle implements Shape
{
    public function __construct(
        private readonly int $width,
        private readonly int $height
    ) {}
    public function area(): float { return $this->width * $this->height; }
}

class Square implements Shape
{
    public function __construct(private readonly int $side) {}
    public function area(): float { return $this->side ** 2; }
}

The lesson: inheritance should model an "is-a" relationship that holds under all circumstances. A Square is a Rectangle geometrically, but not behaviourally — the mutability contract breaks. When in doubt, prefer composition and interfaces over inheritance.

4 — When NOT to Use Inheritance

Inheritance is powerful but overused. It creates tight coupling: a change in the parent ripples into every child. Follow the "favour composition over inheritance" principle from the Gang of Four — most problems that look like inheritance problems are actually composition problems.

The "is-a" test

Only extend a class if the child truly is a kind of the parent in every context, not just sometimes. Ask: "Can I swap a Child everywhere a Parent is expected?" If the answer is "mostly, except…" — don't inherit.

  • ✅ SmsNotification is-a Notification — always valid
  • ✅ AdminUser is-a User — makes sense
  • ❌ Square is-a Rectangle — breaks under mutation
  • ❌ Stack should not extend ArrayList just to reuse its array — that exposes push/pop/peek and get-by-index

Composition: wrapping instead of extending

PHP — Composition over inheritance
// ❌ Inheritance approach — Logger "is-a" FileWriter? No.
class Logger extends FileWriter
{
    public function log(string $msg): void
    {
        $this->write("[LOG] {$msg}");   // also inherits 50 file methods we don't want
    }
}

// ✅ Composition — Logger "has-a" writer (could be file, DB, cloud…)
interface Writer
{
    public function write(string $line): void;
}

class FileWriter implements Writer
{
    public function __construct(private string $path) {}

    public function write(string $line): void
    {
        file_put_contents($this->path, $line . PHP_EOL, FILE_APPEND);
    }
}

class Logger
{
    public function __construct(private Writer $writer) {}

    public function log(string $msg): void
    {
        $this->writer->write(date('[H:i:s] ') . $msg);
    }

    public function error(string $msg): void
    {
        $this->log("ERROR: {$msg}");
    }
}

// Swap the writer without touching Logger at all:
$logger = new Logger(new FileWriter('/var/log/app.log'));
$logger->log('Server started');

// In tests, swap for an in-memory writer — no file I/O needed:
class ArrayWriter implements Writer
{
    public array $lines = [];
    public function write(string $line): void { $this->lines[] = $line; }
}

$spy    = new ArrayWriter();
$logger = new Logger($spy);
$logger->log('test message');
assert(str_contains($spy->lines[0], 'test message')); // easy to test!

This composition-based Logger is more flexible, more testable, and has a smaller public surface area than the inheritance version. You'll use this pattern constantly — it's at the heart of how Laravel's IoC container and Symfony's dependency injection work.

15-Minute Review — Session 04

Switch the sidebar timer to review mode. Answer all questions without scrolling up first.

Q1 — When a child class defines its own __construct(), what happens to the parent constructor?

Q2 — Can a child class override a method and make it private if the parent declared it public?

Q3 — The Liskov Substitution Principle says:

Q4 — Marking a method final means:

Q5 — "Favour composition over inheritance" means:

✓ Key concepts checklist

Next: Abstract Classes & Interfaces →