Home PHP PHP Inheritance: The Protected Property That Broke the CMS
Intermediate 8 min · March 06, 2026

PHP Inheritance: The Protected Property That Broke the CMS

After a refactor, previews showed empty titles and undefined author errors from protected property rename.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Solid grasp of fundamentals
  • Comfortable reading code examples
  • Basic production concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • PHP inheritance uses extends to create parent-child class relationships.
  • Child classes inherit all public and protected properties and methods automatically.
  • Method overriding lets children redefine behavior; use parent:: to call the parent version.
  • Access control: private stays hidden, protected allows child access.
  • Abstract classes enforce method contracts without instantiation.
  • Production pitfall: forgetting parent::__construct() leaves parent properties uninitialized.
✦ Definition~90s read
What is Inheritance in PHP?

PHP inheritance is a language mechanism where a child class (extends) inherits properties and methods from a parent class, enabling code reuse and establishing an "is-a" relationship. It exists to model hierarchical taxonomies—like Admin being a User—but is frequently misused as a code-organization tool, leading to brittle, tightly-coupled systems.

Imagine a generic 'Vehicle' blueprint that defines things every vehicle has — wheels, an engine, the ability to move.

In PHP, inheritance is single (one parent per child), supports method overriding, and respects access modifiers (public, protected, private) that control visibility across levels. The problem it solves is avoiding duplication when classes share behavior, but it introduces a rigid dependency: changes to a parent ripple through all children, which is why production codebases often limit inheritance depth to 2-3 levels.

In the ecosystem, inheritance competes with composition (via traits, interfaces, and dependency injection) for structuring code. Frameworks like Laravel use inheritance sparingly—e.g., Eloquent models extend a base Model class—but rely more on traits for horizontal reuse.

