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.
// 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
publicandprotectedproperties - All
publicandprotectedmethods privatemembers 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.
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
publicmethod toprotectedorprivate— visibility can only widen or stay the same - Return types must be covariant: the child may return a more specific type (e.g., return
Userwhere parent returnsPerson) - 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.
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.
// ─── 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.
- ✅
SmsNotificationis-aNotification— always valid - ✅
AdminUseris-aUser— makes sense - ❌
Squareis-aRectangle— breaks under mutation - ❌
Stackshould not extendArrayListjust to reuse its array — that exposes push/pop/peek and get-by-index
Composition: wrapping instead of extending
// ❌ 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: