Lesson 15 • Advanced
Advanced OOP Patterns 🏗️
By the end of this lesson you'll wield PHP's full object toolkit — interfaces, abstract classes, traits, inheritance, polymorphism, final and late static binding — and see how they snap together into the Strategy and Factory patterns that real applications are built from.
What You'll Learn in This Lesson
- Define a contract with interface and sign it with implements
- Share code and force overrides with abstract classes and methods
- Mix behaviour into any class using traits, and resolve conflicts
- Build on a base class with extends and call up with parent::
- Use polymorphism so one loop runs each object's own method
- Apply final and late static binding, and read Strategy & Factory
class, $this, and __construct feel new, start with the Classes & Objects lesson first, then come back here.php file.php. The Output panel under each example shows exactly what to expect.1️⃣ Interfaces — a Contract to Sign
An interface is a promise. It lists method names and their signatures but no actual code — it describes what a class must be able to do, never how. A class then writes implements SomeInterface to "sign the contract", and PHP forces it to define every listed method or the file won't run. The power is that a function can type-hint the interface and then accept any class that signs it — your code depends on the capability, not on one specific class.
<?php
// An interface is a CONTRACT: a list of methods (no bodies) that any
// class promising to be a "Shape" must provide. It defines WHAT, not HOW.
interface Shape
{
public function area(): float; // every shape must be able to report its area
public function name(): string; // ...and tell you what it is
}
// "implements Shape" = this class signs the contract. It MUST define
// both methods, or PHP refuses to run the file.
class Circle implements Shape
{
public function __construct(private float $radius) {}
public function area(): float { return 3.14159 * $this->radius ** 2; }
public function name(): string { return "Circle"; }
}
class Rectangle implements Shape
{
public function __construct(private float $w, private float $h) {}
public function area(): float { return $this->w * $this->h; }
public function name(): string { return "Rectangle"; }
}
// Because both honour the SAME contract, code can treat them identically.
// The type hint "Shape" accepts any class that implements it.
function describe(Shape $s): void
{
printf("%-10s area = %.2f\n", $s->name(), $s->area());
}
describe(new Circle(2)); // Circle area = 12.57
describe(new Rectangle(3, 4)); // Rectangle area = 12.00Circle area = 12.57
Rectangle area = 12.00Notice describe(Shape $s) never mentions Circle or Rectangle. It only knows it received "something that is a Shape", so you can add a Triangle tomorrow and describe() keeps working untouched.
2️⃣ Abstract Classes — Half-Built Blueprints
An abstract class sits between an interface and a normal class. Like an interface it can declare abstract methods — empty methods that every child must fill in. But unlike an interface it can also hold real, shared code, properties, and a constructor. You can never write new Animal() directly; an abstract class exists only to be extends-ed. Use it when several closely related classes share behaviour but each needs to fill in one or two specifics.
<?php
// An ABSTRACT class is a half-finished blueprint. You can't create one
// directly (no "new Animal()") — it exists only to be EXTENDED.
abstract class Animal
{
public function __construct(protected string $name) {}
// An abstract METHOD is a hole: no body here, every child must fill it.
abstract public function speak(): string;
// A NORMAL method: shared code every child inherits for free.
public function introduce(): string
{
return "{$this->name} says " . $this->speak();
}
}
// "extends Animal" inherits the constructor + introduce(), and fills speak().
class Dog extends Animal
{
public function speak(): string { return "Woof!"; }
}
class Cat extends Animal
{
public function speak(): string { return "Meow!"; }
}
$pets = [new Dog("Rex"), new Cat("Whiskers")];
foreach ($pets as $pet) {
echo $pet->introduce() . "\n"; // uses inherited introduce() + each child's speak()
}Rex says Woof!
Whiskers says Meow!Each child wrote speak() just once, yet both got the entire introduce() method and the constructor for free. That's the trade an interface can't make: shared code lives in one place.
3️⃣ Traits — Horizontal Reuse
PHP allows single inheritance only: a class can extends exactly one parent. So how do unrelated classes share the same code — say logging, used by both an Article and an Invoice? A trait is a bundle of methods you use inside any class, as if they were pasted in. A class can pull in many traits. If two traits define a method with the same name, PHP stops with an error until you resolve it: insteadof picks the winner and as keeps the loser under a new name.
<?php
// A TRAIT is a reusable bundle of methods you can "paste" into ANY class —
// even unrelated ones. It solves single-inheritance: a class extends ONE
// parent but can "use" MANY traits.
trait Timestampable
{
public function touch(): string { return "Updated at " . date("H:i", 0); }
}
trait Loggable
{
public function log(string $msg): string { return "[LOG] $msg"; }
}
class Article
{
use Timestampable, Loggable; // mix BOTH traits into this one class
public function __construct(public string $title) {}
}
$a = new Article("Hello");
echo $a->log("created") . "\n"; // method came from Loggable
echo $a->touch() . "\n"; // method came from Timestampable
// CONFLICT RESOLUTION: if two traits define the same method name, PHP errors
// unless you pick a winner with "insteadof" (and optionally rename with "as").
trait FileStore { public function save(): string { return "Saved to file"; } }
trait CloudStore { public function save(): string { return "Saved to cloud"; } }
class Backup
{
use FileStore, CloudStore {
FileStore::save insteadof CloudStore; // FileStore::save() wins
CloudStore::save as saveToCloud; // keep the other under a new name
}
}
$b = new Backup();
echo $b->save() . "\n"; // Saved to file
echo $b->saveToCloud() . "\n"; // Saved to cloud[LOG] created
Updated at 00:00
Saved to file
Saved to cloud4️⃣ Inheritance, Polymorphism, final & Late Static Binding
When a class extends another, it inherits everything and may override a method by redefining it with a matching signature. Inside an override, parent::method() calls the version you just replaced so you can extend rather than discard it. Polymorphism ("many forms") is the pay-off: loop over a mixed list of objects, call the same method on each, and every object runs its own version. final locks a class or method so it can't be extended or overridden. And late static binding — the static keyword — makes new static() in a parent build the child that was actually called.
<?php
// A base class with shared behaviour.
class PaymentMethod
{
public function __construct(protected float $amount) {}
public function describe(): string
{
return "Paying $" . number_format($this->amount, 2);
}
// LATE STATIC BINDING: "static" resolves to the class that was CALLED,
// not the one where this line is written. "new static()" builds a child.
public static function of(float $amount): static
{
return new static($amount);
}
}
class CardPayment extends PaymentMethod
{
// OVERRIDE: same method name + matching signature, new behaviour.
public function describe(): string
{
// "parent::" calls the version we just overrode, then we add to it.
return parent::describe() . " by card";
}
}
// FINAL: this class cannot be extended, and final methods cannot be overridden.
final class CashPayment extends PaymentMethod
{
public function describe(): string
{
return parent::describe() . " in cash";
}
}
// POLYMORPHISM: one variable, many forms. Each $p runs ITS OWN describe().
$payments = [new CardPayment(50), new CashPayment(30), new PaymentMethod(10)];
foreach ($payments as $p) {
echo $p->describe() . "\n";
}
// Late static binding pay-off: of() lives in the parent but builds the child.
$card = CardPayment::of(99.99);
echo get_class($card) . " => " . $card->describe() . "\n";Paying $50.00 by card
Paying $30.00 in cash
Paying $10.00
CardPayment => Paying $99.99 by cardThe single foreach loop is polymorphism in one line: three different classes, one method call, three different results. And CardPayment::of(99.99) proves late static binding — the of() code lives in the parent yet returned a CardPayment.
5️⃣ A Brief Look at Patterns: Strategy & Factory
Design patterns are just named, repeatable ways to combine the features above. The Strategy pattern defines an interface for an algorithm, then lets you swap interchangeable implementations at runtime — the client holds "a strategy" without caring which. The Factory pattern hides object creation behind a method, so callers ask for a type by name instead of writing new for a specific class. You've already used the bricks; here they are assembled.
<?php
// These OOP features are the bricks behind classic design patterns.
// STRATEGY: an interface + interchangeable implementations you swap at runtime.
interface DiscountStrategy
{
public function apply(float $total): float;
}
class NoDiscount implements DiscountStrategy { public function apply(float $t): float { return $t; } }
class TenPercent implements DiscountStrategy { public function apply(float $t): float { return $t * 0.9; } }
class FlatFiveOff implements DiscountStrategy { public function apply(float $t): float { return max(0, $t - 5); } }
class Checkout
{
// The client holds a DiscountStrategy but doesn't care which one it is.
public function __construct(private DiscountStrategy $discount) {}
public function total(float $subtotal): float { return $this->discount->apply($subtotal); }
}
echo "Strategy:\n";
foreach ([new NoDiscount(), new TenPercent(), new FlatFiveOff()] as $strategy) {
$checkout = new Checkout($strategy);
printf(" %-12s -> $%.2f\n", get_class($strategy), $checkout->total(100));
}
// FACTORY: a static method that returns the right object for a type string,
// so callers never write "new SomethingSpecific()" themselves.
class DiscountFactory
{
public static function make(string $type): DiscountStrategy
{
return match ($type) {
"none" => new NoDiscount(),
"loyal" => new TenPercent(),
"promo" => new FlatFiveOff(),
default => throw new InvalidArgumentException("Unknown: $type"),
};
}
}
echo "Factory:\n";
$strategy = DiscountFactory::make("loyal"); // factory picks the class
echo " loyal -> $" . number_format((new Checkout($strategy))->total(100), 2) . "\n";Strategy:
NoDiscount -> $100.00
TenPercent -> $90.00
FlatFiveOff -> $95.00
Factory:
loyal -> $90.00Now you try. The script below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
<?php
// 🎯 YOUR TURN — finish the contract and sign it.
// A "Greeter" must be able to greet(). Make EnglishGreeter honour it.
interface Greeter
{
public function greet(): string; // the contract: one method, no body
}
// 1) Make this class sign the contract.
class EnglishGreeter ___ Greeter // 👉 the keyword that signs an interface
{
// 2) Fill the contract method so it returns the word "Hello".
public function greet(): string { return ___; } // 👉 "Hello" in double quotes
}
$g = new EnglishGreeter();
echo $g->greet() . "\n";
// ✅ Expected output:
// HelloHello___ with the keyword that signs an interface, and the second with "Hello". Output should be a single line.One more. This time you'll extend an abstract class and fill in its one hole.
<?php
// 🎯 YOUR TURN — extend an abstract class and call the parent.
abstract class Vehicle
{
public function __construct(protected string $name) {}
abstract public function wheels(): int; // child must fill this hole
public function describe(): string { return "{$this->name} has " . $this->wheels() . " wheels"; }
}
// 1) Make Motorbike build on Vehicle.
class Motorbike ___ Vehicle // 👉 the keyword that inherits a class
{
// 2) A motorbike has 2 wheels — return that number.
public function wheels(): int { return ___; } // 👉 a whole number
}
$m = new Motorbike("Harley");
echo $m->describe() . "\n";
// ✅ Expected output:
// Harley has 2 wheelsHarley has 2 wheels___ with the keyword that inherits a class, and the second with the number of wheels a motorbike has.Common Errors (and the fix)
- "Cannot instantiate abstract class Animal" / "Cannot instantiate interface Shape" — you wrote
new Animal()ornew Shape(). Abstract classes and interfaces are blueprints, never objects. Create a concrete child instead —new Dog()— thatextends/implementsthem. - "Trait method save has not been applied as Backup::save, because of collision" — two traits define the same method name. PHP won't guess. Add a
use { ... }block withTraitA::save insteadof TraitB;to pick a winner, and optionallyTraitB::save as saveAlt;to keep the other. - "Class EmailNotifier contains 1 abstract method and must be declared abstract (Notifier::send)" — you said
implements Notifierbut forgot to define one of its methods. Implement every method the interface lists, with a matching signature. - "Declaration of Cat::speak(int $x) must be compatible with Animal::speak()" — your override changed the parameters or return type. An override must keep a compatible signature (same/compatible parameter and return types). Match the parent's declaration exactly unless you're widening it in an allowed way.
- "Cannot override final method PaymentMethod::describe()" — the parent marked that method
final, which forbids overriding. Either removefinalin the parent (if you control it) or leave the method alone and add new behaviour elsewhere.
Pro Tips
- 💡 Program to an interface. Type-hint the interface (
Shape), not the concrete class — your code stays open to new implementations. - 💡 Prefer composition (traits) over deep inheritance. Long chains of
extendsget brittle; a couple of small traits are easier to reason about. - 💡 Use
new static(), notnew self(), in factory-style methods on a base class so subclasses return their own type. - 💡 Reach for a pattern only when pain appears — duplication, tight coupling, hard-to-test code. Premature abstraction is worse than none.
📋 Quick Reference — interface vs abstract vs trait
| Feature | interface | abstract class | trait |
|---|---|---|---|
| Keyword to use it | implements | extends | use |
| How many at once? | many | one | many |
| Can hold real code? | no (signatures only) | yes | yes |
| Properties / constructor? | constants only | yes | yes |
| Can be instantiated? | no | no | no (mixed in) |
| Use it for | a capability / contract | shared code in a family | reuse across unrelated classes |
Frequently Asked Questions
Q: What is the difference between an interface and an abstract class in PHP?
An interface is a pure contract: it lists method signatures with no bodies, and a class can implement many interfaces. An abstract class is a partial blueprint: it can hold real, shared code AND abstract (empty) methods, plus properties and a constructor — but a class can only extend one of them. Rule of thumb: reach for an interface to describe a capability ('this can be compared', 'this can be sent'), and an abstract class to share common code among closely related classes.
Q: When should I use a trait instead of inheritance?
Use a trait when you want to share the SAME methods across classes that are not part of one family tree. PHP only allows single inheritance — a class extends exactly one parent — so traits give you 'horizontal reuse': behaviour like logging, timestamps, or serialization that many unrelated classes need. Use inheritance (extends) when there is a genuine 'is-a' relationship and a shared identity, like Dog is-a Animal.
Q: Why does PHP say a method conflict when I use two traits?
If two traits you 'use' in the same class both define a method with the same name, PHP cannot guess which one you want, so it raises a fatal error. You resolve it inside the use block: 'TraitA::method insteadof TraitB;' picks the winner, and 'TraitB::method as newName;' keeps the loser available under a different name. Being explicit is intentional — it forces you to make the choice visible.
Q: What is late static binding and why do I need 'static' instead of 'self'?
Inside a class, 'self' is locked to the class where the line is written, while 'static' resolves to the class that was actually called at runtime. So 'new static()' in a parent method builds an instance of whichever child invoked it, whereas 'new self()' would always build the parent. This lets a base class provide a factory method like 'of()' that correctly returns child objects.
Q: What does the 'final' keyword do?
'final' locks something down. A final class cannot be extended, and a final method cannot be overridden by a child class. You use it to protect behaviour you never want changed — for example a security check or a Singleton — and to signal intent to other developers. It also lets the engine make small optimisations because it knows the code can't be replaced.
Mini-Challenge: A Shared Trait
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is the write-run-check loop you'll use on every real class.
<?php
// 🎯 MINI-CHALLENGE: a logging trait + two classes that share it.
// No code is filled in — work from the steps below, then run it.
//
// 1. Define a trait called "Identifiable" with one method:
// id(): string that returns "ID-" followed by spl_object_id($this)
// (spl_object_id($this) gives each object a unique number).
// 2. Create a class "Order" that uses the trait and has a public $ref string.
// 3. Create a class "Invoice" that ALSO uses the same trait.
// 4. Make one Order and one Invoice, then echo $obj->id() for each on its own line.
//
// Tip: a class pulls a trait in with "use Identifiable;" inside the class body.
//
// ✅ Expected output (numbers will vary):
// ID-1
// ID-2
// your code hereIdentifiable trait, then use it in two unrelated classes and print each object's id. The numbers will differ from the sample.🎉 Lesson Complete!
- ✅ An
interfaceis a contract; a class signs it withimplementsand must define every method - ✅ An
abstractclass mixes shared code with abstract methods children must fill, and can't be instantiated - ✅ A
traitgives horizontal reuse across unrelated classes; resolve clashes withinsteadofandas - ✅
extendsinherits and overrides;parent::reaches the version you replaced - ✅ Polymorphism lets one loop run each object's own method;
finallocks classes/methods andnew static()builds the called child - ✅ Strategy and Factory are just these features combined into reusable, named designs
- ✅ Next lesson: Dependency Injection — wire objects together so your code stays decoupled and testable
Sign up for free to track which lessons you've completed and get learning reminders.