Abstract classes (abstract) enforce partial contracts: they define method signatures without implementation, forcing children to complete them, which is useful for framework hooks (e.g., Symfony's AbstractController). However, when you need to share behavior across unrelated classes, or when a child only needs a subset of a parent's functionality, inheritance is the wrong choice—composition with interfaces or traits avoids the fragile base class problem.

Real-world breakage often occurs when a protected property in a parent class is changed, silently breaking child classes that depend on it, as seen in CMS plugins that crash after core updates.

Plain-English First

Imagine a generic 'Vehicle' blueprint that defines things every vehicle has — wheels, an engine, the ability to move. Now you want to build a 'Car' and a 'Motorcycle'. Instead of writing the wheels-and-engine stuff twice, you say 'start with the Vehicle blueprint and add Car-specific stuff on top'. That's inheritance. The Car inherits everything from Vehicle automatically and only has to describe what makes it uniquely a Car.

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.

Why Inheritance Is Not a Code-Organization Tool

Inheritance in PHP is a mechanism where a child class automatically receives the properties and methods of a parent class. The core mechanic is the extends keyword: class Child extends Parent. This creates an is-a relationship — the child is a specialized version of the parent. The child can override methods, add new ones, and access protected members of the parent. It is not a shortcut for code reuse; it is a contract about behavior and type compatibility.

In practice, inheritance gives you three things: method inheritance (child can use or override parent methods), property inheritance (child inherits public and protected properties), and type compatibility (a child instance can be used wherever the parent type is expected). The critical nuance is that protected properties are visible to child classes but invisible to the outside world. This sounds safe, but it creates tight coupling: a change in the parent's protected property can silently break every child. The parent cannot refactor its internal state without auditing all subclasses.

Use inheritance when you have a genuine taxonomic hierarchy — for example, a PaymentGateway base class with StripeGateway and PayPalGateway children that share a charge() contract. Do not use it to share utility methods or to avoid copy-pasting configuration. In real systems, deep inheritance trees (depth > 3) are a maintenance liability. Prefer composition over inheritance for most code-sharing needs; reserve inheritance for polymorphic dispatch where the child truly is a specialized version of the parent.

⚠ Protected ≠ Encapsulated
Protected properties are visible to every subclass, making them part of the public API of the class hierarchy — refactoring them breaks children.
📊 Production Insight
A CMS team extended a base Content class with Article and Page. They added a protected $status property to the parent. Later, a developer changed $status from a string to an enum. Every child that directly accessed $status broke silently — no compile-time check, no deprecation warning. Rule: never expose protected properties; use protected methods with getters/setters instead.
🎯 Key Takeaway
Inheritance is a type contract, not a code-sharing shortcut.
Protected properties create hidden coupling that refactoring cannot detect.
Limit hierarchy depth to 2 or 3; beyond that, prefer composition.
inheritance-php Inheritance Hierarchy Layers in a CMS Tiered access modifiers and their visibility scope Abstract Base abstract class CoreModule | protected $config Concrete Parent class BasePlugin extends CoreM | protected $config override Child Class class ThemePlugin extends Base | public method uses $config Grandchild class CustomTheme extends Them | modifies $config directly Client Code CMS runtime | unexpected $config state THECODEFORGE.IO
thecodeforge.io
Inheritance Php

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.

ContentInheritance.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?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;
Output
'Understanding PHP Closures' by Dana Walsh — published 2024-11-01
5 min read
Author: Dana Walsh
⚠ Watch Out: Forgetting parent::__construct()
If your parent class sets up properties in its constructor and your child class defines its own constructor without calling parent::__construct(...), those parent properties will never be set. You'll get empty strings, nulls, or typed property errors at runtime. Always explicitly call parent::__construct() with the required arguments as the first line of the child constructor.
📊 Production Insight
A missing parent constructor call is a silent bug — no error until a property is accessed.
Static analysis tools like PHPStan can detect this; add them to CI.
Rule: if the child has a constructor, always call parent::__construct() unless you intentionally skip parent initialisation.
🎯 Key Takeaway
extends copies public and protected members, not private ones.
Always call parent::__construct() in a child constructor.
Skipping it leaves parent properties uninitialised.

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.

MethodOverriding.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?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;
}
Output
'Mastering SQL Joins' by Priya Mehta — published 2024-10-15 [2040 words]
'The PHP Roundtable Ep.12' by Leo Fischer — published 2024-10-22 [Duration: 45:54]
💡Pro Tip: Use the `final` Keyword to Lock Methods
If a parent method should never be overridden — maybe it's a security check or a core calculation — mark it final public function methodName(). PHP will throw a fatal error if any child class tries to override it. This is a great way to enforce contracts in shared libraries or team codebases.
📊 Production Insight
Overriding without calling parent:: means you own the full method — double the maintenance surface.
When the parent's logic needs to change, you must update every overriding child.
Rule: call parent::methodName() at the start or end of your override to keep a single point of truth.
🎯 Key Takeaway
Override to specialise, not to replace.
parent::methodName() reuses logic.
Skipping it duplicates maintenance.
inheritance-php THECODEFORGE.IO Inheritance Hierarchy Layers in a CMS Layered stack showing multi-level class dependencies Abstract Base AbstractController | AbstractModel Concrete Parent BaseController | BaseModel Child Layer AdminController | UserController Grandchild Layer SuperAdminController | EditorController CMS Modules AuthModule | ContentModule | ConfigModule THECODEFORGE.IO
thecodeforge.io
Inheritance Php

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.

AccessModifiers.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?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;
Output
Sara Okafor (sara@example.com) — Role: editor
Dev Master (dev@example.com) — Role: super
Dev Master is now acting as: customer@example.com
Login valid: Yes
🔥Interview Gold: private vs protected
A very common interview question is 'what's the difference between private and protected?' The answer interviewers want: both restrict external access, but protected is designed for inheritance — it explicitly says 'child classes are expected to use this'. private says 'this is an internal implementation detail, not even children should depend on it'. Choosing between them is a design decision, not just a syntax choice.
📊 Production Insight
Deep inheritance chains (4+ levels) cause unexpected behaviour when a property changes at the top.
A child class three levels down may break if a middle class introduces a conflicting property.
Rule: keep hierarchies shallow; use composition after 2-3 levels.
🎯 Key Takeaway
Default to private; promote to protected deliberately.
Deep trees are fragile.
Shallow hierarchies survive refactoring.

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.

