Advanced OOP Patterns

Reviewed & published by Brayan K

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.

Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

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.00

Notice 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()
}

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

4️⃣ 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";

The 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";

Now 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:
//    Hello

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 wheels

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — interface vs abstract vs trait

Featureinterfaceabstract classtrait
Keyword to use itimplementsextendsuse
How many at once?manyonemany
Can hold real code?no (signatures only)yesyes
Properties / constructor?constants onlyyesyes
Can be instantiated?nonono (mixed in)
Use it fora capability / contractshared code in a familyreuse across unrelated classes

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.

🎉 Lesson Complete!

Practice quiz

Which keyword does a class use to sign an interface contract?

  • extends
  • use
  • implements
  • abstract

Answer: implements. A class writes 'implements SomeInterface' and must then define every method the interface lists.

What is the key difference between an interface and an abstract class?

  • An interface is signatures only; an abstract class can hold real shared code
  • An abstract class has no methods
  • An interface can be instantiated
  • They are identical

Answer: An interface is signatures only; an abstract class can hold real shared code. An interface is a pure contract; an abstract class can also carry shared code, properties, and a constructor.

How many classes can a single class extend in PHP?

  • Two
  • Unlimited
  • Zero
  • One

Answer: One. PHP allows single inheritance only — a class extends exactly one parent. Traits give horizontal reuse.

What happens if you try new Animal() on an abstract class Animal?

  • It creates an empty Animal
  • An error — abstract classes cannot be instantiated
  • It returns null
  • It calls the first child class

Answer: An error — abstract classes cannot be instantiated. Abstract classes are blueprints; you must instantiate a concrete child that extends them.

Inside an override, how do you call the version you just replaced?

  • parent::method()
  • self::method()
  • this::method()
  • super.method()

Answer: parent::method(). parent::method() reaches the overridden parent version so you can extend rather than discard it.

What is late static binding — what does new static() build?

  • Always an instance of the parent class
  • A static array
  • An instance of the class that was actually called at runtime
  • A new interface

Answer: An instance of the class that was actually called at runtime. 'static' resolves to the called class, so new static() in a parent factory returns the child's type.

Why use 'static' instead of 'self' in a base-class factory method?

  • self is faster
  • self is locked to where it is written; static follows the called class
  • static only works on properties
  • There is no difference

Answer: self is locked to where it is written; static follows the called class. new self() always builds the parent; new static() builds whichever child invoked the method.

What does the final keyword do?

  • Marks a class as abstract
  • Makes a method static
  • Allows multiple inheritance
  • Prevents a class from being extended or a method from being overridden

Answer: Prevents a class from being extended or a method from being overridden. final locks behaviour: a final class can't be extended and a final method can't be overridden.

If two traits define a save() method, how do you pick which wins?

  • FileStore::save overrides CloudStore;
  • FileStore::save insteadof CloudStore;
  • use FileStore not CloudStore;
  • PHP picks automatically

Answer: FileStore::save insteadof CloudStore;. insteadof picks the winner; 'CloudStore::save as saveToCloud;' keeps the loser under a new name.

What does the Strategy pattern let you do?

  • Create objects without using new
  • Lock a class from extension
  • Swap interchangeable algorithm implementations behind one interface at runtime
  • Merge two traits

Answer: Swap interchangeable algorithm implementations behind one interface at runtime. Strategy defines an interface for an algorithm and lets the client hold any implementation without caring which.

Continue this course

Frequently asked questions

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.

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.

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.

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.

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.

Related lessons