Skip to main content

🧬 Lesson 17: OOP — Inheritance, Interfaces & Traits

In Lesson 16, you learned how to build classes and objects. But what happens when you need a class that's almost like an existing one, just with some differences? Or when unrelated classes need to share a common contract? Or when you want to mix reusable behavior into multiple classes? That's where inheritance, interfaces, and traits come in — the tools that let you build flexible, maintainable class hierarchies.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Extend classes with extends and override methods in child classes
  • Call parent methods with parent::
  • Define abstract classes and methods as contracts for child classes
  • Create and implement interfaces with implements
  • Use traits to share reusable behavior across unrelated classes
  • Organize code with namespaces and understand basic autoloading
  • Choose the right tool — inheritance vs. interfaces vs. traits — for each situation

Estimated Time: 50 minutes

Prerequisites: Lesson 16 (OOP — Classes & Objects)

📑 In This Lesson

Inheritance Basics

Inheritance lets you create a new class based on an existing one. The new class (the child or subclass) inherits all the public and protected properties and methods of the existing class (the parent or superclass). The child can then add new behavior or modify what it inherited.

📖 Key Terms

Parent class (superclass): The class being extended — the base that provides shared behavior.

Child class (subclass): The class that extends the parent — it inherits everything and can add or change behavior.

extends: The keyword that creates an inheritance relationship.

Creating a Child Class

<?php
// Parent class — shared behavior for all users
class User {
    public function __construct(
        public string $name,
        public string $email,
        public string $role = "user",
    ) {}

    public function getDisplayName(): string {
        return $this->name;
    }

    public function canView(): bool {
        return true; // All users can view
    }

    public function canEdit(): bool {
        return false; // Regular users cannot edit
    }

    public function describe(): string {
        return "{$this->name} ({$this->role})";
    }
}

// Child class — inherits everything from User, adds more
class AdminUser extends User {
    public function __construct(
        string $name,
        string $email,
        public string $department = "General",
    ) {
        // Call the parent constructor
        parent::__construct($name, $email, "admin");
    }

    // Override — admins CAN edit
    public function canEdit(): bool {
        return true;
    }

    // New method — only admins have this
    public function deleteUser(User $user): string {
        return "Admin {$this->name} deleted user {$user->name}";
    }
}

// Usage
$user = new User("Alice", "alice@example.com");
$admin = new AdminUser("Bob", "bob@example.com", "Engineering");

echo $user->describe();     // "Alice (user)"
echo $admin->describe();    // "Bob (admin)" — inherited from User

echo $user->canEdit();      // false
echo $admin->canEdit();     // true — overridden in AdminUser

echo $admin->deleteUser($user); // "Admin Bob deleted user Alice"
// $user->deleteUser($admin);   // Error! deleteUser doesn't exist on User

// Type checking
var_dump($admin instanceof AdminUser); // true
var_dump($admin instanceof User);      // true — AdminUser IS a User
classDiagram class User { +string name +string email +string role +getDisplayName() string +canView() bool +canEdit() bool +describe() string } class AdminUser { +string department +canEdit() bool +deleteUser(User) string } User <|-- AdminUser : extends note for AdminUser "Inherits name, email, role,\ngetDisplayName(), canView(),\ndescribe() from User.\nOverrides canEdit().\nAdds department & deleteUser()."

⚠️ Single Inheritance Only

PHP supports single inheritance — a class can only extend one parent. You cannot write class AdminUser extends User, Manager. This is by design to avoid the complexity of multiple inheritance. If you need to share behavior from multiple sources, use interfaces and traits (covered later in this lesson).

What Gets Inherited?

Member Type Inherited? Notes
public properties/methods ✅ Yes Fully accessible in child class and from outside
protected properties/methods ✅ Yes Accessible in child class, but not from outside
private properties/methods ❌ No Only the defining class can access them — invisible to children
Constants ✅ Yes Can be overridden in child class
Static members ✅ Yes Shared via inheritance chain

✅ The protected Sweet Spot

Now that you know about inheritance, protected makes more sense. Use it for properties or methods that child classes need to access but external code shouldn't. It's the "family only" visibility — not fully open (public), not locked away (private), but available within the class hierarchy.

Method Overriding & parent::

When a child class defines a method with the same name as a parent method, the child's version overrides the parent's. This is how you customize inherited behavior.

Basic Overriding

<?php
class Shape {
    public function __construct(
        public string $color = "black",
    ) {}

    public function describe(): string {
        return "A {$this->color} shape";
    }

    public function area(): float {
        return 0.0; // Default — subclasses should override
    }
}

class Circle extends Shape {
    public function __construct(
        public float $radius,
        string $color = "black",
    ) {
        parent::__construct($color);
    }

    // Override describe() with circle-specific info
    public function describe(): string {
        return "A {$this->color} circle with radius {$this->radius}";
    }

    // Override area() with actual calculation
    public function area(): float {
        return M_PI * $this->radius ** 2;
    }
}

class Rectangle extends Shape {
    public function __construct(
        public float $width,
        public float $height,
        string $color = "black",
    ) {
        parent::__construct($color);
    }

