Skip to Content
Course content

79: The Factory Pattern in PHP

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably been there: you're building a feature that needs to handle a few different variations of the same thing. Maybe it's different payment gateways, different notification channels, or different file exporters. When you first start, the instinct is to keep it simple. You just check a value and instantiate the class you need right there in your business logic.

The temptation to just use a switch statement

Let's say we're building a notification system. We support Email and SMS. In a naive implementation, your code probably looks something like this:

public function notifyUser(User $user, string $method, string $message) 
{
    if ($method === 'email') {
        $notifier = new EmailNotifier($this->emailApiKey);
    } elseif ($method === 'sms') {
        $notifier = new SmsNotifier($this->smsApiKey);
    } else {
        throw new Exception("Unsupported notification method");
    }

    $notifier->send($user, $message);
}

At first glance, this is perfectly readable. It works. But here is the problem: your high-level business logic is now tightly coupled to the specific implementation details of EmailNotifier and SmsNotifier. You aren't just telling the system to "send a notification"; you're managing the specific requirements (like API keys) for every single delivery method inside your main service.

When the 'new' keyword becomes a liability

The real pain starts when the project grows. Imagine your boss tells you that you now need to support Slack, WhatsApp, and Push notifications. Suddenly, your notifyUser method is a massive switch statement. Worse, if the SmsNotifier constructor changes—maybe it now requires a timeout setting or a logger—you have to hunt down every single place in your codebase where you wrote new SmsNotifier() and update it manually.

I call this "leaky abstraction." The knowledge of how to build the object has leaked into the place where the object is actually being used. In a professional codebase, we want to separate the creation of an object from its execution.

Moving the creation logic into a dedicated Factory

This is where the Factory Pattern comes in. Instead of your service worrying about API keys and class names, it asks a Factory to handle the dirty work. First, we ensure all our notifiers follow the same contract via an interface.

interface NotifierInterface {
    public function send(User $user, string $message): void;
}

class NotifierFactory {
    public function __construct(
        private string $emailApiKey, 
        private string $smsApiKey
    ) {}

    public function create(string $method): NotifierInterface {
        return match($method) {
            'email' => new EmailNotifier($this->emailApiKey),
            'sms'   => new SmsNotifier($this->smsApiKey),
            'slack' => new SlackNotifier('default_webhook_url'),
            default => throw new InvalidArgumentException("Unknown method: $method"),
        };
    }
}

Now, look at how clean your business logic becomes. Your service no longer knows how to build a notifier; it just knows that it can get one from the factory.

public function notifyUser(User $user, string $method, string $message) 
{
    $notifier = $this->notifierFactory->create($method);
    $notifier->send($user, $message);
}

Is the extra boilerplate actually worth it?

I'll be honest with you: for a tiny project, this is overkill. You've added an interface and a new class just to avoid a few if statements. But in a real-world application, the trade-off is almost always worth it for two reasons: testing and maintenance.

If you want to write a unit test for notifyUser, you no longer have to worry about actual API keys being passed into real notifier classes. You can simply mock the NotifierFactory to return a fake NotifierInterface. You've isolated your business logic from your infrastructure. When the business decides to switch from one SMS provider to another, you change one line of code in the Factory, and the rest of your application remains blissfully unaware of the change.




📋 Practical Task

Implementation: Logistics Carrier Factory

You are building a shipping module for an e-commerce store. The store uses three different carriers: FedEx, UPS, and DHL. Each carrier has a different method for calculating shipping costs (calculateCost(float $weight)).

Your task is to refactor a messy shipping service using the Factory Pattern:

  1. Create a ShippingCarrierInterface that defines the calculateCost method.
  2. Create three concrete classes (FedExCarrier, UpsCarrier, DhlCarrier) that implement this interface with different dummy calculation logic (e.g., FedEx multiplies weight by 1.2, UPS by 1.5, etc.).
  3. Create a ShippingCarrierFactory class with a createCarrier(string $carrierName) method that returns the appropriate instance.
  4. Implement a ShippingService class that takes the factory in its constructor and uses it to calculate the cost for a given package weight based on a user's carrier choice.

Ensure that if an invalid carrier name is passed to the factory, it throws an InvalidArgumentException.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.