🧬 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
extendsand 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
⚠️ 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\User → src/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.
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";
}
}
Hands-On Exercises
🏋️ Exercise 1: Vehicle Hierarchy
Objective: Build a vehicle class hierarchy using inheritance and abstract methods.
Instructions:
- Create an abstract
Vehicleclass with:make,model,year(constructor promoted), an abstractfuelType(): stringmethod, and a concretedescribe(): stringmethod - Create
GasCar extends VehiclewithfuelType()returning"Gasoline"and ampgproperty - Create
ElectricCar extends VehiclewithfuelType()returning"Electric"and arangeKmproperty - Create
HybridCar extends GasCarthat overridesfuelType()to return"Hybrid (Gas + Electric)"and adds abatteryKwhproperty - 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:
- Create an interface
Storablewith methods:save(): bool,toArray(): array,static fromArray(array $data): static - Create a trait
HasIdthat provides: areadonly string $idproperty, and a staticgenerateId(): stringmethod (useuniqid()) - Create a
Noteclass that usesHasIdand implementsStorablewith properties:title,content,createdAt - Implement
save()to write to a JSON file (use the id as filename) - Implement
toArray()andfromArray()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
- PHP Manual: Inheritance
- PHP Manual: Abstract Classes
- PHP Manual: Interfaces
- PHP Manual: Traits
- PHP Manual: Namespaces
- PSR-4: Autoloading Standard
🚀 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.