    public function describe(): string {
        return "A {$this->color} rectangle ({$this->width}×{$this->height})";
    }

    public function area(): float {
        return $this->width * $this->height;
    }
}

$circle = new Circle(5, "red");
$rect   = new Rectangle(10, 4, "blue");

echo $circle->describe(); // "A red circle with radius 5"
echo $rect->describe();   // "A blue rectangle (10×4)"

echo $circle->area();     // 78.5398...
echo $rect->area();       // 40

Calling the Parent with parent::

Sometimes you want to extend the parent's behavior rather than completely replace it. Use parent::methodName() to call the parent's version from inside the override:

<?php
class Vehicle {
    public function __construct(
        public string $make,
        public string $model,
        public int $year,
    ) {}

    public function describe(): string {
        return "{$this->year} {$this->make} {$this->model}";
    }

    public function start(): string {
        return "Engine started.";
    }
}

class ElectricVehicle extends Vehicle {
    public function __construct(
        string $make,
        string $model,
        int $year,
        public int $batteryCapacity, // kWh
    ) {
        parent::__construct($make, $model, $year);
    }

    // Extend the parent's describe() — add battery info
    public function describe(): string {
        // Call parent version first, then append
        return parent::describe() . " (Electric, {$this->batteryCapacity} kWh)";
    }

    // Completely replace start()
    public function start(): string {
        return "Electric motor engaged silently.";
    }
}

$car = new Vehicle("Honda", "Civic", 2024);
$ev  = new ElectricVehicle("Tesla", "Model 3", 2024, 75);

echo $car->describe(); // "2024 Honda Civic"
echo $ev->describe();  // "2024 Tesla Model 3 (Electric, 75 kWh)"

echo $car->start();    // "Engine started."
echo $ev->start();     // "Electric motor engaged silently."

Constructors and parent::__construct()

<?php
class Animal {
    public function __construct(
        public string $name,
        public string $species,
        protected int $age = 0,
    ) {}
}

class Pet extends Animal {
    public function __construct(
        string $name,
        string $species,
        int $age,
        public string $owner,
        public bool $vaccinated = false,
    ) {
        // MUST call parent constructor to initialize inherited properties
        parent::__construct($name, $species, $age);
    }

    public function describe(): string {
        $status = $this->vaccinated ? "vaccinated" : "not vaccinated";
        return "{$this->name} the {$this->species}, owned by {$this->owner} ($status)";
    }
}

$pet = new Pet("Buddy", "Golden Retriever", 3, "Alice", true);
echo $pet->describe(); // "Buddy the Golden Retriever, owned by Alice (vaccinated)"
echo $pet->name;       // "Buddy" — inherited from Animal
⚠️ Important: If the parent class has a constructor, the child should call parent::__construct() to properly initialize inherited properties. PHP won't automatically call it for you (unlike some other languages). Forgetting this is a common source of bugs.

The final Keyword

Mark a class or method as final to prevent it from being overridden or extended:

<?php
class Payment {
    // This method CANNOT be overridden — security-critical logic
    final public function processPayment(float $amount): bool {
        // Validate, charge, log — no subclass should change this
        $this->validate($amount);
        $result = $this->charge($amount);
        $this->log($amount, $result);
        return $result;
    }

    // These CAN be overridden for different payment types
    protected function validate(float $amount): void {
        if ($amount <= 0) throw new InvalidArgumentException("Invalid amount");
    }

    protected function charge(float $amount): bool {
        return true; // Default implementation
    }

    protected function log(float $amount, bool $success): void {
        echo ($success ? "Charged" : "Failed") . ": $$amount\n";
    }
}

// You can extend Payment...
class CreditCardPayment extends Payment {
    protected function charge(float $amount): bool {
        // Custom credit card charging logic
        return true;
    }

    // But you CANNOT override processPayment():
    // public function processPayment(float $amount): bool { ... }
    // Error: Cannot override final method Payment::processPayment()
}

// A final class CANNOT be extended at all:
final class SecurityToken {
    public function __construct(
        public readonly string $token,
        public readonly int $expiresAt,
    ) {}
}
// class CustomToken extends SecurityToken {} // Error! Cannot extend final class

Abstract Classes & Methods

What if you want to create a base class that requires child classes to implement certain methods? That's an abstract class. It serves as a partial blueprint — it provides some shared behavior but leaves specific details for child classes to fill in.

Defining Abstract Classes

<?php
// Abstract class — cannot be instantiated directly
abstract class Shape {
    public function __construct(
        public string $color = "black",
    ) {}

    // Abstract method — NO body, child MUST implement it
    abstract public function area(): float;
    abstract public function perimeter(): float;

    // Concrete method — shared by all shapes
    public function describe(): string {
        $area = number_format($this->area(), 2);
        $perimeter = number_format($this->perimeter(), 2);
        return "A {$this->color} " . static::class
             . " (area: $area, perimeter: $perimeter)";
    }

    // Compare shapes by area
    public function isLargerThan(Shape $other): bool {
        return $this->area() > $other->area();
    }
}

