01 · OOP Deep Dive¶
Level 1 covered classes, properties, and
visibility. Real applications need more than single classes with no shared
contract — they need ways to guarantee that unrelated classes support the
same operations (interfaces), share partial implementations across a
family of related classes (abstract classes), and reuse method bodies
across classes that aren't related by inheritance at all (traits). These
three tools are how PHP does polymorphism and code reuse beyond simple
extends.
Interfaces: contracts without implementation¶
An interface declares what methods a class must have, not how they work. Any class that implements the interface must provide bodies for every method.
<?php
interface Shape
{
public function area(): float;
public function perimeter(): float;
}
class Rectangle implements Shape
{
public function __construct(private float $width, private float $height) {}
public function area(): float
{
return $this->width * $this->height;
}
public function perimeter(): float
{
return 2 * ($this->width + $this->height);
}
}
class Circle implements Shape
{
public function __construct(private float $radius) {}
public function area(): float
{
return M_PI * $this->radius ** 2;
}
public function perimeter(): float
{
return 2 * M_PI * $this->radius;
}
}
// Works with ANY Shape -- doesn't care which concrete class it receives
function describe(Shape $shape): string
{
return sprintf("area=%.2f perimeter=%.2f", $shape->area(), $shape->perimeter());
}
echo describe(new Rectangle(4, 5)); // area=20.00 perimeter=18.00
echo "\n";
echo describe(new Circle(3)); // area=28.27 perimeter=18.85
Rectangle and Circle share no parent class and store completely different
data, but because both implement Shape, describe() can accept either.
This is polymorphism: code written against the interface works with any
class that honors the contract, including ones that don't exist yet.
A class can implement several interfaces at once (separated by commas),
unlike extends, which only allows one parent class. Interfaces can also
extend other interfaces, and declare constants — but never properties or
method bodies. Checking whether an object satisfies an interface (or class)
at runtime uses instanceof:
<?php
$r = new Rectangle(4, 5);
var_dump($r instanceof Shape); // bool(true)
var_dump($r instanceof Rectangle); // bool(true) -- also true for the concrete class
Abstract classes: shared implementation plus required overrides¶
An abstract class is a base class that cannot be instantiated directly.
It can mix concrete (fully implemented) methods with abstract methods that
every subclass must implement. Use it when related classes share real code,
not just a contract.
<?php
abstract class Employee
{
public function __construct(
protected string $name,
protected float $baseSalary,
) {}
// Concrete method -- shared by every subclass as-is
public function getName(): string
{
return $this->name;
}
// Abstract method -- no body here; each subclass MUST provide one
abstract public function calculatePay(): float;
// Concrete method that calls the abstract one -- this is the
// "template method" pattern: the algorithm's shape lives in the
// base class, the varying step is delegated to subclasses.
public function paySlip(): string
{
return sprintf("%s: $%.2f", $this->name, $this->calculatePay());
}
}
class SalariedEmployee extends Employee
{
public function calculatePay(): float
{
return $this->baseSalary / 12; // annual salary paid monthly
}
}
class CommissionEmployee extends Employee
{
public function __construct(
string $name,
float $baseSalary,
private float $commission,
) {
parent::__construct($name, $baseSalary);
}
public function calculatePay(): float
{
return $this->baseSalary / 12 + $this->commission;
}
}
$staff = [
new SalariedEmployee("Priya", 84000),
new CommissionEmployee("Marco", 60000, 1200),
];
foreach ($staff as $employee) {
echo $employee->paySlip() . "\n";
}
// Priya: $7000.00
// Marco: $6200.00
// new Employee("X", 1000); // Fatal error: Cannot instantiate abstract class
A subclass that forgets to implement an abstract method is caught at
class-definition time, not silently ignored: class Incomplete extends
Employee {} fails immediately with Fatal error: Class Incomplete contains
1 abstract method and must therefore be declared abstract or implement the
remaining methods.
Traits: sharing method bodies across unrelated classes¶
Interfaces share a contract; abstract classes share a contract and an
inheritance relationship. Sometimes you want to share actual method bodies
between classes that have no business relationship at all — a Logger mixin
usable by both a Payment class and a Report class, for example. PHP only
allows single inheritance (extends one class), so traits fill that gap.
<?php
trait Loggable
{
private array $log = [];
public function logMessage(string $message): void
{
$this->log[] = sprintf("[%s] %s", date("H:i:s"), $message);
}
public function getLog(): array
{
return $this->log;
}
}
class Payment
{
use Loggable; // "copies in" logMessage() and getLog() as if written here
public function process(float $amount): void
{
$this->logMessage("Processing payment of $amount");
}
}
class Report
{
use Loggable;
public function generate(): void
{
$this->logMessage("Report generated");
}
}
$payment = new Payment();
$payment->process(49.99);
print_r($payment->getLog());
// Array ( [0] => [10:22:01] Processing payment of 49.99 )
$report = new Report();
$report->generate();
print_r($report->getLog());
// Array ( [0] => [10:22:01] Report generated )
Payment and Report share no parent/child relationship — each got its own
independent $log array and its own copy of the trait's methods. A trait is
best thought of as "copy-paste that the language does for you," not a real
is-a relationship (unlike interfaces or abstract classes, instanceof
does not work against a trait name).
Resolving conflicts between traits¶
If a class uses two traits that define the same method name, PHP requires you to resolve the collision explicitly rather than silently picking one:
<?php
trait Swimmer
{
public function move(): string
{
return "swims";
}
}
trait Runner
{
public function move(): string
{
return "runs";
}
}
class Duck
{
use Swimmer, Runner {
Swimmer::move insteadof Runner; // prefer Swimmer's version for move()
Runner::move as run; // but keep Runner's version too, renamed
}
}
$duck = new Duck();
echo $duck->move(); // swims
echo "\n";
echo $duck->run(); // runs
Without the insteadof/as block, combining two traits that define the same
method is a fatal error — PHP refuses to guess which one you meant.
Choosing between them¶
| Tool | Instantiable? | Shares state? | Multiple at once? | Use when |
|---|---|---|---|---|
| Interface | No | No (contract only) | Yes, unlimited | You need a guaranteed set of methods, no shared code |
| Abstract class | No | Yes | No, single extends |
Related classes share real implementation plus required overrides |
| Trait | No | Yes (per including class) | Yes, unlimited | Unrelated classes need the same method bodies (no is-a relationship) |
A single class often combines all three: implements one or more
interfaces, extends at most one (possibly abstract) parent, and uses any
number of traits.
How It Actually Works¶
Interfaces, abstract classes, and traits are all compiled into zend_class_entry structures, but the engine treats them very differently at link time (the step, right after compilation, where class hierarchies actually get resolved). An interface contributes only a set of required method signatures to a class entry's function table — checked once, when the implementing class is linked, by verifying every interface method has a concrete match; it adds zero method bodies. An abstract class contributes real compiled method bodies plus placeholders that must be overridden — the engine refuses to instantiate any class entry still carrying an unresolved abstract method, checked at the new opcode. Traits are fundamentally different: use TraitName; triggers a compile-time copy of the trait's compiled opcodes directly into the using class's method table, as if you'd hand-pasted the method bodies in — this is why two traits with a method of the same name in one class produce a compile error (an actual name collision at link time), not a runtime override, and why insteadof/as exist as compiler directives to resolve that collision before linking can succeed.
Exercise¶
Define an interface Notifiable with one method, notify(string $message):
void. Create an abstract class BaseNotifier implements Notifiable with a
concrete formatMessage(string $message): string method that adds a
timestamp prefix, plus an abstract send(string $formatted): void — have
notify() call formatMessage() then send() (the template method
pattern). Write two concrete subclasses, EmailNotifier and SmsNotifier,
each implementing send() by just echo-ing what it would send. Then write
a trait RateLimited with a hasSentTooRecently(): bool method (track a
last-sent timestamp) and mix it into one of the notifiers to skip sending if
called twice within one second.