Inheritance: extends, Overriding and parent::
Inheritance lets one class build on another. The child gets everything public and protected from the parent, and can add to it or replace parts of it. The keyword is extends, and PHP allows only one parent per class — no multiple inheritance.
The test for whether inheritance is appropriate is the phrase "is a". A PdfReport is a Report. A Manager is an Employee. If the sentence sounds wrong, inheritance is the wrong tool — a Car is not an Engine, it has one, and that is composition rather than inheritance.
A child can override a parent method by declaring one with the same name. Inside the override, parent::methodName() calls the version it replaced, which is how you extend behaviour rather than discarding it. This matters most in constructors: if a child defines __construct(), the parent's constructor is not run automatically, so you must call parent::__construct() yourself. Forgetting is a common source of objects that are half-initialised in ways that only show up later.
There is a rule about signatures. An override must remain usable everywhere the parent was, so it may not demand more than the parent did. It can accept a broader parameter type and return a narrower one, but narrowing a parameter or broadening a return produces a fatal error. That is PHP enforcing a genuine design principle rather than being difficult: code holding a Report must keep working when handed a PdfReport.
protected finally becomes useful here. It is the visibility that says "my subclasses may use this, but nobody else", which is exactly right for helper methods and internal state that a child legitimately needs.
<?php
class Notification
{
public function __construct(
protected string $recipient,
protected string $message,
) {}
public function send(): bool
{
return $this->deliver($this->format());
}
protected function format(): string
{
return $this->message;
}
protected function deliver(string $body): bool
{
error_log("To {$this->recipient}: $body");
return true;
}
}
class SmsNotification extends Notification
{
public function __construct(string $recipient, string $message, private string $senderId)
{
parent::__construct($recipient, $message); // do not forget this
}
// Override, but reuse the parent's work
protected function format(): string
{
$body = parent::format();
return mb_substr("[{$this->senderId}] $body", 0, 160); // SMS limit
}
}
$sms = new SmsNotification('9876500000', 'Your fee receipt is ready.', 'CAMPUS');
$sms->send();
// A child is usable anywhere the parent is
function dispatch(Notification $n): void { $n->send(); }
dispatch($sms);
var_dump($sms instanceof Notification); // true finalstops inheritance where you want it stopped:final class Xcannot be extended, andfinal public function y()cannot be overridden. Marking a method final is a way of saying "this behaviour is part of the guarantee, do not change it in a subclass".
Abstract Classes: a Half-Built Template
An abstract class is one that cannot be instantiated on its own. It exists to be extended. It can hold complete methods that all its children share, and it can declare abstract methods that have no body at all — a demand that every child must supply that method itself.
This is the right shape whenever several classes share most of their behaviour but differ in one step. A report generator that fetches data, formats it and outputs it may do the fetching and the wrapping identically for every format, while the actual rendering differs between PDF, CSV and HTML. Put the shared work in the abstract parent and leave render() abstract.
The value is that the compiler enforces it. A child that forgets to implement an abstract method is a fatal error at the moment the class is loaded, not a mystery at runtime six weeks later. And because the parent cannot be instantiated, there is no way to accidentally use the incomplete version.
Abstract classes can have constructors, properties, private helpers and constants — everything a normal class has. The only differences are that you cannot say new on one, and that it may contain abstract methods.
The pattern shown below, where a parent method defines the sequence of steps and calls abstract methods for the parts that vary, is common enough to have a name: the template method pattern. It is one of the clearest and most useful applications of inheritance.
<?php
abstract class Report
{
public function __construct(protected PDO $db) {}
// The sequence is fixed here; the steps that vary are abstract
public function generate(): string
{
$rows = $this->fetchRows();
return $this->header() . $this->render($rows) . $this->footer();
}
abstract protected function fetchRows(): array;
abstract protected function render(array $rows): string;
protected function header(): string { return "=== Report ===\n"; }
protected function footer(): string { return "\nGenerated " . date('d M Y'); }
}
class TopScorersReport extends Report
{
protected function fetchRows(): array
{
return $this->db
->query('SELECT name, marks FROM students ORDER BY marks DESC LIMIT 10')
->fetchAll();
}
protected function render(array $rows): string
{
$out = '';
foreach ($rows as $i => $r) {
$out .= sprintf("%2d. %-20s %3d\n", $i + 1, $r['name'], $r['marks']);
}
return $out;
}
}
// $report = new Report($pdo); // Error: cannot instantiate abstract class
$report = new TopScorersReport($pdo);
echo $report->generate();
// A child that forgets an abstract method will not even load:
// class BrokenReport extends Report {}
// Fatal error: contains 2 abstract methods and must be declared abstract - Abstract methods may be
protectedas well aspublic. That is often the better choice for template steps, because those steps are part of how the class works internally, not part of what the outside world is meant to call.
Interfaces: a Contract With No Implementation
An interface lists method signatures with no bodies. A class that implements it promises to provide every one of them. There is no shared code involved at all — an interface says what can be done, never how.
The point is substitutability. If three payment classes all implement PaymentGateway, then any code that accepts a PaymentGateway works with all three, and with a fourth you write next year. That is why type declarations should usually name an interface rather than a concrete class: you are stating what you need, not who must provide it.
Unlike extends, a class may implement many interfaces, separated by commas. This is how PHP gives you the flexibility of multiple inheritance without its problems: a class can promise several unrelated capabilities without inheriting conflicting code from several parents.
Interface methods are always public — a private contract would be meaningless. Interfaces may declare constants, and since PHP 8 they can be implemented by enums too. What they cannot do is declare properties, because how a class stores its data is an implementation detail, which is exactly what an interface is not about.
PHP itself ships several interfaces you will meet. Implementing Countable makes count($object) work. Implementing JsonSerializable lets you control exactly what json_encode() produces for your object, which is very useful for keeping private fields such as a password hash out of an API response.
<?php
interface PaymentGateway
{
public function charge(int $paise, string $reference): bool;
public function refund(string $reference): bool;
}
interface WritesAuditLog
{
public function auditLine(): string;
}
class UpiGateway implements PaymentGateway, WritesAuditLog
{
public function charge(int $paise, string $reference): bool
{
// call the UPI provider's API
return true;
}
public function refund(string $reference): bool { return true; }
public function auditLine(): string { return 'UPI charge ' . date('c'); }
}
class CardGateway implements PaymentGateway
{
public function charge(int $paise, string $reference): bool { return true; }
public function refund(string $reference): bool { return true; }
}
// Depend on the contract, not on a specific class
function checkout(PaymentGateway $gateway, int $paise): void
{
if ($gateway->charge($paise, 'ORD-42')) {
echo 'Payment successful';
}
}
checkout(new UpiGateway(), 499900);
checkout(new CardGateway(), 499900); // works with no change to checkout()
// Built-in interfaces are worth using
class Cart implements Countable, JsonSerializable
{
private array $items = [];
private string $secretToken = 'do-not-expose';
public function count(): int { return count($this->items); }
public function jsonSerialize(): array
{
return ['items' => $this->items, 'count' => $this->count()];
// secretToken is deliberately left out
}
}
$cart = new Cart();
echo count($cart); // uses Countable
echo json_encode($cart); // uses jsonSerialize - Do not declare an interface named
Serializable,Countable,IteratororStringablein the global namespace — PHP already defines those, and redeclaring one is a fatal error. Either pick a different name or put your code inside a namespace, which the best-practices lesson covers.
Abstract Class or Interface? Choosing Between Them
These two are frequently confused because both define methods that subclasses must supply. The difference is what else they carry, and the choice follows from one question: do the implementations share actual code?
If yes — if every implementation will repeat the same twenty lines of setup — use an abstract class. It can hold that shared code once, along with properties and a constructor, so nobody has to duplicate it. The cost is that a class can only have one parent, so you have spent its single inheritance slot.
If no — if the implementations have nothing in common except the shape of their methods — use an interface. It costs nothing, a class can implement as many as it likes, and it leaves the implementer entirely free about how to do the job.
In practice, mature code often uses both together: an interface that defines the contract, and an abstract class implementing it that provides a convenient starting point. Callers type against the interface; implementers extend the abstract class if the shared code helps them, or implement the interface directly if it does not. That combination gives you the guarantees of the interface without forcing anyone into your hierarchy.
- Interface — no code at all, only signatures and constants
- Interface — a class can implement any number of them
- Interface — the right way to express a capability such as "can be exported" or "can be charged"
- Abstract class — can hold complete methods, properties and a constructor
- Abstract class — only one per class, since PHP has single inheritance
- Abstract class — the right way to express "these are all variations of the same thing"
- Both — declare the interface for callers, and offer an abstract base class for implementers who want the shared code
- A practical tie-breaker: write your type declarations against interfaces from the beginning, even when there is only one implementation today. It costs nothing now and means you can add a second implementation — or a fake one for testing — without touching any calling code.
Traits: Sharing Code Without a Parent
Sometimes two classes need the same few methods but are not related at all. A Post and an Invoice might both want created and updated timestamps, but an Invoice is certainly not a kind of Post. Inheritance cannot help, and copying the methods into both is exactly what you were trying to avoid.
A trait is a bundle of methods and properties that gets copied into a class at compile time by writing use TraitName; inside the class body. It is not a type — you cannot type-hint against a trait, and instanceof does not work with one — it is code reuse and nothing more. A class can use as many traits as it likes.
Precedence matters when names collide. A method defined in the class itself wins over one from a trait, and a trait's method wins over one inherited from a parent. If two traits provide the same method name, PHP does not guess: it is a fatal error, and you resolve it explicitly with insteadof to pick one and as to rename the other.
The caution is real. Traits are easy to overuse, and a class using six of them can be very hard to read, because its behaviour is scattered across seven files and nothing in the class declaration hints at what those traits assume about it. Traits also encourage hidden coupling — a trait that calls $this->db silently requires every user of it to have that property.
A useful discipline: keep traits small and self-contained, and if a trait needs something from its host class, declare that requirement as an abstract method inside the trait. Then a class that uses it without providing that method fails immediately and clearly, instead of at some later runtime moment.
<?php
trait HasTimestamps
{
private ?string $createdAt = null;
private ?string $updatedAt = null;
public function touchCreated(): void
{
$this->createdAt = date('Y-m-d H:i:s');
}
public function touchUpdated(): void
{
$this->updatedAt = date('Y-m-d H:i:s');
}
public function createdAt(): ?string { return $this->createdAt; }
}
trait Sluggable
{
// State the requirement, so a bad host fails immediately
abstract public function slugSource(): string;
public function slug(): string
{
$s = mb_strtolower(trim($this->slugSource()));
return trim(preg_replace('/[^a-z0-9]+/', '-', $s), '-');
}
}
class Post
{
use HasTimestamps, Sluggable;
public function __construct(private string $title)
{
$this->touchCreated();
}
public function slugSource(): string { return $this->title; }
}
class Invoice
{
use HasTimestamps; // unrelated class, same timestamp behaviour
public function __construct(public readonly string $number)
{
$this->touchCreated();
}
}
$post = new Post('Working With MySQL');
echo $post->slug(); // working-with-mysql
echo $post->createdAt();
// Resolving a clash between two traits
trait FileLogger { public function log(string $m): void { /* to a file */ } }
trait EmailLogger { public function log(string $m): void { /* to email */ } }
class Service
{
use FileLogger, EmailLogger {
FileLogger::log insteadof EmailLogger; // pick one
EmailLogger::log as logByEmail; // keep the other under a new name
}
} - Before reaching for a trait, ask whether a small collaborating object would be clearer. Instead of a
Sluggabletrait, aSluggerclass with amake()method can be passed in, tested on its own, and swapped out. Traits copy code; objects can be replaced.
Polymorphism, and Preferring Composition
Everything in this lesson exists to make one thing possible: writing code that works with several different classes without knowing which one it has. That is polymorphism, and it is what turns a growing set of cases from a growing switch statement into a set of small classes you can add to.
Compare the two shapes. A function with a switch over an export format has to be edited every time a format is added, and every such edit risks breaking the formats that already worked. A set of exporter classes implementing a common interface requires no edit at all — you add a class, and the existing code uses it because it satisfies the contract.
The warning that belongs at the end of any inheritance lesson is against deep hierarchies. Inheritance is a strong, permanent coupling: the child depends on the parent's internals, and a change three levels up can break something you never looked at. Once you are four levels deep, tracing where a method actually comes from becomes genuine work.
The usual guidance is composition over inheritance. Rather than inheriting behaviour, hold an object that provides it and delegate. A Report that has a Formatter can swap formatters at runtime, be tested with a fake one, and combine behaviours freely — none of which a PdfReport extends Report hierarchy allows.
A workable rule of thumb: use interfaces liberally, inheritance sparingly and shallowly, traits carefully, and composition by default. Reach for inheritance when the "is a" relationship is genuine and the shared code is substantial — and when you find yourself writing a class that overrides most of what it inherits, that is the design telling you it was the wrong relationship.
<?php
// Rigid: every new format means editing this function
function exportOld(array $rows, string $format): string
{
switch ($format) {
case 'csv': return '...';
case 'json': return '...';
// add 'xml' here, and retest everything above it
}
return '';
}
// Open to extension: add a class, change nothing else
interface Exporter
{
public function export(array $rows): string;
public function contentType(): string;
}
class CsvExporter implements Exporter
{
public function export(array $rows): string
{
$out = fopen('php://temp', 'r+');
foreach ($rows as $r) { fputcsv($out, $r); }
rewind($out);
return stream_get_contents($out);
}
public function contentType(): string { return 'text/csv'; }
}
class JsonExporter implements Exporter
{
public function export(array $rows): string
{
return json_encode($rows, JSON_THROW_ON_ERROR);
}
public function contentType(): string { return 'application/json'; }
}
function download(Exporter $exporter, array $rows): void
{
header('Content-Type: ' . $exporter->contentType());
echo $exporter->export($rows);
}
// Composition: the report HAS an exporter rather than inheriting one
class MarksReport
{
public function __construct(
private PDO $db,
private Exporter $exporter, // swap this without subclassing
) {}
public function output(): string
{
$rows = $this->db->query('SELECT name, marks FROM students')->fetchAll();
return $this->exporter->export($rows);
}
}
echo (new MarksReport($pdo, new JsonExporter()))->output(); - Passing dependencies into the constructor like this is called dependency injection, and it is the single habit that makes object-oriented PHP testable. Because
MarksReportreceives itsExporter, a test can hand it a simple stand-in and check the result without generating a real file.