// Concrete child — MUST implement area() and perimeter()
class Circle extends Shape {
    public function __construct(
        public float $radius,
        string $color = "black",
    ) {
        parent::__construct($color);
    }

    public function area(): float {
        return M_PI * $this->radius ** 2;
    }

    public function perimeter(): float {
        return 2 * M_PI * $this->radius;
    }
}

class Rectangle extends Shape {
    public function __construct(
        public float $width,
        public float $height,
        string $color = "black",
    ) {
        parent::__construct($color);
    }

    public function area(): float {
        return $this->width * $this->height;
    }

    public function perimeter(): float {
        return 2 * ($this->width + $this->height);
    }
}

class Triangle extends Shape {
    public function __construct(
        public float $base,
        public float $height,
        public float $sideA,
        public float $sideB,
        string $color = "black",
    ) {
        parent::__construct($color);
    }

    public function area(): float {
        return 0.5 * $this->base * $this->height;
    }

    public function perimeter(): float {
        return $this->base + $this->sideA + $this->sideB;
    }
}

// $shape = new Shape();  // Error! Cannot instantiate abstract class

$circle = new Circle(5, "red");
$rect   = new Rectangle(10, 4, "blue");
$tri    = new Triangle(6, 8, 10, 8, "green");

echo $circle->describe();
// "A red Circle (area: 78.54, perimeter: 31.42)"

echo $rect->describe();
// "A blue Rectangle (area: 40.00, perimeter: 28.00)"

// Polymorphism — treat different shapes uniformly
$shapes = [$circle, $rect, $tri];
foreach ($shapes as $shape) {
    echo $shape->describe() . "\n";
}

echo $circle->isLargerThan($rect) ? "Circle wins" : "Rectangle wins";
// "Circle wins" (78.54 > 40.00)

📖 Abstract vs. Concrete

Abstract class: Has the abstract keyword. Cannot be instantiated with new. May contain abstract methods (no body) and concrete methods (with body). Forces child classes to implement the abstract methods.

Concrete class: A normal class without abstract. Can be instantiated. Must implement all inherited abstract methods.

✅ When to Use Abstract Classes

Use an abstract class when related classes share some implementation (code) but each has unique parts. The abstract class provides the shared code (like describe() and isLargerThan() above) while requiring each child to fill in the blanks (like area() and perimeter()).

Interfaces

An interface defines a contract — a set of methods that a class must implement. Unlike abstract classes, interfaces contain no implementation at all (no method bodies, no properties). They say what a class can do without specifying how.

Defining and Implementing Interfaces

<?php
// Interface — defines WHAT methods must exist
interface Printable {
    public function toString(): string;
    public function toHTML(): string;
}

interface Exportable {
    public function toJSON(): string;
    public function toCSV(): string;
}

// A class can implement MULTIPLE interfaces (unlike extends)
class Invoice implements Printable, Exportable {
    private array $items = [];

    public function __construct(
        public readonly string $invoiceNumber,
        public readonly string $customer,
        public readonly string $date,
    ) {}

    public function addItem(string $description, float $amount): void {
        $this->items[] = ["description" => $description, "amount" => $amount];
    }

    public function getTotal(): float {
        return array_sum(array_column($this->items, "amount"));
    }

    // --- Implement Printable ---
    public function toString(): string {
        $lines = ["Invoice #{$this->invoiceNumber}", "Customer: {$this->customer}", "Date: {$this->date}", ""];
        foreach ($this->items as $item) {
            $lines[] = sprintf("  %-30s $%8.2f", $item["description"], $item["amount"]);
        }
        $lines[] = str_repeat("-", 42);
        $lines[] = sprintf("  %-30s $%8.2f", "TOTAL", $this->getTotal());
        return implode("\n", $lines);
    }

    public function toHTML(): string {
        $rows = "";
        foreach ($this->items as $item) {
            $rows .= "<tr><td>{$item['description']}</td>"
                   . "<td>\${$item['amount']}</td></tr>";
        }
        return "<table><thead><tr><th>Item</th><th>Amount</th></tr></thead>"
             . "<tbody>{$rows}</tbody></table>";
    }

    // --- Implement Exportable ---
    public function toJSON(): string {
        return json_encode([
            "invoiceNumber" => $this->invoiceNumber,
            "customer"      => $this->customer,
            "date"          => $this->date,
            "items"         => $this->items,
            "total"         => $this->getTotal(),
        ], JSON_PRETTY_PRINT);
    }

    public function toCSV(): string {
        $lines = ["Description,Amount"];
        foreach ($this->items as $item) {
            $lines[] = "\"{$item['description']}\",{$item['amount']}";
        }
        $lines[] = "\"TOTAL\",{$this->getTotal()}";
        return implode("\n", $lines);
    }
}

$invoice = new Invoice("INV-001", "Acme Corp", "2026-04-17");
$invoice->addItem("Web Development", 2500.00);
$invoice->addItem("Hosting (Annual)", 300.00);
$invoice->addItem("SSL Certificate", 50.00);

echo $invoice->toString();
echo $invoice->toJSON();
echo $invoice->toCSV();

Type Hints with Interfaces

