Using Design Patterns in PHP Without Overdoing It
Design patterns are a tool, not a goal. When to use which pattern in PHP, and when they create unnecessary complexity.
When you first learn design patterns, you start seeing them everywhere. You’re writing a form handler and the Observer pattern pops into your head. You’re managing a database connection and Singleton seems like the obvious answer. It’s a natural impulse to apply a new concept wherever you can — but you won’t get real value from patterns until you move past that phase.
I went through it myself. I’ve seen codebases where every class mimics some pattern, yet none of them actually solve the underlying problem. The pattern itself was correct; the need just wasn’t there.
A Pattern Is a Solution, Not a Problem
A design pattern is a proven solution to a recurring problem in a specific context. All three parts of that definition matter: specific context, recurring problem, proven solution.
No context, no problem — no pattern.
Knowing the pattern language doesn’t mean you’re obligated to use every pattern at every opportunity. A mechanic having a screwdriver, a hammer, and pliers doesn’t mean they’ll drive a screw with the hammer.
Where They Actually Pay Off
There are a handful of patterns I’ve seen deliver real value in day-to-day PHP development.
Strategy: For encapsulating interchangeable algorithms. Payment methods, export formats, notification channels — these are classic Strategy use cases. When you need to add a new option, you write a new strategy class instead of touching existing code.
interface PaymentStrategy
{
public function charge(int $amount): bool;
}
class CreditCardPayment implements PaymentStrategy
{
public function charge(int $amount): bool
{
// Card processing
return true;
}
}
class BankTransferPayment implements PaymentStrategy
{
public function charge(int $amount): bool
{
// Bank transfer processing
return true;
}
}
class OrderProcessor
{
public function __construct(
private readonly PaymentStrategy $payment
) {}
public function process(int $amount): bool
{
return $this->payment->charge($amount);
}
}
This code naturally follows the Open-Closed Principle, but that’s not the main win. The main win: you no longer need real payment infrastructure to test payment logic.
Decorator: For layering behavior onto an existing object without changing it. Cache layers, logging, rate limiting — these wrap existing functionality. Because it works through composition rather than inheritance, it’s far more flexible.
Template Method: For defining the skeleton of an algorithm in a base class and letting subclasses fill in the steps. Laravel’s queued job classes use this pattern: you write the handle() method, the framework handles the rest.
Signs You’re Overdoing It
How do you know when you’ve overused a pattern?
First sign: you’ve written an abstract class or interface, there’s exactly one class implementing it, and you have no plans to add another. That’s a job for a file and a delete key, not an abstraction.
Second sign: when someone looks at the code and asks “why is it like this?” the answer is “I used a pattern.” The name of the pattern is not a justification. What problem it solves is.
Third sign: you’ve built a class hierarchy for something a simple function could handle. Not everything in PHP needs to be an object. A transformation or a calculation is sometimes just a function.
// Unnecessary factory class
class SlugFactory
{
public function make(string $title): string
{
return strtolower(str_replace(' ', '-', $title));
}
}
// Good enough
function make_slug(string $title): string
{
return strtolower(str_replace(' ', '-', $title));
}
A PHP-Specific Note
In PHP, frameworks like Laravel or Symfony already provide many patterns out of the box. The service container manages dependency injection. The event system covers Observer. Trying to build these yourself usually means reinventing what the framework already gives you.
There’s an important distinction between using these patterns through the framework and writing them from scratch. Before doing the latter, make sure the framework hasn’t already solved the need.
Patterns are a shared communication language among experienced developers. Saying “I used the Strategy pattern here” is far more efficient than explaining a design decision in paragraphs. That value is real. But learning the language doesn’t mean stuffing every sentence with technical jargon — it means knowing the right word when you actually need it.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.