AbstractContent.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?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;
}
Output
By Yuki Tanaka on 2024-09-10
[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
--------------------------------------------------
🔥Abstract Class vs Interface — The Quick Rule
Use an abstract class when child classes should share real implemented code from the parent. Use an interface when you just want to guarantee a set of method signatures with no shared implementation. A class can only extend one abstract class but can implement multiple interfaces. Many real-world designs use both together.
📊 Production Insight
Adding an abstract method later breaks every existing child class — they all must implement it.
Plan for evolution: start with concrete methods, only abstract when you're sure the contract is stable.
Rule: abstract classes are a big commitment — prefer interfaces for loosely coupled contracts.
🎯 Key Takeaway
Abstract classes enforce contracts at load time.
They share logic AND mandate methods.
Use interfaces for purely contractual guarantees.

Inheritance vs Composition — When Extending a Class Is the Wrong Choice

The 'favor composition over inheritance' principle isn't just a design-pattern mantra — it's a practical survival tactic. Inheritance creates tight coupling: a change to the parent can silently break every child. Composition (delegation) keeps classes independent.

Consider a ReadOnlyList. You might be tempted to extend ArrayList and throw exceptions on add(). That violates Liskov substitution — you'd break code that expects a normal ArrayList. The right approach: compose an internal ArrayList and expose only the methods you want.

This principle also applies to deep hierarchies. If you find yourself extending a class three levels deep just to reuse one method, ask: is a true 'is-a' relationship here? If not, extract that method into a service class and pass it in via constructor injection. Composition wins where inheritance adds fragility.

InheritanceVsComposition.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

// BAD: Inheritance violates Liskov substitution
class ReadOnlyListInherited extends SplFixedArray
{
    public function offsetSet($index, $newval): void
    {
        throw new \BadMethodCallException('Read-only list cannot be modified');
    }
}

// GOOD: Composition delegates only desired behaviour
class ReadOnlyList
{
    private array $items;

    public function __construct(array $items)
    {
        $this->items = $items;
    }

    public function get(int $index): mixed
    {
        return $this->items[$index] ?? null;
    }

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

    // Expose only read methods
}

// Usage
$readOnly = new ReadOnlyList(['a', 'b', 'c']);
echo $readOnly->get(1) . PHP_EOL; // b
// No add, no set — safe by design
Output
b
Mental Model
IS-A vs HAS-A
Inheritance models an 'is-a' relationship; composition models a 'has-a' relationship.
  • IS-A: Car IS-A Vehicle — true subtype polymorphism.
  • HAS-A: Car HAS-A Engine — reuse with delegation.
  • If you can't honestly say 'Child IS-A Parent', use composition.
  • Composition is looser coupling; inheritance leaks implementation.
📊 Production Insight
A 5-level inheritance tree in a production codebase is a disaster waiting to happen.
One protected property change at the top cascades to every child.
Rule: after 2-3 levels, extract shared logic into a service and inject it.
🎯 Key Takeaway
Favor composition over inheritance.
Use inheritance only for true subtype polymorphism.
Everything else is delegation.
Inheritance vs Composition Decision Tree
IfTrue IS-A relationship (e.g., Car IS-A Vehicle)
UseUse inheritance
IfNeed to share code but no subtype relationship
UseUse composition (delegation)
IfChild class needs to expose the same interface as parent
UseUse interface + composition
IfChild should be substitutable for parent (LSP)
UseInheritance is appropriate

Type Hinting, Return Types, and LSP — Why Your Inheritance Is Probably Broken

Most inheritance failures aren’t syntax errors. They’re violations of the Liskov Substitution Principle (LSP). You’ve seen the mess: a child class that throws different exceptions, accepts null where the parent didn’t, or returns a string when the parent promised an int.

PHP isn’t pedantic by default. Your parent has a method public function calculate(float $tax): float. Your child overrides it with public function calculate($tax): string. PHP won’t scream at runtime unless you’ve typed the child correctly. But your downstream code will explode when a string shows up.

Covariance and contravariance aren’t just spec jargon. In PHP 8+, return types can be narrower in the child (covariant), and parameter types can be wider (contravariant). That means your Cat class can return a PersianCat where Animal returns Animal. But if your child accepts array where the parent accepts iterable, you’re asking for runtime hell.

Rule: The child must behave exactly like the parent from the outside. If you can’t swap the parent for the child without breaking tests, your inheritance is a lie.

LSPBroken.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// io.thecodeforge — php tutorial

// This will fail at runtime because the child violates LSP

class PaymentGateway
{
    public function charge(float $amount, string $currency): string
    {
        return sprintf('Charged %.2f %s', $amount, $currency);
    }
}

class StripeGateway extends PaymentGateway
{
    // Violates contravariance: parameter $currency is now nullable
    // Violates covariance: return type changed from string to int
    public function charge(float $amount, ?string $currency = null): int
    {
        $currency ??= 'USD';
        return (int) parent::charge($amount, $currency);
    }
}

$processor = new PaymentGateway();
echo $processor->charge(100, 'EUR'); // Works

$processor = new StripeGateway();
echo $processor->charge(50, 'GBP'); // Returns int, not string
Output
Fatal error: Declaration of StripeGateway::charge(float $amount, ?string $currency): int must be compatible with PaymentGateway::charge(float $amount, string $currency): string
⚠ Production Trap:
PHP 8.1+ will throw a fatal error if your child method signature isn’t compatible with the parent — even without strict types. Always declare strict types and explicitly typehint everything.
🎯 Key Takeaway
If parent and child can’t be swapped without breaking tests, your inheritance violates Liskov Substitution.

The `final` Keyword — The Underrated Power of Saying "No"

You’ve been burned. Some junior on your team extended your PaymentProcessor class, overrode validateTransaction(), and introduced a security hole. Now you have to audit every subclass.

Enter final. It’s not a punishment. It’s a contract. When you mark a class as final, you’re saying: "This class has been designed. Don’t extend it. If you need different behavior, compose it." When you mark a method as final, you’re saying: "This logic must not be overridden. Period."

PHP’s final works in two scopes
  • final class Logger: Can’t be extended at all.
  • final public function process(): Can’t be overridden by any subclass.

Why does this matter? Because open classes get abused. Libraries get hacked. Someone will "improve" your flush_cache() method by adding a sleep(2). Use final to protect invariants — logging, encryption, database connections, payment flows.

Senior shortcut: Mark every class final by default. Remove it only when you have a real, tested need for inheritance. That single habit will kill hours of debugging and security reviews.

FinalClass.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — php tutorial

// Using final to prevent dangerous extension

final class CryptoService
{
    public function __construct(private string $key) {}

    final public function encrypt(string $plain): string
    {
        return openssl_encrypt($plain, 'aes-256-gcm', $this->key, 0, $iv = random_bytes(12), $tag);
    }

    final public function decrypt(string $cipher): string
    {
        $plain = openssl_decrypt($cipher, 'aes-256-gcm', $this->key, 0, $iv, $tag);
        return $plain !== false ? $plain : throw new \RuntimeException('Decryption failed');
    }
}

// This will fail
class EvilCrypto extends CryptoService {}

echo (new CryptoService('my-32-byte-secret-key-1234567890'))->encrypt('sensitive data');
Output
Fatal error: Class EvilCrypto cannot extend final class CryptoService
💡Senior Shortcut:
Run final on every class and method by default. Remove only when you write a test proving you need polymorphism. Your future self will thank you when a desperate developer tries to "fix" your core logic.
🎯 Key Takeaway
final prevents inheritance abuse and security holes. Default to final, extend only by design.

Readonly Classes and Inheritance (PHP 8.2)

PHP 8.2 introduced readonly classes, which enforce that all properties are readonly and no dynamic properties are allowed. When combined with inheritance, readonly classes impose strict rules: a readonly class can only extend another readonly class, and its properties cannot be overridden in child classes. This prevents the common pitfall where a child class accidentally mutates a parent's immutable state. For example:

```php readonly class BaseConfig { public function __construct( public string $apiKey, public string $endpoint ) {} }

// This will cause a fatal error: // class ExtendedConfig extends BaseConfig { // public string $apiKey = 'override'; // Error: Cannot override readonly property // } ```

Readonly classes are ideal for value objects and configuration DTOs where immutability is critical. In a CMS context, you might use a readonly class for a PageMetadata object that should never change after creation. However, be cautious: inheritance with readonly classes is restrictive—you cannot add non-readonly properties in child classes, which may force you to use composition instead. This aligns with the principle of favoring composition over inheritance when extensibility is needed.

readonly-inheritance.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

readonly class BaseConfig {
    public function __construct(
        public string $apiKey,
        public string $endpoint
    ) {}
}

// Valid: extending a readonly class with another readonly class
readonly class ExtendedConfig extends BaseConfig {
    public function __construct(
        string $apiKey,
        string $endpoint,
        public int $timeout
    ) {
        parent::__construct($apiKey, $endpoint);
    }
}

// Invalid: attempting to override a readonly property
// class BrokenConfig extends BaseConfig {
//     public string $apiKey = 'override'; // Fatal error
// }

echo "Readonly inheritance works as expected.\n";
⚠ Readonly Inheritance Restrictions
📊 Production Insight
Use readonly classes for immutable value objects like configuration or metadata. If you anticipate needing mutable extensions, prefer composition over inheritance to avoid breaking the readonly contract.
🎯 Key Takeaway
Readonly classes enforce immutability across the inheritance chain, but they restrict property overriding and require all child classes to also be readonly.

Final Constants in PHP 8.1

PHP 8.1 introduced the ability to declare class constants as final, preventing child classes from overriding them. This is particularly useful in inheritance hierarchies where a constant represents a fixed value that should remain consistent across all subclasses. For example:

class PaymentGateway {
    final public const VERSION = '1.0';
}
class StripeGateway extends PaymentGateway {
    // This will cause a fatal error:
    // public const VERSION = '2.0'; // Error: Cannot override final constant
}

Final constants help enforce design contracts. In a CMS, you might have a BaseContent class with a final constant MAX_TITLE_LENGTH = 255 to ensure all content types respect the database schema. Without final, a child class could inadvertently change this value, leading to data integrity issues. However, use final constants sparingly—they are best for truly invariant values like API versions, configuration keys, or business rules that must not change. Overusing them can make your hierarchy rigid and harder to extend.

final-constants.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class PaymentGateway {
    final public const VERSION = '1.0';
    public const API_URL = 'https://api.example.com';
}

class StripeGateway extends PaymentGateway {
    // Allowed: overriding a non-final constant
    public const API_URL = 'https://api.stripe.com';

    // Fatal error: cannot override final constant
    // public const VERSION = '2.0';
}

echo StripeGateway::API_URL; // https://api.stripe.com
echo StripeGateway::VERSION; // 1.0
?>

// Output:
// https://api.stripe.com
// 1.0
💡When to Use Final Constants
📊 Production Insight
Identify constants that represent fixed business rules or technical limits and mark them as final. This prevents accidental overrides that could cause subtle bugs in production.
🎯 Key Takeaway
Final constants in PHP 8.1 prevent child classes from overriding critical constant values, ensuring consistency in inheritance hierarchies.
Inheritance vs Composition in PHP CMS Trade-offs between extending classes and composing objects Inheritance Composition Code Reuse Reuse via extends, but rigid hierarchy Reuse via delegation, flexible assembly Encapsulation Protected properties expose internals Private properties, only public interfac Change Impact Parent changes break all children Component changes isolated via interface Testing Hard to mock parent dependencies Easy to mock individual components LSP Compliance Often violated by method overrides Naturally follows Liskov Substitution THECODEFORGE.IO
thecodeforge.io
Inheritance Php

Trait Composition vs Inheritance Decision Guide

Choosing between trait composition and inheritance is a common dilemma in PHP. Both allow code reuse, but they serve different purposes. Inheritance models an "is-a" relationship (e.g., a Car is a Vehicle), while traits provide horizontal reuse of methods without establishing a parent-child relationship. Use this guide to decide:

Inheritance** is appropriate when
  • You have a clear hierarchical relationship (e.g., AdminUser extends User).
  • You need polymorphic behavior (e.g., a method that works on any User).
  • You want to enforce a contract via abstract methods.
Traits** are better when
  • You need to share behavior across unrelated classes (e.g., Loggable trait used by both User and Order).
  • You want to avoid deep inheritance chains that become brittle.
  • You need to compose multiple behaviors (PHP supports multiple traits but single inheritance).

Example: Instead of creating a LoggableEntity base class that forces all entities to extend it, use a Loggable trait:

trait Loggable {
    public function log(string $message): void {
        echo "[LOG] " . $message;
    }
}
class User {
    use Loggable;
}
class Order {
    use Loggable;
}

In a CMS, you might have a Publishable trait for content types that can be published, avoiding a forced inheritance from a PublishableContent base class. Remember: traits cannot define properties that conflict with class properties, and they can lead to naming collisions. Use traits for small, focused behaviors, and inheritance for core domain modeling.

trait-vs-inheritance.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

// Inheritance: is-a relationship
class Vehicle {
    public function startEngine(): void {
        echo "Engine started.\n";
    }
}

class Car extends Vehicle {}

// Trait: horizontal reuse
trait Loggable {
    public function log(string $message): void {
        echo "[LOG] $message\n";
    }
}

class User {
    use Loggable;
}

class Order {
    use Loggable;
}

$car = new Car();
$car->startEngine(); // Engine started.

$user = new User();
$user->log('User created.'); // [LOG] User created.

$order = new Order();
$order->log('Order placed.'); // [LOG] Order placed.
?>

// Output:
// Engine started.
// [LOG] User created.
// [LOG] Order placed.
🔥Decision Guide Summary
📊 Production Insight
In large codebases, prefer traits over deep inheritance to reduce coupling. However, be mindful of trait conflicts—use insteadof and as operators to resolve them, and keep traits focused on a single responsibility.
🎯 Key Takeaway
Traits enable horizontal code reuse without the coupling of inheritance, making them ideal for sharing behavior across unrelated classes.
● Production incidentPOST-MORTEMseverity: high

The Protected Property That Broke the CMS — A PHP Inheritance Failure

Symptom
After a refactor, all article, video, and podcast previews showed empty titles and 'undefined author' errors.
Assumption
Since the property was protected, all child classes should still have access — no change needed.
Root cause
A new developer renamed the protected property from $title to $contentTitle in the parent class, but forgot that child classes directly accessed $this->title. The change was not caught because no child class overrides were updated.
Fix
Enforce getter methods for all protected properties. Use final on core getters to prevent overrides. Audit all child class references after any parent property change.
Key lesson
  • Protected properties are part of the public API of inheritance — treat them as contract, not implementation detail.
  • Always use getter/setter methods for any property that child classes might access.
  • Add CI checks that detect direct property access in child classes (static analysis).
Production debug guideCommon symptoms and actions when your class hierarchy isn't behaving as expected.4 entries
Symptom · 01
Child class property is null even though parent constructor sets it
Fix
Check if child constructor calls parent::__construct(...). If child defines own constructor and doesn't call parent's, parent properties remain uninitialized.
Symptom · 02
Method override not being called when object is used as parent type
Fix
Verify the method name and signature exactly match parent's. PHP only overrides if the method signature is compatible (same or covariant return type).
Symptom · 03
Fatal error: Cannot override final method
Fix
Look for final keyword in parent method declaration. Either remove final from parent (if safe) or redesign to not require override.
Symptom · 04
Cannot access property from child class
Fix
Check visibility modifier in parent. If private, child cannot access. Change to protected if child needs direct access, or add getter method.
★ PHP Inheritance Debug Cheat SheetQuick commands and checks when inheritance breaks in production.
Parent constructor not executed
Immediate action
Check child constructor for `parent::__construct(...)` call.
Commands
Add `echo 'parent constructor called';` inside parent __construct to verify.
Use `(new ReflectionClass($child))->getParentClass()->getConstructor()` to inspect.
Fix now
Call parent::__construct(...) as first line in child constructor.
Wrong method implementation being executed+
Immediate action
Use `var_dump($object)` to confirm object's class.
Commands
`echo get_class($object);` to see actual class.
`if(method_exists($object, 'methodName')) { echo 'exists'; }` to verify inheritance.
Fix now
Ensure method signature matches exactly — same parameter count and types.
Child class cannot access parent property+
Immediate action
Check property visibility: is it `private`?
Commands
`(new ReflectionClass('ParentClass'))->getProperty('prop')->isPrivate()`
Change property to `protected` or add a getter method.
Fix now
If property is private, add a protected getter method in parent.
FeatureAbstract ClassInterface
Can be instantiated with newNo — fatal errorNo — fatal error
Can contain implemented methodsYes — full method bodies allowedNo — only signatures (PHP 8+ allows default interface methods)
Can contain propertiesYes — regular class propertiesOnly constants
Access modifiers on methodspublic, protected, or privateAlways public
How many can a class extend/implementOne only (single inheritance)Many — implements A, B, C
Best used when...Children share real logic + must implement some methodsYou need a pure capability contract across unrelated classes
Keyword usedextendsimplements
⚙ Quick Reference
10 commands from this guide
FileCommand / CodePurpose
ContentInheritance.phpclass ContentThe `extends` Keyword
MethodOverriding.phpclass ContentMethod Overriding
AccessModifiers.phpclass UserAccess Modifiers and Multi-Level Inheritance
AbstractContent.phpabstract class ContentAbstract Classes
InheritanceVsComposition.phpclass ReadOnlyListInherited extends SplFixedArrayInheritance vs Composition
LSPBroken.phpclass PaymentGatewayType Hinting, Return Types, and LSP
FinalClass.phpfinal class CryptoServiceThe `final` Keyword
readonly-inheritance.phpreadonly class BaseConfig {Readonly Classes and Inheritance (PHP 8.2)
final-constants.phpclass PaymentGateway {Final Constants in PHP 8.1
trait-vs-inheritance.phpclass Vehicle {Trait Composition vs Inheritance Decision Guide

Key takeaways

1
extends copies all public and protected members to the child
but private members stay locked inside the parent class only.
2
Always call parent::__construct() in a child constructor when the parent has one
skipping it leaves shared properties uninitialised.
3
Method overriding + parent::methodName() is the sweet spot
you get specialised behaviour without duplicating the base logic.
4
Abstract classes enforce a contract at class-load time
far safer than hoping a developer 'remembers' to implement a required method.
5
Favor composition over inheritance unless you have a true IS-A relationship.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What's the difference between `private` and `protected` in PHP inheritan...
Q02SENIOR
Can you explain what `parent::` does and give a scenario where you'd use...
Q03JUNIOR
If a child class doesn't define a constructor, what happens when you cal...
Q01 of 03SENIOR

What's the difference between `private` and `protected` in PHP inheritance, and how would you decide which one to use for a parent class property?

ANSWER
private restricts visibility to the class that defines it — even child classes cannot access private properties or methods directly. protected allows access within the class and all its descendants. The decision rule: start with private. Only promote to protected if a child class has a deliberate need to access the member directly. If the property is merely needed for internal state that children shouldn't depend on, keep it private and provide a protected getter if necessary. Never use protected as a default 'just in case' — it creates tight coupling.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can a PHP class extend more than one class at once?
02
What does `parent::` actually refer to in PHP?
03
What's the difference between an abstract class and a regular class in PHP?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's OOP in PHP. Mark it forged?

8 min read · try the examples if you haven't

Previous
OOP in PHP — Classes and Objects
2 / 7 · OOP in PHP
Next
Interfaces and Abstract Classes in PHP