-
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
75: Practice Exercise: Building an Interface-Based Payment Gateway System
I've seen this happen in countless code reviews. A developer decides to use an interface to "do things the right way," but then they accidentally write code that completely defeats the purpose of that interface. Let's look at a snippet of a payment system that looks correct at first glance but is fundamentally broken from an architectural standpoint.
interface PaymentGateway {
public function processPayment(float $amount): bool;
}
class StripeGateway implements PaymentGateway {
public function processPayment(float $amount): bool {
echo "Processing \${$amount} via Stripe...";
return true;
}
}
class PayPalGateway implements PaymentGateway {
public function processPayment(float $amount): bool {
echo "Processing \${$amount} via PayPal...";
return true;
}
}
class CheckoutService {
private $gateway;
// Here is the mistake
public function __construct(StripeGateway $gateway) {
$this->gateway = $gateway;
}
public function execute(float $amount) {
return $this->gateway->processPayment($amount);
}
}
$stripe = new StripeGateway();
$checkout = new CheckoutService($stripe);
$checkout->execute(100.00);
The Tight Coupling Trap
If you run the code above, it works perfectly. But here is the problem: we spent all that time defining a PaymentGateway interface, yet the CheckoutService specifically demands a StripeGateway in its constructor.
I call this "Interface Theater." We're pretending to use interfaces, but we've actually hard-coded our dependency. If the business decides to switch to PayPal tomorrow, we can't just swap the object passed into the constructor. We would have to go into the CheckoutService class and manually change the type-hint from StripeGateway to PayPalGateway. That is exactly the kind of manual, error-prone work interfaces are meant to eliminate.
Coding to the Interface, Not the Implementation
To fix this, we need to stop the CheckoutService from caring about which gateway is being used. It should only care that whatever object it receives is a PaymentGateway.
class CheckoutService {
private $gateway;
// Fix: Type-hint the interface, not the concrete class
public function __construct(PaymentGateway $gateway) {
$this->gateway = $gateway;
}
public function execute(float $amount) {
return $this->gateway->processPayment($amount);
}
}
// Now we can swap gateways without touching the CheckoutService code
$gateway = (rand(0, 1) == 0) ? new StripeGateway() : new PayPalGateway();
$checkout = new CheckoutService($gateway);
$checkout->execute(100.00);
By changing that one word in the constructor, we've decoupled the service from the implementation. The CheckoutService is now "blind" to the specific vendor; it just knows that any object passed to it is guaranteed to have a processPayment method. This is the core of the Dependency Inversion Principle.
Designing for Extensibility
When you're building these systems, I always recommend thinking about the "Third Vendor Scenario." If you have Stripe and PayPal, it's easy to think you can just use an if/else block. But what happens when you add Square, Apple Pay, and Braintree?
If you've coded to the interface, adding a new provider takes exactly two steps:
- Create a new class that implements the interface.
- Pass that new class into your service.
CheckoutService again. That's the difference between a codebase that feels like a house of cards and one that feels like a professional tool.📋 Practical Task
Exercise: Building a Multi-Vendor Payment Orchestrator
Your goal is to build a flexible payment system that can handle multiple providers and a "logging" wrapper to track transactions.
Requirements:
- Create a
PaymentProviderinterface with a methodpay(float $amount): string. - Implement three concrete classes:
StripeProvider,PayPalProvider, andSquareProvider. Each should return a unique string (e.g., "Paid $100 via Stripe"). - Create a
PaymentManagerclass. This class should have a constructor that accepts thePaymentProviderinterface. - The
PaymentManagershould have a method calledprocessOrder(float $amount)that calls the provider'spaymethod and returns the result. - The Challenge: Create a
PaymentLoggerclass that also implementsPaymentProvider. This class should take anotherPaymentProviderin its constructor (the "real" provider), call that provider'spaymethod, and then prepend "[LOG]: " to the resulting string before returning it.
Test your code by:
- Instantiating a
SquareProvider. - Wrapping that provider inside the
PaymentLogger. - Passing that logger into the
PaymentManager. - Calling
processOrder(50.00)and ensuring the output is "[LOG]: Paid $50 via Square".
There are no comments for now.