The real power of interfaces is using them as type hints. You can write functions that work with any class implementing the interface — without knowing or caring which specific class it is:

<?php
interface Loggable {
    public function toLogString(): string;
}

class UserAction implements Loggable {
    public function __construct(
        public string $username,
        public string $action,
        public string $timestamp,
    ) {}

    public function toLogString(): string {
        return "[{$this->timestamp}] {$this->username}: {$this->action}";
    }
}

class SystemEvent implements Loggable {
    public function __construct(
        public string $level,
        public string $message,
        public string $timestamp,
    ) {}

    public function toLogString(): string {
        return "[{$this->timestamp}] [{$this->level}] {$this->message}";
    }
}

// This function works with ANY Loggable — doesn't matter which class
function writeLog(Loggable $entry, string $file = "app.log"): void {
    file_put_contents($file, $entry->toLogString() . "\n", FILE_APPEND);
}

// Both work — they both implement Loggable
writeLog(new UserAction("alice", "logged in", "2026-04-17 10:30:00"));
writeLog(new SystemEvent("WARNING", "Disk usage at 85%", "2026-04-17 10:31:00"));

// Process a mixed collection
function writeAllLogs(array $entries): void {
    foreach ($entries as $entry) {
        if ($entry instanceof Loggable) {
            echo $entry->toLogString() . "\n";
        }
    }
}

Interface Constants

<?php
interface HttpStatusCodes {
    const OK = 200;
    const CREATED = 201;
    const NOT_FOUND = 404;
    const SERVER_ERROR = 500;
}

interface CacheDriver {
    const DEFAULT_TTL = 3600; // 1 hour in seconds

    public function get(string $key): mixed;
    public function set(string $key, mixed $value, int $ttl = self::DEFAULT_TTL): void;
    public function delete(string $key): void;
    public function has(string $key): bool;
}

Interfaces Can Extend Other Interfaces

<?php
interface Readable {
    public function read(): string;
}

interface Writable {
    public function write(string $data): void;
}

// Combine multiple interfaces into one
interface ReadWritable extends Readable, Writable {
    public function seek(int $position): void;
}

📖 Abstract Class vs. Interface

Feature Abstract Class Interface
Method bodies ✅ Can have concrete methods ❌ Method signatures only
Properties ✅ Yes ❌ Only constants
Constructors ✅ Yes ❌ No
Multiple ❌ Extend only one ✅ Implement many
Purpose Shared implementation + contract Contract only (what, not how)
When to use Related classes with shared code Unrelated classes with shared capability

Traits

PHP has single inheritance — a class can only extend one parent. But what if you have reusable behavior that doesn't fit into a single inheritance chain? Traits solve this problem. A trait is a collection of methods that can be "mixed in" to any class, regardless of its inheritance hierarchy.

Basic Trait Usage

<?php
// Define a trait — a reusable bundle of methods
trait Timestampable {
    public string $createdAt = "";
    public string $updatedAt = "";

    public function setCreatedAt(): void {
        $this->createdAt = date("Y-m-d H:i:s");
    }

    public function setUpdatedAt(): void {
        $this->updatedAt = date("Y-m-d H:i:s");
    }

    public function getTimestamps(): array {
        return [
            "created_at" => $this->createdAt,
            "updated_at" => $this->updatedAt,
        ];
    }
}

trait SoftDeletable {
    public ?string $deletedAt = null;

    public function softDelete(): void {
        $this->deletedAt = date("Y-m-d H:i:s");
    }

    public function restore(): void {
        $this->deletedAt = null;
    }

    public function isDeleted(): bool {
        return $this->deletedAt !== null;
    }
}

// Use traits in any class with the "use" keyword
class BlogPost {
    use Timestampable, SoftDeletable;

    public function __construct(
        public string $title,
        public string $content,
    ) {
        $this->setCreatedAt();
    }
}

class Comment {
    use Timestampable, SoftDeletable;

    public function __construct(
        public string $author,
        public string $text,
    ) {
        $this->setCreatedAt();
    }
}

// Both classes now have timestamp AND soft-delete behavior
$post = new BlogPost("Learning PHP", "PHP is awesome...");
echo $post->createdAt;      // "2026-04-17 14:30:00"

$post->softDelete();
echo $post->isDeleted();    // true
$post->restore();
echo $post->isDeleted();    // false

$comment = new Comment("Alice", "Great post!");
echo $comment->createdAt;   // same timestamp behavior

A Practical Logging Trait

<?php
trait HasLogger {
    private array $logs = [];

    public function log(string $level, string $message): void {
        $this->logs[] = [
            "level"     => $level,
            "message"   => $message,
            "timestamp" => date("Y-m-d H:i:s"),
            "class"     => static::class, // Which class logged this?
        ];
    }

    public function info(string $message): void {
        $this->log("INFO", $message);
    }

    public function warning(string $message): void {
        $this->log("WARNING", $message);
    }

    public function error(string $message): void {
        $this->log("ERROR", $message);
    }

    public function getLogs(): array {
        return $this->logs;
    }

    public function getLogsByLevel(string $level): array {
        return array_filter($this->logs, fn($log) => $log["level"] === $level);
    }
}

