01 · THE IDEAREAD → TRY → LEARN
Why inheritance?
Sometimes two classes share most of their behavior and differ in one detail. Copying the shared code into both classes means every fix has to be made twice. Inheritance lets a child class reuse a parent class and change only what is different.
You declare a child with the extends keyword. The child inherits the public and protected methods, properties and constants of its parent. Unless the child overrides a method, that method keeps its original behavior.
A first example
class Animal {
public function __construct(protected string $name) {}
public function speak(): string { return $this->name . ' makes a sound'; }
public function name(): string { return $this->name; }
}
class Dog extends Animal {
public function speak(): string { return $this->name . ' barks'; }
}
$animal = new Animal('Generic');
$dog = new Dog('Rex');
echo json_encode([$animal->speak(), $dog->speak(), $dog->name()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["Generic makes a sound","Rex barks","Rex"]
Dog never declares a constructor or a name() method, yet both work, because they are inherited. Dog overrides speak(), so Dog objects use the new version. Because $name is protected, the child can read it directly, while outside code cannot.
Overriding without breaking the contract
An override should behave like a drop-in replacement for the parent method. Callers written against the parent should still work when they receive a child. In practice this means:
- keep the same parameters, and the same return type or a compatible one;
- return the same kind of value, with the same meaning;
- do not make visibility stricter. PHP lets a protected method become public, but a public one cannot become private or protected.
A child often wants to add to the parent's result instead of replacing it. Inside a method, parent::methodName() calls the parent's version.
class Receipt {
public function line(int $cents): string { return 'Total: ' . $cents; }
}
class TaxReceipt extends Receipt {
public function line(int $cents): string {
return parent::line($cents + intdiv($cents, 10)) . ' incl. tax';
}
}
echo (new TaxReceipt())->line(250), PHP_EOL;Total: 275 incl. tax
The child adds ten percent, passes the new amount to the parent, then appends text. It still returns a string and still accepts an integer, so the contract is preserved.
Private methods are not inherited
A child cannot access a parent's private methods. If the child declares a private method with the same name, it is a separate method, not an override. Code inside the parent keeps using the parent's own private method.
class Base {
private function secret(): string { return 'base'; }
public function reveal(): string { return $this->secret(); }
}
class Child extends Base {
private function secret(): string { return 'child'; }
}
$child = new Child();
echo json_encode([$child->reveal(), $child instanceof Base], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["base",true]
The result is "base" because reveal() is defined in Base, so it sees Base::secret(). Also notice that $child instanceof Base is true: a child object is also a parent object.
Other rules to remember
- Unless autoloading is used, declare the parent class before the child.
- You cannot override a normal read-write property with a
readonlyone, or the other way around. - Constructors are an exception to the visibility rule: a child may make a constructor more restricted.
Common mistakes
- Changing the return type or meaning in an override, so callers of the parent break.
- Copying the parent's logic into the child instead of calling
parent::method(). - Expecting a private parent method to be replaced by a same-named child method.
- Using
privatefor a property the child needs; useprotectedwhen subclasses should read it.
Exercise contract
The starter file defines a Ticket class with price(): int, which returns its base price in cents, and an empty StudentTicket subclass. Override price() in StudentTicket so it applies a 20 percent discount by calling parent::price(). Use integer math, intdiv(parent::price() * 80, 100), so the result rounds down and stays an int. Do not change the ticketPrice(string $type, int $basePrice): int function: it creates a StudentTicket when $type is 'student', otherwise a plain Ticket. Your results are checked on returned integers, including small prices like 1 cent, where rounding matters.
Recap
Use extends to reuse a class and override only what differs. Keep overrides compatible with the parent's contract, call parent::method() to build on existing behavior, and remember that private methods are not part of inheritance.
Classes
- Extend a base class
- Override a method while preserving its contract