Inheritance and interfaces
Extending a class to change part of it, an interface as a promise several classes can keep, and the polymorphism that lets one function accept all of them.
Extending: keeping the parts that fit
class EmailNotifier extends Notifier means everything the parent has — properties, methods, the constructor — arrives with the child, and the child may add to it or replace a method entirely by defining it again. parent::send(...) calls the version being replaced, which is how an override adds to the parent instead of duplicating it.
The visibility keywords from the last chapter are what make this work: a private parent property is invisible to the child, while protected is exactly "mine and my children's". A member that is private can still be used through a public or protected method the parent provides — which is the usual arrangement.
Not every method should be overridable. Marking a method final says the parent has decided this behaviour and a child may not replace it; marking a whole class final says the family ends here. That sounds restrictive, and it is meant to: a class with a small, fixed contract is easier to change than one whose subclasses may depend on any internal detail.
An interface is a promise, not a parent
An interface declares method signatures and nothing else: interface Notifier { public function send(string $message): string; }. A class that says implements Notifier promises to provide exactly those methods. The promise is checked by the engine at the class definition, so a missing method is an error before anything runs.
Inheritance says what something is; an interface says what something can do. A class may extend one parent but implement many interfaces, which is why libraries hand out interfaces: they describe capability without dictating a family tree.
Polymorphism: one call, several shapes
A function typed against an interface accepts anything that implements it: function notifyAll(array $notifiers, string $message) can be handed an email notifier and an SMS notifier in the same array. Each object answers the same call in its own way, and the caller does not care which — the difference is the point.
This is the last big idea in object-oriented PHP, and it is where the design pays off: adding a new way to notify is a new class and nothing else. The function, the loop and the caller stay exactly as they were.
Favour composition
Inheritance is the right tool when the child really is a kind of the parent. A Square is a Rectangle; a LoggedEmailNotifier is an EmailNotifier that adds a line. It is the wrong tool when the child merely uses the parent’s code — a report that needs a formatter is not a kind of formatter, and extending one to borrow its methods makes the family tree lie.
Composition says the same thing without the lie: give the class a property that holds the other object and call its methods. The class can then change that collaborator, test it in isolation, and avoid inheriting methods it never wanted. The interface still describes the contract, so the caller keeps the polymorphism that made the design work.
One promise, three classes, one loop
<?php
declare(strict_types=1);
interface Notifier
{
public function send(string $message): string;
}
class EmailNotifier implements Notifier
{
protected string $address;
public function __construct(string $address)
{
$this->address = $address;
}
public function send(string $message): string
{
return 'Email to ' . $this->address . ': ' . $message;
}
}
class LoggingEmailNotifier extends EmailNotifier
{
public function send(string $message): string
{
// parent:: is the method being replaced, not the one being written.
$line = parent::send($message);
return $line . ' [logged]';
}
}
class SmsNotifier implements Notifier
{
private string $number;
public function __construct(string $number)
{
$this->number = $number;
}
public function send(string $message): string
{
return 'SMS to ' . $this->number . ': ' . $message;
}
}
function notifyAll(array $notifiers, string $message): void
{
foreach ($notifiers as $notifier) {
echo $notifier->send($message) . PHP_EOL;
}
}
notifyAll([
new EmailNotifier('[email protected]'),
new LoggingEmailNotifier('[email protected]'),
new SmsNotifier('+155501'),
], 'The build finished.');Shapes that know their area
Run it and read the table. Then add a `Triangle` class implementing Shape, and confirm it slots into the same array with no change to the function that prints.
An abstract class sits between the two ideas: it cannot be instantiated itself, but it may hold real properties and real methods for its children to use — an interface with a body. Reach for the interface when the contract is all that matters, the abstract class when there is shared work to keep.
Method lookup and contracts
When a method is called, PHP starts at the object’s own class and walks up the parent chain until it finds a definition. An override is simply the first match; parent::method() starts the search one level higher. A final method stops the walk by refusing to be overridden, and an abstract method has no body at all — the child must supply one.
An interface is a contract checked when the class is defined: every declared method must exist, be public, and keep a compatible signature. A class can implement several interfaces but extend only one class, which is why interfaces are the usual way to accept “anything that can do this” without demanding a family tree.
Three inheritance mistakes are easy to miss: a child constructor that never calls parent::__construct(), a child that tries to read a private parent property, and an override that changes a signature incompatibly. Call the parent constructor, use protected when children need the value, and keep parameter and return types compatible.
Try these next
- Add a
Triangleclass implementing the shape interface and put it in the same array. - Make a
Titledinterface with one method and implement it on two unrelated classes. - Override a method, call
parent::inside it, and show both results side by side.
A checkout with two payment types
Run the checkout, then add a `Voucher` class implementing the same interface with a fixed fee. Put it in the array and confirm the checkout function needs no change.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What is parent::send($message) for inside an overriding method?