class OrderProcessor {
    use HasLogger;

    public function process(int $orderId): bool {
        $this->info("Processing order #$orderId");

        // Simulate processing...
        if ($orderId <= 0) {
            $this->error("Invalid order ID: $orderId");
            return false;
        }

        $this->info("Order #$orderId processed successfully");
        return true;
    }
}

$processor = new OrderProcessor();
$processor->process(42);
$processor->process(-1);

foreach ($processor->getLogs() as $log) {
    echo "[{$log['level']}] {$log['message']}\n";
}
// [INFO] Processing order #42
// [INFO] Order #42 processed successfully
// [INFO] Processing order #-1
// [ERROR] Invalid order ID: -1

Resolving Trait Conflicts

If two traits define a method with the same name, you must resolve the conflict explicitly:

<?php
trait Flyable {
    public function move(): string {
        return "Flying through the air";
    }
}

trait Swimmable {
    public function move(): string {
        return "Swimming through water";
    }
}

class Duck {
    // Both traits have move() — we must resolve the conflict
    use Flyable, Swimmable {
        Flyable::move as fly;       // Rename Flyable's move to fly
        Swimmable::move as swim;    // Rename Swimmable's move to swim
        Flyable::move insteadof Swimmable; // Default move() uses Flyable
    }
}

$duck = new Duck();
echo $duck->move(); // "Flying through the air" (Flyable won via insteadof)
echo $duck->fly();  // "Flying through the air" (alias)
echo $duck->swim(); // "Swimming through water" (alias)

⚠️ Traits Are Not Interfaces

A trait provides implementation (actual code). An interface provides a contract (method signatures only). You can't use instanceof to check if a class uses a trait — for type checking, use interfaces. A common pattern is to pair an interface with a trait that implements it.

Namespaces

As projects grow, you'll have many classes — and some will naturally have the same name (like User in your app and User in a third-party library). Namespaces organize classes into logical groups, preventing naming collisions — like folders organize files on disk.

Declaring a Namespace

<?php
// File: src/Models/User.php
namespace App\Models;

class User {
    public function __construct(
        public string $name,
        public string $email,
    ) {}
}

// File: src/Models/Product.php
namespace App\Models;

class Product {
    public function __construct(
        public string $name,
        public float $price,
    ) {}
}

// File: src/Services/AuthService.php
namespace App\Services;

class AuthService {
    public function login(string $email, string $password): bool {
        // Authentication logic...
        return true;
    }
}

Using Namespaced Classes

<?php
// File: src/index.php
namespace App;

// Option 1: Fully qualified name (verbose)
$user = new \App\Models\User("Alice", "alice@example.com");

// Option 2: Import with "use" (preferred)
use App\Models\User;
use App\Models\Product;
use App\Services\AuthService;

$user    = new User("Alice", "alice@example.com");
$product = new Product("Widget", 12.99);
$auth    = new AuthService();

// Option 3: Alias to resolve naming conflicts
use App\Models\User as AppUser;
use ThirdParty\Auth\User as AuthUser;

$appUser  = new AppUser("Alice", "alice@example.com");
$authUser = new AuthUser("alice@example.com", "token123");

// Group imports (PHP 7+)
use App\Models\{User, Product};
use App\Services\{AuthService, EmailService};

✅ Namespace Conventions

The standard convention (PSR-4) is to mirror your directory structure:

src/
├── Models/
│   ├── User.php          → namespace App\Models
│   └── Product.php       → namespace App\Models
├── Services/
│   ├── AuthService.php   → namespace App\Services
│   └── EmailService.php  → namespace App\Services
└── Controllers/
    └── HomeController.php → namespace App\Controllers

The top-level namespace (here App) maps to the src/ directory. This makes autoloading work seamlessly.

Autoloading

Without autoloading, you'd need require or include at the top of every file for every class you use. Autoloading tells PHP: "When you encounter a class you haven't loaded yet, use this function to find and load its file automatically."

Manual Autoloading with spl_autoload_register

<?php
// File: autoload.php
spl_autoload_register(function (string $className) {
    // Convert namespace separators to directory separators
    // App\Models\User → src/Models/User.php
    $path = __DIR__ . "/src/" . str_replace("\\", "/", $className) . ".php";

    // Remove the top-level "App/" if present
    $path = str_replace("/App/", "/", $path);

    if (file_exists($path)) {
        require_once $path;
    }
});

// Now classes load automatically when first used
// No require statements needed!
$user = new App\Models\User("Alice", "alice@example.com");
$auth = new App\Services\AuthService();

Composer Autoloading (The Standard Way)

In real projects, you'll use Composer (PHP's package manager) for autoloading. Composer handles the mapping automatically based on a composer.json configuration:

