Factory Method
As an application grows, code that decides which class to instantiate tends to spread
everywhere — a sprinkling of if/else or switch statements, each picking a
concrete class based on some type flag. Every time a new type is added, every one of those scattered
blocks has to be found and updated. The Factory Method pattern fixes this by giving
object creation a single, well-defined home: an interface (or method) whose whole job is deciding
which concrete class to build, so the rest of the code never has to know or care.
The Problem: Scattered if/else Creation Logic
Imagine an app that needs to send notifications by email or SMS, chosen by a $type string
coming from user preferences. A natural first attempt puts the decision right where the notification
is sent:
// Bad: every call site that sends a notification repeats this logic
function sendUserAlert(string $type, string $message) {
if ($type === 'email') {
$notification = new EmailNotification();
} elseif ($type === 'sms') {
$notification = new SmsNotification();
} else {
throw new InvalidArgumentException("Unknown type: $type");
}
$notification->send($message);
}
This same if/elseif block gets copy-pasted into every place the app sends a notification.
Add a push notification type later, and every single copy needs to be tracked down and
updated — a maintenance trap, and a direct violation of the
Open/Closed Principle (code should be open for extension but closed for modification).
The Factory Method Version
Instead, define a shared Notification interface, a concrete class per channel, and a
single NotificationFactory whose only job is deciding which one to build.
<?php
interface Notification {
public function send(string $message): void;
}
class EmailNotification implements Notification {
public function send(string $message): void {
mail('user@example.com', 'Alert', $message);
}
}
class SmsNotification implements Notification {
public function send(string $message): void {
// Call out to an SMS gateway API here
SmsGateway::send('+1555000123', $message);
}
}
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"),
};
}
}
// Client code no longer knows or cares about concrete classes
function sendUserAlert(string $type, string $message) {
$notification = NotificationFactory::create($type);
$notification->send($message);
}
PushNotification channel, you write a new class
implementing Notification and add one line inside NotificationFactory::create().
Every call site that already calls sendUserAlert() needs zero changes — they were only ever
coupled to the Notification interface, not to any concrete class. That's the Open/Closed
Principle in practice: the system is open to new notification types, closed to modifying existing,
working client code.
The "Method" in Factory Method
In the strict GoF definition, Factory Method is usually a method that subclasses override — an
abstract Creator class declares a createProduct() method, and each concrete
subclass overrides it to return a different product. The static-factory style shown above is a
common, pragmatic PHP variant of the same idea: a single place decides which concrete class to
instantiate, keeping that decision out of client code either way.
Where You'll See This in Real Frameworks
Laravel's Illuminate\Notifications and queue "driver" systems work this way — you ask
for a mail driver or a queue connection by name, and a factory-style method resolves the correct
concrete class. Doctrine's DriverManager similarly picks a concrete database driver class
based on a connection's configured driver name, keeping calling code free of vendor-specific
new calls.