PHP Inheritance Explained — Extend Classes, Override Methods and Avoid Classic Mistakes
Every non-trivial PHP application has classes that share behaviour. A BlogPost and a NewsArticle both have a title, a publish date, and an author. An AdminUser and a RegularUser both log in, have a name, and belong to an account. Without inheritance, you'd copy-paste that shared logic into every class — and the moment requirements change, you'd have to update it in five places instead of one. That's not a PHP problem, that's a maintenance nightmare waiting to happen.
Inheritance solves the DRY (Don't Repeat Yourself) problem at the class level. It lets you define common behaviour once in a parent (or base) class and have multiple child (or derived) classes automatically gain that behaviour. Child classes can then specialise — adding new properties and methods, or overriding existing ones to behave differently. The hierarchy reads like plain English: a Car IS-A Vehicle. An AdminUser IS-A User.
By the end of this article you'll understand not just the syntax of PHP inheritance but when to reach for it versus other tools like interfaces or traits. You'll be able to build a realistic multi-level class hierarchy, safely override parent methods, call parent behaviour with parent::, and sidestep the classic errors that trip up intermediate developers in code reviews and interviews.
The `extends` Keyword — Building Your First Parent-Child Relationship
The word extends is the entire engine of PHP inheritance. When ClassB extends ClassA, ClassB automatically gets every public and protected property and method that ClassA defines. Private members stay private to the parent — the child can't see them directly.
Let's model something real: a content publishing platform. Every piece of content — whether it's an article, a video, or a podcast — shares a common core: a title, an author, a publication date, and the ability to render a summary. We put all of that in a Content parent class. Then Article extends it and adds article-specific things like a word count.
Notice how the child class constructor calls parent::__construct(). That's critical. The parent's constructor sets up the shared properties. If the child doesn't call it, those properties never get initialised and you'll get null where you expect a string. Always call the parent constructor when the parent has one — unless you have a very deliberate reason not to.
<?php // Parent class — defines everything ALL content types share class Content { protected string $title; // protected = visible to child classes protected string $author; protected string $publishedAt; public function __construct(string $title, string $author, string $publishedAt) { $this->title = $title; $this->author = $author; $this->publishedAt = $publishedAt; } // Any child class can call or override this public function getSummaryLine(): string { return "'{$this->title}' by {$this->author} — published {$this->publishedAt}"; } public function getAuthor(): string { return $this->author; } } // Child class — inherits everything above, adds word count class Article extends Content { private int $wordCount; public function __construct( string $title, string $author, string $publishedAt, int $wordCount ) { // MUST call parent constructor to initialise $title, $author, $publishedAt parent::__construct($title, $author, $publishedAt); // Now handle Article-specific setup $this->wordCount = $wordCount; } public function getReadingTime(): string { // Average adult reads ~238 words per minute $minutes = (int) ceil($this->wordCount / 238); return "{$minutes} min read"; } } // --- Usage --- $article = new Article( title: 'Understanding PHP Closures', author: 'Dana Walsh', publishedAt: '2024-11-01', wordCount: 1190 ); // Method inherited directly from Content — no copy-paste needed echo $article->getSummaryLine() . PHP_EOL; // Method defined on Article itself echo $article->getReadingTime() . PHP_EOL; // Works because getAuthor() is inherited echo 'Author: ' . $article->getAuthor() . PHP_EOL;
5 min read
Author: Dana Walsh
Method Overriding — Teaching a Child to Do Things Differently
Inheritance gives you a starting point. Method overriding lets child classes customise that starting point. When a child class defines a method with the same name as a parent method, the child's version wins for objects of that type. This is the mechanism behind polymorphism — one interface, different behaviours.
Back to our publishing platform: every Content type has a getSummaryLine(). But a Podcast should also show the episode duration. We override the method on Podcast to add that extra information. The trick is that we can still call the parent's version of the method using parent::getSummaryLine() and build on top of it — instead of rewriting the whole thing.
This is the real power move. You're not throwing away the parent's logic; you're extending it. Think of it as: 'do everything the parent does, and then do this extra thing'. The alternative — copy-pasting the parent's summary format into the child — means two places to update when the format changes. Use parent::methodName() to reuse rather than repeat.
<?php class Content { protected string $title; protected string $author; protected string $publishedAt; public function __construct(string $title, string $author, string $publishedAt) { $this->title = $title; $this->author = $author; $this->publishedAt = $publishedAt; } // Base version — returns the standard summary public function getSummaryLine(): string { return "'{$this->title}' by {$this->author} — published {$this->publishedAt}"; } } class Article extends Content { private int $wordCount; public function __construct(string $title, string $author, string $publishedAt, int $wordCount) { parent::__construct($title, $author, $publishedAt); $this->wordCount = $wordCount; } // Override: article summary adds word count public function getSummaryLine(): string { // Reuse the parent's format, then append article-specific info $baseSummary = parent::getSummaryLine(); return "{$baseSummary} [{$this->wordCount} words]"; } } class Podcast extends Content { private int $durationInSeconds; public function __construct(string $title, string $author, string $publishedAt, int $durationInSeconds) { parent::__construct($title, $author, $publishedAt); $this->durationInSeconds = $durationInSeconds; } // Override: podcast summary adds formatted duration public function getSummaryLine(): string { $baseSummary = parent::getSummaryLine(); // reuse, don't rewrite $formattedMinutes = floor($this->durationInSeconds / 60); $formattedSeconds = $this->durationInSeconds % 60; $duration = sprintf('%d:%02d', $formattedMinutes, $formattedSeconds); return "{$baseSummary} [Duration: {$duration}]"; } } // --- Polymorphism in action --- // Both are Content objects — same method call, different output $contentItems = [ new Article('Mastering SQL Joins', 'Priya Mehta', '2024-10-15', 2040), new Podcast('The PHP Roundtable Ep.12', 'Leo Fischer', '2024-10-22', 2754), ]; foreach ($contentItems as $item) { // PHP calls the RIGHT getSummaryLine() based on the actual object type echo $item->getSummaryLine() . PHP_EOL; }
'The PHP Roundtable Ep.12' by Leo Fischer — published 2024-10-22 [Duration: 45:54]
Access Modifiers and Multi-Level Inheritance — What Children Can Actually See
PHP has three access modifiers that interact with inheritance in very specific ways. public members are visible everywhere — inside the class, in child classes, and from outside code. protected members are visible inside the class AND in any child class, but not from external code. private members are visible only inside the class that defines them — not even in child classes.
This distinction matters a lot in practice. When you mark a parent property private, child classes can't read or write it directly. They have to go through a getter or setter. Mark it protected and the child class can access it like its own property. The rule of thumb: start with private. Promote to protected only when a child class genuinely needs direct access. Never go straight to public for properties.
Multi-level inheritance works naturally in PHP — Child extends Parent, Grandchild extends Child. But keep the chain shallow. A three-level hierarchy is usually fine. Going deeper than that is often a sign you need composition (using other objects) instead of more inheritance.
<?php class User { private string $passwordHash; // ONLY accessible inside User — not even children protected string $email; // Accessible in User and all child classes public string $displayName; // Accessible from anywhere public function __construct(string $displayName, string $email, string $plainPassword) { $this->displayName = $displayName; $this->email = $email; $this->passwordHash = password_hash($plainPassword, PASSWORD_BCRYPT); // stored hashed } public function verifyPassword(string $plainPassword): bool { // Only User itself handles the private passwordHash — child classes can't touch it return password_verify($plainPassword, $this->passwordHash); } protected function getFormattedEmail(): string { // Protected helper — child classes can reuse this logic return strtolower(trim($this->email)); } } class AdminUser extends User { private string $adminRole; public function __construct(string $displayName, string $email, string $plainPassword, string $adminRole) { parent::__construct($displayName, $email, $plainPassword); $this->adminRole = $adminRole; } public function getAdminSummary(): string { // Can access protected $email via the inherited protected method $formattedEmail = $this->getFormattedEmail(); // Can access public $displayName directly return "{$this->displayName} ({$formattedEmail}) — Role: {$this->adminRole}"; // This would cause a fatal error — private property of parent: // return $this->passwordHash; // ERROR: Cannot access private property } } // Multi-level: SuperAdmin extends AdminUser extends User class SuperAdmin extends AdminUser { public function __construct(string $displayName, string $email, string $plainPassword) { // SuperAdmin always has the 'super' role — enforced here parent::__construct($displayName, $email, $plainPassword, 'super'); } public function impersonateUser(string $targetEmail): string { return "{$this->displayName} is now acting as: {$targetEmail}"; } } // --- Usage --- $admin = new AdminUser('Sara Okafor', 'Sara@Example.com', 'securePass99', 'editor'); $superAdmin = new SuperAdmin('Dev Master', 'dev@example.com', 'rootPass123'); echo $admin->getAdminSummary() . PHP_EOL; echo $superAdmin->getAdminSummary() . PHP_EOL; echo $superAdmin->impersonateUser('customer@example.com') . PHP_EOL; // Password verification still works — the private hash logic lives in the base class $loginOk = $admin->verifyPassword('securePass99'); echo 'Login valid: ' . ($loginOk ? 'Yes' : 'No') . PHP_EOL;
Dev Master (dev@example.com) — Role: super
Dev Master is now acting as: customer@example.com
Login valid: Yes
Abstract Classes — Enforcing a Contract Without Finishing the Blueprint
Sometimes a parent class is so general that it doesn't make sense to instantiate it directly. You'd never create a raw Content object on your platform — you'd always create an Article, a Podcast, or a Video. Abstract classes formalise this pattern.
An abstract class does two things. First, it can't be instantiated with new — PHP throws a fatal error if you try. Second, it can define abstract methods — method signatures with no body — that every concrete child class must implement. It's a contract: 'if you extend me, you promise to provide these methods'.
This is more powerful than a normal parent class because it gives you compile-time enforcement instead of runtime surprises. You don't find out a child class is missing a method when a user hits a bug in production — PHP tells you immediately when the class is loaded. Use abstract classes when you have shared logic that all children should inherit and a set of behaviours that each child must implement in its own way.
<?php // Abstract class — provides shared logic, demands child-specific implementations abstract class Content { protected string $title; protected string $author; protected string $publishedAt; public function __construct(string $title, string $author, string $publishedAt) { $this->title = $title; $this->author = $author; $this->publishedAt = $publishedAt; } // Concrete shared method — all children inherit this as-is public function getByline(): string { return "By {$this->author} on {$this->publishedAt}"; } // Abstract method — every child MUST provide its own implementation // PHP enforces this at class-load time, not at runtime abstract public function getMediaType(): string; abstract public function renderPreview(): string; } // Concrete child — MUST implement both abstract methods or PHP throws a fatal error class VideoContent extends Content { private int $durationInSeconds; private string $thumbnailUrl; public function __construct( string $title, string $author, string $publishedAt, int $durationInSeconds, string $thumbnailUrl ) { parent::__construct($title, $author, $publishedAt); $this->durationInSeconds = $durationInSeconds; $this->thumbnailUrl = $thumbnailUrl; } // Fulfils the abstract contract public function getMediaType(): string { return 'video'; } public function renderPreview(): string { $mins = floor($this->durationInSeconds / 60); $secs = $this->durationInSeconds % 60; return "[VIDEO] {$this->title} ({$mins}m {$secs}s) — thumb: {$this->thumbnailUrl}"; } } class WrittenArticle extends Content { private int $wordCount; public function __construct(string $title, string $author, string $publishedAt, int $wordCount) { parent::__construct($title, $author, $publishedAt); $this->wordCount = $wordCount; } public function getMediaType(): string { return 'article'; } public function renderPreview(): string { return "[ARTICLE] {$this->title} — {$this->wordCount} words"; } } // --- This would throw: Cannot instantiate abstract class Content --- // $raw = new Content('Test', 'Someone', '2024-01-01'); $video = new VideoContent('Async PHP Deep Dive', 'Yuki Tanaka', '2024-09-10', 1845, '/thumbs/async-php.jpg'); $article = new WrittenArticle('PHP 8.3 New Features', 'Amara Diallo', '2024-09-18', 3200); $contentFeed = [$video, $article]; foreach ($contentFeed as $piece) { // getByline() is inherited — same for all echo $piece->getByline() . PHP_EOL; // renderPreview() — each type handles this its own way echo $piece->renderPreview() . PHP_EOL; echo str_repeat('-', 50) . PHP_EOL; }
[VIDEO] Async PHP Deep Dive (30m 45s) — thumb: /thumbs/async-php.jpg
--------------------------------------------------
By Amara Diallo on 2024-09-18
[ARTICLE] PHP 8.3 New Features — 3200 words
--------------------------------------------------
| Feature | Abstract Class | Interface |
|---|---|---|
| Can be instantiated with `new` | No — fatal error | No — fatal error |
| Can contain implemented methods | Yes — full method bodies allowed | No — only signatures (PHP 8+ allows default interface methods) |
| Can contain properties | Yes — regular class properties | Only constants |
| Access modifiers on methods | public, protected, or private | Always public |
| How many can a class extend/implement | One only (single inheritance) | Many — `implements A, B, C` |
| Best used when... | Children share real logic + must implement some methods | You need a pure capability contract across unrelated classes |
| Keyword used | `extends` | `implements` |
🎯 Key Takeaways
extendscopies all public and protected members to the child — butprivatemembers stay locked inside the parent class only.- Always call
parent::__construct()in a child constructor when the parent has one — skipping it leaves shared properties uninitialised. - Method overriding +
parent::methodName()is the sweet spot: you get specialised behaviour without duplicating the base logic. - Abstract classes enforce a contract at class-load time — far safer than hoping a developer 'remembers' to implement a required method.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Not calling parent::__construct() in the child — The child's constructor silently skips parent setup, so properties like $title or $author are never initialised. You'll get empty values or typed property errors like 'Typed property Content::$title must not be accessed before initialisation'. Fix: Always make
parent::__construct(...)the first line of your child constructor whenever the parent defines one. - ✕Mistake 2: Changing a method's return type or parameter signature when overriding — PHP will throw a fatal error if your overriding method signature is incompatible with the parent's (stricter rules apply in PHP 7.4+ with typed properties and PHP 8+ with union types). For example, if the parent declares
public function getPrice(): floatand the child declarespublic function getPrice(): string, PHP rejects it. Fix: Match the parent method's signature exactly, or use covariant return types (a narrower type that extends the parent's return type), which PHP allows. - ✕Mistake 3: Making everything
protectedinstead ofprivate'just in case' — This seems harmless but it tightly couples every child class to internal implementation details of the parent. When you refactor the parent's internals, you break all child classes that accessed those protected properties directly. Fix: Default toprivate. Promote toprotectedonly when a child class has a genuine, deliberate need for direct access. Use getters and setters as the interface between parent and child where possible.
Interview Questions on This Topic
- QWhat's the difference between `private` and `protected` in PHP inheritance, and how would you decide which one to use for a parent class property?
- QCan you explain what `parent::` does and give a scenario where you'd use it in a method override rather than completely rewriting the parent's logic?
- QIf a child class doesn't define a constructor, what happens when you call `new ChildClass(...)`? And what happens to the parent's constructor arguments in that case?
Frequently Asked Questions
Can a PHP class extend more than one class at once?
No. PHP only supports single inheritance — a class can extend exactly one parent class. If you need a class to combine behaviour from multiple sources, use interfaces (which a class can implement many of) or traits (which let you mix reusable method sets into any class).
What does `parent::` actually refer to in PHP?
parent:: refers to the immediate parent class in the inheritance chain. It's most commonly used as parent::__construct() to run the parent's constructor, or parent::methodName() inside an overriding method to call the parent's version of that method before or after your custom logic.
What's the difference between an abstract class and a regular class in PHP?
An abstract class cannot be instantiated directly with new — it only exists to be extended. It can also declare abstract methods, which are method signatures with no body that every non-abstract child class must implement. A regular class has no such restrictions and can be used directly.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.