PHP Inheritance
Inheritance is the mechanism that lets one PHP class acquire the properties and methods of another, so a family of related classes can share common code instead of duplicating it. A class that inherits is called a subclass (or child class), and the class it inherits from is the superclass (or parent class). Understanding inheritance well — including when not to use it — is essential for writing clean, maintainable object-oriented PHP.
Overview: How Inheritance Works
In PHP, a class declares that it inherits from another using the extends keyword. A child class automatically receives every public and protected property and method defined on its parent, without having to redeclare them. private members, by contrast, belong exclusively to the class that declares them — they exist in memory for every instance of a subclass, but the subclass’s own code cannot reference them directly by name.
Internally, PHP’s Zend Engine builds a class entry for each class the first time it is used: a structure recording its properties and method table, plus a pointer back to its parent’s class entry. When you call a method on an object, the engine checks the object’s own class first; if the method isn’t found there, it walks up the parent chain until it finds a match or reaches the top of the hierarchy without one, producing a fatal “call to undefined method” error. This lookup is resolved and cached per class, which is why method calls through inheritance chains stay fast even in deep hierarchies.
PHP supports only single inheritance for classes — a class can extend exactly one parent — but a class may implement any number of interfaces and use any number of traits, which is how PHP approximates the flexibility of multiple inheritance without the ambiguity of the classic “diamond problem”. Inheritance is also transitive: if C extends B and B extends A, then C inherits from both B and A.
A subclass can override an inherited method by redeclaring it with the same name. When code calls that method on an instance of the subclass, PHP resolves it using the object’s actual runtime class, not the declared type of the variable holding it — this is what makes polymorphism work: a single line like $animal->makeSound() can produce different behavior depending on whether $animal is a Dog or a Cat.
Syntax
The basic shape of an inheriting class looks like this:
<?php
class ParentClass {
protected string $value;
public function __construct(string $value) {
$this->value = $value;
}
public function show(): string {
return $this->value;
}
}
class ChildClass extends ParentClass {
public function shout(): string {
return strtoupper(parent::show());
}
}
$child = new ChildClass("hello");
echo $child->shout();
extends— placed after the child class name, followed by exactly one parent class name.parent::— calls a method (or the constructor) defined on the parent class explicitly, even when the child has overridden it.protected— visibility accessible inside the declaring class and any subclass, but not from outside the hierarchy.abstract— marks a class that cannot be instantiated directly, or a method a subclass is required to implement.final— prevents a class from being extended, or a method from being overridden.static::— refers to the class actually used at runtime (late static binding), useful when a parent method needs to know which subclass called it.
| Visibility | In declaring class | In subclass | Outside the class |
|---|---|---|---|
public |
Yes | Yes | Yes |
protected |
Yes | Yes | No |
private |
Yes | No | No |
Examples
Example 1: Basic Overriding
Every subclass can override a parent method to provide its own behavior, while the parent still defines the shared logic that ties everything together.
<?php
class Animal {
public function __construct(protected string $name) {}
public function makeSound(): string {
return "Some generic sound";
}
public function describe(): string {
return "{$this->name} says: {$this->makeSound()}";
}
}
class Dog extends Animal {
public function makeSound(): string {
return "Woof!";
}
}
class Cat extends Animal {
public function makeSound(): string {
return "Meow!";
}
}
$dog = new Dog("Rex");
$cat = new Cat("Whiskers");
echo $dog->describe() . PHP_EOL;
echo $cat->describe() . PHP_EOL;
Output:
Rex says: Woof!
Whiskers says: Meow!
Both Dog and Cat inherit describe() unchanged, but each overrides makeSound(). Because describe() calls $this->makeSound(), PHP resolves that call at runtime based on whichever class the object actually is — that’s polymorphism in action, and it’s why the shared parent method produces two different results.
Example 2: Calling parent:: and Chaining Constructors
When a subclass defines its own constructor, it usually still needs the parent’s setup logic. Calling parent::__construct() lets it reuse that logic instead of duplicating it.
<?php
class Employee {
public function __construct(
protected string $name,
protected float $baseSalary
) {}
public function getPay(): float {
return $this->baseSalary;
}
public function summary(): string {
return sprintf("%s earns $%.2f", $this->name, $this->getPay());
}
}
class Manager extends Employee {
private float $bonus;
public function __construct(string $name, float $baseSalary, float $bonus) {
parent::__construct($name, $baseSalary);
$this->bonus = $bonus;
}
public function getPay(): float {
return parent::getPay() + $this->bonus;
}
}
$employee = new Employee("Alice", 50000);
$manager = new Manager("Bob", 60000, 15000);
echo $employee->summary() . PHP_EOL;
echo $manager->summary() . PHP_EOL;
Output:
Alice earns $50000.00
Bob earns $75000.00
Manager overrides getPay(), but instead of recomputing the base salary logic, it calls parent::getPay() to get the original value and adds the bonus on top. The constructor does the same thing: it calls parent::__construct() first so $name and $baseSalary are set up exactly like they would be for a plain Employee, then adds its own $bonus field.
Example 3: Abstract Classes, final, and Multi-Level Inheritance
Abstract classes define a contract that every subclass must fulfill, while final methods guarantee that shared logic can never be silently overridden further down the chain.
<?php
abstract class Shape {
abstract public function area(): float;
final public function describe(): string {
return sprintf("%s has area %.2f", static::class, $this->area());
}
}
class Rectangle extends Shape {
public function __construct(
protected float $width,
protected float $height
) {}
public function area(): float {
return $this->width * $this->height;
}
}
class Square extends Rectangle {
public function __construct(float $side) {
parent::__construct($side, $side);
}
}
$rectangle = new Rectangle(4, 5);
$square = new Square(3);
echo $rectangle->describe() . PHP_EOL;
echo $square->describe() . PHP_EOL;
Output:
Rectangle has area 20.00
Square has area 9.00
Shape cannot be instantiated on its own because it has an abstract method; both Rectangle and Square are forced to provide their own area(). describe() is final, so no subclass can change how the message is built, yet it still reports the correct class name for each object because static::class uses late static binding to look up the object’s real runtime class rather than the class where describe() was written.
How It Works Step by Step
Using Example 2, here is what actually happens when $manager->getPay() runs:
- PHP looks up the method table for the object’s actual class,
Manager, not the declared type of the variable holding it. - It finds
getPay()directly onManagerbecause the subclass overrides it, and stops searching there — it never looks atEmployee‘s version unless the code explicitly asks for it. - Inside that overriding method,
parent::getPay()is a statically-resolved call: PHP already knows at compile time thatparentmeansEmployee(the class named inManager‘sextendsclause), so it jumps directly intoEmployee::getPay()using the current$this. Employee::getPay()returns$this->baseSalary, a property that was populated earlier when the constructor ranparent::__construct().- Control returns to
Manager::getPay(), which adds$this->bonusand returns the combined total.
The key idea: normal method calls always start the search from the object’s real class and walk upward only as far as needed, while an explicit parent:: call skips that search entirely and jumps straight to the named ancestor.
Common Mistakes
Mistake 1: Forgetting to Call parent::__construct()
When a child class defines its own constructor, PHP does not automatically call the parent constructor for you. Skip it, and any properties the parent constructor was responsible for setting are left uninitialized.
<?php
class Vehicle {
protected string $type;
public function __construct(string $type) {
$this->type = $type;
}
public function getType(): string {
return $this->type;
}
}
class Car extends Vehicle {
private int $wheels;
public function __construct(int $wheels) {
// Forgot to call parent::__construct(), so $type is never set
$this->wheels = $wheels;
}
}
$car = new Car(4);
echo $car->getType();
Because Car‘s constructor never calls parent::__construct(), the typed property $type is never assigned a value. Calling getType() throws a fatal Error: “Typed property Vehicle::$type must not be accessed before initialization.” The fix is to call the parent constructor explicitly and pass along whatever it needs:
<?php
class Vehicle {
protected string $type;
public function __construct(string $type) {
$this->type = $type;
}
public function getType(): string {
return $this->type;
}
}
class Car extends Vehicle {
private int $wheels;
public function __construct(string $type, int $wheels) {
parent::__construct($type);
$this->wheels = $wheels;
}
}
$car = new Car("sedan", 4);
echo $car->getType();
Mistake 2: Assuming private Properties Are Inherited
private properties belong to the class that declares them. A subclass gets its own copy of the property in memory, but it cannot refer to it by name — PHP treats that as accessing a property it isn’t allowed to see.
<?php
class BankAccount {
private float $balance = 0.0;
public function deposit(float $amount): void {
$this->balance += $amount;
}
public function getBalance(): float {
return $this->balance;
}
}
class SavingsAccount extends BankAccount {
public function addInterest(float $rate): void {
$this->balance += $this->balance * $rate;
}
}
$savings = new SavingsAccount();
$savings->deposit(1000);
$savings->addInterest(0.05);
echo $savings->getBalance();
SavingsAccount::addInterest() tries to touch $balance directly, but since it’s private in BankAccount, PHP throws a fatal Error: “Cannot access private property BankAccount::$balance.” The fix is to loosen the property to protected, which stays hidden from outside code but remains visible to subclasses:
<?php
class BankAccount {
protected float $balance = 0.0;
public function deposit(float $amount): void {
$this->balance += $amount;
}
public function getBalance(): float {
return $this->balance;
}
}
class SavingsAccount extends BankAccount {
public function addInterest(float $rate): void {
$this->balance += $this->balance * $rate;
}
}
$savings = new SavingsAccount();
$savings->deposit(1000);
$savings->addInterest(0.05);
echo $savings->getBalance();
Best Practices
- Use inheritance to model a genuine “is-a” relationship (a
Dogis anAnimal); if the relationship is really “has-a”, prefer composition instead. - Keep inheritance hierarchies shallow — two or three levels is usually enough. Deep chains make it hard to trace where behavior actually comes from.
- Always call
parent::__construct()in an overriding constructor unless you deliberately want to skip the parent’s initialization. - Declare shared properties
protectedonly when subclasses genuinely need direct access; otherwise keep themprivateand expose behavior through methods. - Mark a class or method
finalwhen it isn’t designed to be extended or overridden — it documents intent and prevents accidental misuse later. - Use
abstractclasses to define a contract that subclasses must fulfill, rather than relying on developers to remember to override a method. - Follow the Liskov Substitution Principle: a subclass should be usable anywhere its parent is expected, without surprising callers with different behavior.
- When you need to share behavior across unrelated class hierarchies, reach for a
traitor aninterfaceinstead of forcing an artificial common parent.
Practice Exercises
- Create an abstract class
Paymentwith an abstract methodprocess(): stringand a concrete methodreceipt(float $amount): stringthat usesprocess()in its message. Write two subclasses,CreditCardPaymentandPayPalPayment, each overridingprocess()with its own message, and print a receipt from each. - Build a three-level hierarchy:
Person(with a name),Employee extends Person(adds a salary), andManager extends Employee(adds a count of direct reports). Give each level a method that callsparent::to build on the level below it, then print a full description of aManagerinstance. - Using the
Vehicle/Carclasses from the Common Mistakes section above, write the fixed version ofCar‘s constructor from memory, without looking back at the answer. Then check: which property would stay uninitialized if you forgot theparent::__construct()call?
Summary
extendslets a class (subclass) inherit public and protected properties and methods from another class (superclass).- PHP supports single inheritance for classes, but interfaces and traits can be combined freely to add more shared behavior.
- Overriding a method replaces the parent’s version for that subclass;
parent::lets you still call the original implementation explicitly. - Method calls resolve based on the object’s actual runtime class, not the variable’s declared type — that’s what makes polymorphism work.
abstractclasses and methods define contracts subclasses must implement;finalclasses and methods prevent further extension or overriding.- Forgetting to call
parent::__construct(), and assuming private properties are inherited, are two of the most common inheritance bugs. - Prefer composition over inheritance when the relationship isn’t a true “is-a”, and keep hierarchies shallow.
