Design Patterns in PHP

Course 3 · Ch 2
Design Patterns in PHP: Singleton, Factory, Strategy, Repository
Named, reusable solutions to recurring object-design problems

A design pattern is a named, well-understood solution to a problem that comes up repeatedly in object-oriented code. Knowing the name and shape of a few common patterns means recognising the problem they solve — and being able to communicate a design decision to another developer in a single word, rather than re-explaining the whole structure every time.

Singleton
Exactly one instance of a class, shared everywhere
Factory
Delegates object creation to a dedicated method/class
Strategy
Swap an algorithm/behaviour at runtime via a common interface
Repository
Hides data-storage details behind a simple, collection-like API

Singleton — Exactly One Instance

<?php class Config { private static ?Config $instance = null; private array $settings = []; private function __construct() { // private — cannot be called with "new" from outside $this->settings = ['env' => 'production']; } public static function getInstance(): Config { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } public function get(string $key) { return $this->settings[$key] ?? null; } } $config1 = Config::getInstance(); $config2 = Config::getInstance(); var_dump($config1 === $config2); // true — genuinely the SAME object both times ?>

The constructor is private, so new Config() is impossible from outside the class — the only way to get an instance is getInstance(), which creates it once and returns that same object on every subsequent call.

Singleton is genuinely controversial — use sparingly
It introduces global, shared state, which can make code harder to test (since a test can't easily substitute a different Config without extra effort) and creates a hidden dependency between any two classes that both call Config::getInstance(). Dependency injection (passing the needed object in explicitly, rather than reaching for a global singleton) is usually the better-regarded modern approach — Singleton is shown here because it's a genuinely common pattern to recognise, not necessarily one to reach for by default.

Factory — Delegating Object Creation

<?php interface Notification { public function send(string $message); } class EmailNotification implements Notification { public function send(string $message) { echo "Email: $message<br>"; } } class SmsNotification implements Notification { public function send(string $message) { echo "SMS: $message<br>"; } } class NotificationFactory { public static function create(string $type): Notification { return match ($type) { 'email' => new EmailNotification(), 'sms' => new SmsNotification(), default => throw new InvalidArgumentException("Unknown type: $type"), }; } } $notifier = NotificationFactory::create('email'); $notifier->send("Your order has shipped"); ?>

Calling code asks the factory for "an email notification" without needing to know — or directly reference — the specific EmailNotification class. This decouples the calling code from exactly which class gets instantiated, which becomes genuinely useful when that decision needs to vary (different config, different environment, a new notification type added later).

match — a cleaner alternative to a long switch for simple value-to-result mapping
PHP's match expression (used above) is similar to switch from Fundamentals Chapter 3, but uses strict comparison by default, requires no break, and is itself an expression — it directly returns a value, rather than needing variables assigned inside each case.

Strategy — Swappable Behaviour Behind a Common Interface

<?php interface DiscountStrategy { public function apply(float $price): float; } class PercentageDiscount implements DiscountStrategy { public function __construct(private float $percent) {} public function apply(float $price): float { return $price * (1 - $this->percent / 100); } } class FixedAmountDiscount implements DiscountStrategy { public function __construct(private float $amount) {} public function apply(float $price): float { return max(0, $price - $this->amount); } } class ShoppingCart { public function __construct(private DiscountStrategy $discount) {} public function checkout(float $price): float { return $this->discount->apply($price); } } $cart = new ShoppingCart(new PercentageDiscount(10)); echo $cart->checkout(100); // 90 ?>

ShoppingCart doesn't know or care which discount logic it's using — only that whatever it's given implements DiscountStrategy (Intermediate Chapter 2's interfaces, applied here). Swapping discount behaviour means passing a different object into the constructor; ShoppingCart itself never needs to change.

Repository — Hiding Storage Details Behind a Simple API

<?php interface UserRepository { public function find(int $id): ?array; public function save(array $user): void; } class MySqlUserRepository implements UserRepository { public function __construct(private PDO $pdo) {} public function find(int $id): ?array { $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = :id"); $stmt->execute(['id' => $id]); return $stmt->fetch() ?: null; } public function save(array $user): void { // INSERT or UPDATE via PDO — full SQL omitted for brevity } } // Calling code only ever depends on the INTERFACE: function greetUser(UserRepository $repo, int $id) { $user = $repo->find($id); echo "Hello, " . ($user['name'] ?? 'stranger'); } ?>

greetUser() works against the UserRepository interface — it has no idea whether data actually comes from MySQL, a JSON file, or an in-memory test array (a FakeUserRepository could be swapped in for testing). This is genuinely the same idea as Strategy, applied specifically to data access — and it's what makes automated testing of database-dependent code realistically feasible.

Coding Challenges

Challenge 1

Implement a Singleton class called Logger with a private array property for storing log lines, a static getInstance() method, and a public method log(string $message) that appends to the array. Demonstrate that calling Logger::getInstance() from two different points in the script still shares the same log entries.

📄 View solution
Challenge 2

Create a Shape interface with a getArea() method, two implementations (Circle, Square), and a ShapeFactory with a static create(string $type, ...$args) method using match() to construct the right one. Create one of each via the factory and echo their areas.

📄 View solution
Challenge 3

Create a SortStrategy interface with a sort(array $items): array method, and two implementations: AscendingSort and DescendingSort. Write a class Report that accepts a SortStrategy via its constructor and a generate(array $items) method using it. Demonstrate swapping strategies on two different Report instances with the same data.

📄 View solution

Chapter 2 Quick Reference

  • Singleton — private constructor + static getInstance(); exactly one shared instance; use sparingly, prefer dependency injection generally
  • Factory — a dedicated method/class for creating objects, decoupling calling code from specific classes
  • Strategy — swappable behaviour behind a common interface, injected via the constructor
  • Repository — hides data-storage details behind a simple, interface-based API; enables swapping real DB for a fake in tests
  • match expression — strict comparison, no break needed, returns a value directly
  • Next chapter: PHP and MVC architecture — building a simple framework from scratch