Skip to main content
    Courses/PHP/Advanced OOP Patterns

    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

    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.

    An interface and two classes that implement it
    <?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
    Output
    Circle     area = 12.57
    Rectangle  area = 12.00
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    An abstract base with one shared method and one hole
    <?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()
    }
    Output
    Rex says Woof!
    Whiskers says Meow!
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Mixing two traits, then resolving a name clash
    <?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
    Output
    [LOG] created
    Updated at 00:00
    Saved to file
    Saved to cloud
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Override, parent::, polymorphism, final and new static()
    <?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";
    Output
    Paying $50.00 by card
    Paying $30.00 in cash
    Paying $10.00
    CardPayment => Paying $99.99 by card
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Strategy and Factory built from interfaces
    <?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";
    Output
    Strategy:
      NoDiscount   -> $100.00
      TenPercent   -> $90.00
      FlatFiveOff  -> $95.00
    Factory:
      loyal -> $90.00
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    🎯 Your turn: sign an interface
    <?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
    Output
    Hello
    Replace the first ___ 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.

    🎯 Your turn: extend an abstract class
    <?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
    Output
    Harley has 2 wheels
    Replace the first ___ 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() or new Shape(). Abstract classes and interfaces are blueprints, never objects. Create a concrete child instead — new Dog() — that extends/implements them.
    • "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 with TraitA::save insteadof TraitB; to pick a winner, and optionally TraitB::save as saveAlt; to keep the other.
    • "Class EmailNotifier contains 1 abstract method and must be declared abstract (Notifier::send)" — you said implements Notifier but 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 remove final in 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 extends get brittle; a couple of small traits are easier to reason about.
    • 💡 Use new static(), not new 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

    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

    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.

    🎯 Mini-Challenge: build and share a trait
    <?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 here
    Define an Identifiable trait, then use it in two unrelated classes and print each object's id. The numbers will differ from the sample.

    🎉 Lesson Complete!

    • ✅ An interface is a contract; a class signs it with implements and must define every method
    • ✅ An abstract class mixes shared code with abstract methods children must fill, and can't be instantiated
    • ✅ A trait gives horizontal reuse across unrelated classes; resolve clashes with insteadof and as
    • extends inherits and overrides; parent:: reaches the version you replaced
    • ✅ Polymorphism lets one loop run each object's own method; final locks classes/methods and new 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service