Sharing behaviour between classes
A shop sells physical products, which need shipping, and digital ones, such as an e-book, which do not. Both have a name and a price, and both can calculate tax. Writing two unrelated classes would duplicate all of that. Inheritance lets one class build on another: the child class extends the parent, receives everything the parent has, and adds or changes only what is different.
<?php
class Product
{
public function __construct(
public string $name,
protected float $price,
) {}
public function priceWithTax(): float
{
return round($this->price * 1.18, 2);
}
public function shippingCost(): float
{
return $this->price >= 500 ? 0 : 49;
}
public function describe(): string
{
return "$this->name: " . $this->priceWithTax() . " + shipping " . $this->shippingCost();
}
}
class DigitalProduct extends Product
{
public function shippingCost(): float
{
return 0;
}
}
$mat = new Product('Yoga Mat', 24.99);
$ebook = new DigitalProduct('SQL Basics e-book', 9.99);
echo $mat->describe() . "\n";
echo $ebook->describe() . "\n";Yoga Mat: 29.49 + shipping 49
SQL Basics e-book: 11.79 + shipping 0DigitalProduct has no constructor, no priceWithTax() and no describe() of its own: it inherited them. It only overrides shippingCost(). Notice that describe(), written in the parent, called $this->shippingCost() and got the child's version for the e-book. The method that runs depends on the object's actual class, which is called polymorphism.
A child object is also an instance of the parent: $ebook instanceof Product is true, so code that expects a Product accepts a DigitalProduct too.
protected
price above is protected: outside code cannot touch it, but the child class can. That is the middle level of visibility:
| Visibility | Inside the class | In child classes | Outside |
|---|---|---|---|
public |
yes | yes | yes |
protected |
yes | yes | no |
private |
yes | no | no |
Use private by default and loosen it to protected only when a child genuinely needs it.
Calling the parent with parent::
An overriding method often wants to extend the parent's behaviour rather than replace it. parent::method() calls the parent's version. The most common case is a constructor that needs extra arguments:
<?php
class Product
{
public function __construct(public string $name, protected float $price) {}
public function label(): string
{
return "$this->name (" . number_format($this->price, 2) . ")";
}
}
class SaleProduct extends Product
{
public function __construct(string $name, float $price, private int $percentOff)
{
parent::__construct($name, $price * (100 - $percentOff) / 100);
}
public function label(): string
{
return parent::label() . " - {$this->percentOff}% off!";
}
}
$sale = new SaleProduct('Noise-Cancelling Headphones', 199.99, 25);
echo $sale->label() . "\n";Noise-Cancelling Headphones (149.99) - 25% off!If a child defines its own constructor, PHP does not call the parent's automatically; call parent::__construct() yourself, usually first.
Method signatures must be compatible
An overriding method must accept at least what the parent accepted and return something compatible. PHP checks this when it loads the class:
<?php
class Product
{
public function shippingCost(): float
{
return 49;
}
}
class Furniture extends Product
{
public function shippingCost(): string
{
return "Quote on request";
}
}Fatal error: Declaration of Furniture::shippingCost(): string must be compatible with Product::shippingCost(): float in /path/to/script.php on line 12That rule (the Liskov substitution principle) is what makes polymorphism safe: any code written for a Product can trust that shippingCost() returns a float, whatever kind of product it gets.
final
final before a class stops it from being extended; before a method, it stops that method from being overridden. Use it when a class or method is not designed to be changed by subclasses, as with the Money class in Constructors, typed and readonly properties:
<?php
class Order
{
final public function total(): float
{
return 100.0;
}
}
class DiscountedOrder extends Order
{
public function total(): float
{
return 90.0;
}
}Fatal error: Cannot override final method Order::total() in /path/to/script.php on line 12When not to inherit
Inheritance is powerful but easy to overuse. Deep hierarchies (Product > PhysicalProduct > ElectronicProduct > Laptop > ...) become hard to change, because a change at the top ripples down. Some guidelines:
- Inherit only when the child really is a kind of the parent (an e-book is a product).
- Keep hierarchies shallow, one or two levels.
- When classes merely share some behaviour, consider interfaces (next lesson), traits (Traits) or giving one object another object to use (composition), for example a
Productthat has aShippingRuleobject.
A PHP class can extend only one parent, which also pushes you towards those alternatives.
Common mistakes
- Forgetting parent::__construct() in a child constructor, so the parent's properties are never set.
- Using private where the child needs access, then getting "Undefined property" errors in the child.
- Changing a method's return type in the child to something incompatible.
- Building deep hierarchies for code reuse alone.
Exercise
Create a class Employee with a name and a monthly salary, and a method annualPay() (12 times the salary). Create Manager extends Employee that adds a yearly bonus through its constructor and overrides annualPay() using parent::annualPay(). Print the annual pay of an employee earning 50,000 a month and a manager earning 90,000 with a 200,000 bonus.
Show one possible answer
<?php
class Employee
{
public function __construct(public string $name, protected float $monthlySalary) {}
public function annualPay(): float
{
return $this->monthlySalary * 12;
}
}
class Manager extends Employee
{
public function __construct(string $name, float $monthlySalary, private float $bonus)
{
parent::__construct($name, $monthlySalary);
}
public function annualPay(): float
{
return parent::annualPay() + $this->bonus;
}
}
foreach ([new Employee('Kavya', 50000), new Manager('Farid', 90000, 200000)] as $person) {
echo $person->name . ": " . number_format($person->annualPay()) . "\n";
}Kavya: 600,000
Farid: 1,280,000Key points
- class Child extends ParentClass gives the child every public and protected property and method of the parent.
- A child can override a method by defining one with the same name; parent::method() calls the original.
- protected members are visible to the class and its children, not to outside code.
- final stops a class from being extended or a method from being overridden. Prefer small hierarchies.