PHP
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
79: The Factory Pattern in PHP
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:
- Create a
ShippingCarrierInterfacethat defines thecalculateCostmethod. - 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.). - Create a
ShippingCarrierFactoryclass with acreateCarrier(string $carrierName)method that returns the appropriate instance. - Implement a
ShippingServiceclass 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.
There are no comments for now.