// composer.json
{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

// After running: composer dump-autoload
// Just include this one file:
require_once __DIR__ . "/vendor/autoload.php";

// Everything loads automatically!
use App\Models\User;
use App\Models\Product;
use App\Services\AuthService;

$user = new User("Alice", "alice@example.com");

📖 PSR-4 Autoloading

PSR-4 is the PHP community standard for autoloading. The rule is simple: the namespace maps directly to the directory structure. App\Models\Usersrc/Models/User.php. One class per file, filename matches class name. Every modern PHP framework and library follows this convention.

💡 For Now: You don't need to set up Composer yet — that's covered in Lesson 24 (Deployment & Next Steps). The key takeaway is understanding what autoloading does and why namespaces + PSR-4 naming conventions make it possible.

Choosing the Right Tool

With inheritance, abstract classes, interfaces, and traits in your toolbox, the question becomes: which one do you use when? Here's a decision guide.

flowchart TD A[Need to share code between classes?] --> B{Are the classes related?} B -->|Yes, IS-A relationship| C{Need shared implementation?} C -->|Yes| D[Use Inheritance or Abstract Class] C -->|No, just a contract| E[Use Interface] B -->|No, unrelated classes| F{Need implementation or just contract?} F -->|Implementation code| G[Use Trait] F -->|Just method signatures| E D --> H{Base class should be instantiable?} H -->|Yes| I[Regular class + extends] H -->|No| J[Abstract class + extends]

Quick Reference

Situation Use Example
"A Dog IS an Animal" extends class Dog extends Animal
"All shapes must calculate area, but each differently" Abstract class abstract class Shape { abstract public function area(): float; }
"Anything that can be serialized to JSON" Interface interface JsonSerializable { public function toJson(): string; }
"Many classes need timestamp tracking" Trait trait Timestampable { ... }
"Contract + default implementation" Interface + Trait Interface defines the contract, trait provides a default implementation

Combining All Three

In practice, you'll often combine these tools. Here's a common real-world pattern:

<?php
// Interface — defines the contract
interface Renderable {
    public function render(): string;
}

// Trait — provides reusable behavior
trait HasAttributes {
    private array $attributes = [];

    public function setAttribute(string $key, string $value): self {
        $this->attributes[$key] = $value;
        return $this;
    }

    public function getAttribute(string $key): ?string {
        return $this->attributes[$key] ?? null;
    }

    protected function renderAttributes(): string {
        $parts = [];
        foreach ($this->attributes as $key => $value) {
            $parts[] = "$key=\"$value\"";
        }
        return $parts ? " " . implode(" ", $parts) : "";
    }
}

// Abstract class — shared base for HTML elements
abstract class HtmlElement implements Renderable {
    use HasAttributes;

    abstract protected function getTagName(): string;
}

// Concrete classes — specific elements
class Paragraph extends HtmlElement {
    public function __construct(private string $text) {}

    protected function getTagName(): string {
        return "p";
    }

    public function render(): string {
        $tag = $this->getTagName();
        $attrs = $this->renderAttributes();
        return "<{$tag}{$attrs}>{$this->text}</{$tag}>";
    }
}

class Link extends HtmlElement {
    public function __construct(
        private string $text,
        private string $href,
    ) {
        $this->setAttribute("href", $this->href);
    }

    protected function getTagName(): string {
        return "a";
    }

    public function render(): string {
        $tag = $this->getTagName();
        $attrs = $this->renderAttributes();
        return "<{$tag}{$attrs}>{$this->text}</{$tag}>";
    }
}

// Usage — works with the Renderable interface
function renderAll(array $elements): string {
    $html = "";
    foreach ($elements as $el) {
        if ($el instanceof Renderable) {
            $html .= $el->render() . "\n";
        }
    }
    return $html;
}

$elements = [
    (new Paragraph("Hello, World!"))->setAttribute("class", "intro"),
    (new Link("PHP Manual", "https://php.net"))->setAttribute("target", "_blank"),
    (new Paragraph("Learning OOP is fun!"))->setAttribute("class", "content"),
];

echo renderAll($elements);

Output:

<p class="intro">Hello, World!</p>
<a href="https://php.net" target="_blank">PHP Manual</a>
<p class="content">Learning OOP is fun!</p>

Putting It All Together

Let's build a notification system that demonstrates inheritance, interfaces, traits, and namespaces working together.

<?php
// === INTERFACE: Defines what all notifications can do ===
interface Sendable {
    public function send(): bool;
    public function getRecipient(): string;
    public function getContent(): string;
}

// === TRAIT: Reusable retry logic ===
trait Retryable {
    private int $maxRetries = 3;
    private int $attempts = 0;

    public function setMaxRetries(int $max): self {
        $this->maxRetries = $max;
        return $this;
    }

    protected function executeWithRetry(callable $operation): bool {
        $this->attempts = 0;

        while ($this->attempts < $this->maxRetries) {
            $this->attempts++;
            try {
                if ($operation()) {
                    return true;
                }
            } catch (\Exception $e) {
                if ($this->attempts >= $this->maxRetries) {
                    throw $e;
                }
                // Wait before retrying (exponential backoff)
                usleep(100000 * $this->attempts); // 100ms × attempt
            }
        }
        return false;
    }

    public function getAttempts(): int {
        return $this->attempts;
    }
}

// === TRAIT: Logging for notifications ===
trait NotificationLogger {
    private array $log = [];

    protected function logSend(string $status, string $details = ""): void {
        $this->log[] = [
            "status"    => $status,
            "details"   => $details,
            "timestamp" => date("Y-m-d H:i:s"),
            "type"      => static::class,
        ];
    }

    public function getLog(): array {
        return $this->log;
    }
}

// === ABSTRACT BASE CLASS: Shared notification behavior ===
abstract class Notification implements Sendable {
    use Retryable, NotificationLogger;

    public function __construct(
        protected string $recipient,
        protected string $subject,
        protected string $message,
    ) {}

    public function getRecipient(): string {
        return $this->recipient;
    }

    public function getContent(): string {
        return "{$this->subject}: {$this->message}";
    }

    // Template method — defines the send workflow
    public function send(): bool {
        $this->logSend("attempting", "Sending to {$this->recipient}");

        $success = $this->executeWithRetry(function () {
            return $this->doSend(); // Each subclass implements the actual sending
        });

        $this->logSend(
            $success ? "success" : "failed",
            "After {$this->getAttempts()} attempt(s)"
        );

        return $success;
    }

    // Each notification type implements this differently
    abstract protected function doSend(): bool;
}

// === CONCRETE CLASSES ===

class EmailNotification extends Notification {
    protected function doSend(): bool {
        // In real code: use mail() or a library like PHPMailer
        echo "📧 Email to {$this->recipient}: {$this->subject}\n";
        return true; // Simulate success
    }
}

class SMSNotification extends Notification {
    protected function doSend(): bool {
        // In real code: use Twilio or similar API
        echo "📱 SMS to {$this->recipient}: {$this->message}\n";
        return true;
    }
}

class SlackNotification extends Notification {
    public function __construct(
        string $channel,
        string $message,
        private string $webhookUrl = "",
    ) {
        parent::__construct($channel, "Slack", $message);
    }

    protected function doSend(): bool {
        echo "💬 Slack to #{$this->recipient}: {$this->message}\n";
        return true;
    }
}

// === USAGE ===

// Create different notification types
$notifications = [
    new EmailNotification("alice@example.com", "Welcome!", "Thanks for joining."),
    new SMSNotification("+1234567890", "Verification", "Your code is 123456"),
    (new SlackNotification("general", "Deployment complete!"))->setMaxRetries(5),
];

// Send them all — polymorphism in action
foreach ($notifications as $notification) {
    $notification->send();
}

// Check logs
foreach ($notifications as $notification) {
    echo "\nLog for {$notification->getRecipient()}:\n";
    foreach ($notification->getLog() as $entry) {
        echo "  [{$entry['status']}] {$entry['details']}\n";
    }
}
classDiagram class Sendable { <<interface>> +send() bool +getRecipient() string +getContent() string } class Retryable { <<trait>> -int maxRetries -int attempts +setMaxRetries(int) self #executeWithRetry(callable) bool +getAttempts() int } class NotificationLogger { <<trait>> -array log #logSend(status, details) void +getLog() array } class Notification { <<abstract>> #string recipient #string subject #string message +send() bool #doSend()* bool } class EmailNotification { #doSend() bool } class SMSNotification { #doSend() bool } class SlackNotification { -string webhookUrl #doSend() bool } Sendable <|.. Notification : implements Notification <|-- EmailNotification Notification <|-- SMSNotification Notification <|-- SlackNotification Retryable <.. Notification : uses NotificationLogger <.. Notification : uses

Hands-On Exercises

🏋️ Exercise 1: Vehicle Hierarchy

Objective: Build a vehicle class hierarchy using inheritance and abstract methods.

Instructions:

  1. Create an abstract Vehicle class with: make, model, year (constructor promoted), an abstract fuelType(): string method, and a concrete describe(): string method
  2. Create GasCar extends Vehicle with fuelType() returning "Gasoline" and a mpg property
  3. Create ElectricCar extends Vehicle with fuelType() returning "Electric" and a rangeKm property
  4. Create HybridCar extends GasCar that overrides fuelType() to return "Hybrid (Gas + Electric)" and adds a batteryKwh property
  5. Write a function showFleet(array $vehicles) that prints each vehicle's description and fuel type
💡 Hint

HybridCar extends GasCar (which extends Vehicle), so it inherits both mpg and all of Vehicle's members. Use parent::__construct() at each level to pass values up the chain.

✅ Solution
<?php
abstract class Vehicle {
    public function __construct(
        public readonly string $make,
        public readonly string $model,
        public readonly int $year,
    ) {}

    abstract public function fuelType(): string;

    public function describe(): string {
        return "{$this->year} {$this->make} {$this->model}";
    }
}

class GasCar extends Vehicle {
    public function __construct(
        string $make,
        string $model,
        int $year,
        public float $mpg,
    ) {
        parent::__construct($make, $model, $year);
    }

    public function fuelType(): string {
        return "Gasoline";
    }
}

class ElectricCar extends Vehicle {
    public function __construct(
        string $make,
        string $model,
        int $year,
        public int $rangeKm,
    ) {
        parent::__construct($make, $model, $year);
    }

    public function fuelType(): string {
        return "Electric";
    }
}

class HybridCar extends GasCar {
    public function __construct(
        string $make,
        string $model,
        int $year,
        float $mpg,
        public float $batteryKwh,
    ) {
        parent::__construct($make, $model, $year, $mpg);
    }

    public function fuelType(): string {
        return "Hybrid (Gas + Electric)";
    }
}

function showFleet(array $vehicles): void {
    foreach ($vehicles as $v) {
        echo $v->describe() . " — Fuel: " . $v->fuelType() . "\n";
    }
}

$fleet = [
    new GasCar("Honda", "Civic", 2024, 36.0),
    new ElectricCar("Tesla", "Model 3", 2024, 560),
    new HybridCar("Toyota", "Prius", 2024, 57.0, 1.3),
];

showFleet($fleet);
// 2024 Honda Civic — Fuel: Gasoline
// 2024 Tesla Model 3 — Fuel: Electric
// 2024 Toyota Prius — Fuel: Hybrid (Gas + Electric)

var_dump($fleet[2] instanceof GasCar);   // true
var_dump($fleet[2] instanceof Vehicle);  // true

🏋️ Exercise 2: Interface + Trait Combo

Objective: Build a storage system using interfaces and traits.

Instructions:

  1. Create an interface Storable with methods: save(): bool, toArray(): array, static fromArray(array $data): static
  2. Create a trait HasId that provides: a readonly string $id property, and a static generateId(): string method (use uniqid())
  3. Create a Note class that uses HasId and implements Storable with properties: title, content, createdAt
  4. Implement save() to write to a JSON file (use the id as filename)
  5. Implement toArray() and fromArray() for serialization
💡 Hint

In fromArray(), create a new instance with the data from the array. Since it's a static method, use new static(...) or new self(...). For save(), use file_put_contents() with json_encode($this->toArray()).

✅ Solution
<?php
interface Storable {
    public function save(): bool;
    public function toArray(): array;
    public static function fromArray(array $data): static;
}

trait HasId {
    public readonly string $id;

    public static function generateId(): string {
        return uniqid("", true);
    }

    protected function initId(string $id = ""): void {
        $this->id = $id ?: self::generateId();
    }
}

class Note implements Storable {
    use HasId;

    public readonly string $createdAt;

    public function __construct(
        public string $title,
        public string $content,
        string $id = "",
        string $createdAt = "",
    ) {
        $this->initId($id);
        $this->createdAt = $createdAt ?: date("Y-m-d H:i:s");
    }

    public function save(): bool {
        $dir = "data/notes";
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        $json = json_encode($this->toArray(), JSON_PRETTY_PRINT);
        return file_put_contents("$dir/{$this->id}.json", $json) !== false;
    }

    public function toArray(): array {
        return [
            "id"        => $this->id,
            "title"     => $this->title,
            "content"   => $this->content,
            "createdAt" => $this->createdAt,
        ];
    }

    public static function fromArray(array $data): static {
        return new static(
            title: $data["title"],
            content: $data["content"],
            id: $data["id"] ?? "",
            createdAt: $data["createdAt"] ?? "",
        );
    }
}

// Create and save a note
$note = new Note("PHP OOP", "Interfaces and traits are powerful!");
$note->save();
echo "Saved note: {$note->id}\n";

// Reconstruct from array
$data = $note->toArray();
$restored = Note::fromArray($data);
echo "Restored: {$restored->title} (id: {$restored->id})\n";

🎯 Quick Quiz

Question 1: What keyword creates an inheritance relationship between two classes?

Question 2: How do you call the parent class's constructor from a child class?

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

Question 4: What problem do traits solve?

Question 5: What does the final keyword do on a method?

Summary

🎉 Key Takeaways

  • Inheritance (extends): Create child classes that inherit properties and methods from a parent. PHP supports single inheritance only.
  • Method overriding: Child classes can replace inherited methods with their own versions. Use parent::method() to call the parent's version.
  • final: Prevents a method from being overridden or a class from being extended.
  • Abstract classes: Cannot be instantiated directly. Can contain abstract methods (no body — children must implement) and concrete methods (with body — shared code).
  • Interfaces: Pure contracts — method signatures only, no implementation. A class can implement multiple interfaces.
  • Traits: Reusable bundles of methods that can be mixed into any class with use. Solve the lack of multiple inheritance.
  • Namespaces: Organize classes into logical groups (like App\Models) to prevent naming collisions.
  • Autoloading: Automatically loads class files based on namespace-to-directory mapping (PSR-4). Composer handles this in real projects.
  • Choosing the right tool: Use inheritance for IS-A relationships, interfaces for shared contracts, traits for shared implementation across unrelated classes.

📚 Additional Resources

🚀 What's Next?

With OOP under your belt, you're ready for Lesson 18: Error Handling. You'll learn to use try/catch/finally, create custom exception classes, and build robust error-handling strategies — the final piece before we connect PHP to MySQL with PDO.

🎉 Congratulations!

You've mastered the pillars of object-oriented PHP! With classes, inheritance, interfaces, and traits, you can build flexible, maintainable code architectures that scale with your projects.