Skip to Content
Course content

137: Dependency Injection in PHP

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

Imagine you're building a high-end coffee machine. If you weld the coffee bean hopper directly to the internal chassis and fill it with one specific brand of dark roast, you've built a machine that only makes one kind of coffee. If you suddenly want a light roast, or if that brand of coffee goes out of business, you have to take a blowtorch to your machine and rebuild the whole thing from scratch.

A better way to build it is to create a slot where the user can slide in any bean container they want. The machine doesn't care who produced the beans or what roast they are, as long as the container fits the slot. That's the essence of Dependency Injection (DI): instead of a class "reaching out" to create its own tools, you "inject" those tools into the class from the outside.

The trap of the "new" keyword

In PHP, we often fall into the trap of instantiating dependencies right inside our constructors. I've seen this in countless legacy codebases. It looks like this:

class UserRegistration {
    private $mailer;

    public function __construct() {
        // This is the "welded hopper" approach
        $this->mailer = new SendGridMailer(); 
    }

    public function register($email) {
        // ... logic to save user ...
        $this->mailer->sendWelcomeEmail($email);
    }
}

At first glance, this seems fine. It's simple. But notice how UserRegistration is now married to SendGridMailer. If you want to switch to Mailchimp, or if you want to write a unit test and don't actually want to send a real email to a customer every time the test runs, you're stuck. You can't change the mailer without changing the UserRegistration code itself.

Handing the tools to the class

To fix this, we stop the class from deciding which mailer to use. Instead, we tell it: "You need a mailer to function; I'll give you one when I create you."

class UserRegistration {
    private $mailer;

    // We "inject" the dependency through the constructor
    public function __construct(MailerInterface $mailer) {
        $this->mailer = $mailer;
    }

    public function register($email) {
        // ... logic to save user ...
        $this->mailer->sendWelcomeEmail($email);
    }
}

Now, look at the difference. UserRegistration no longer cares how the email is sent. It just knows that whatever object it receives will have a sendWelcomeEmail method. I've found that this one shift in thinking is usually the "aha!" moment for developers moving from script-writing to software engineering.

Why the Interface is the secret sauce

You'll notice I used MailerInterface in the type-hint above. This is crucial. If I just type-hinted SendGridMailer, I'd still be tied to that specific class. By using an interface, I'm defining a contract.

Here is how that maps back to our coffee machine:

  • The Interface: The shape and size of the bean slot. It defines what is required, not how it's made.
  • The Implementation: The actual beans (SendGrid, Mailchimp, or a "Mock" mailer for testing).
  • The Injection: The act of sliding the beans into the slot.

When it comes time to actually run the code, you handle the instantiation outside the class:

$mailer = new SendGridMailer('api-key-123');
$registration = new UserRegistration($mailer);
$registration->register('dev@example.com');

If you're running a test, you just swap one line: $mailer = new MockMailer();. Your UserRegistration class remains untouched, untested, and blissfully unaware that it's not sending real emails. That is the power of decoupling.




📋 Practical Task

Refactoring the Payment Gateway to Support Multiple Providers

You have a Checkout class that is currently hard-coded to use a StripePaymentProcessor. This is causing problems because the business wants to add PayPal support for international customers.

Your Task:

  1. Create a PaymentProcessorInterface that defines a single method: processPayment(float $amount): bool.
  2. Modify the StripePaymentProcessor class to implement this interface.
  3. Create a new PayPalPaymentProcessor class that also implements the interface.
  4. Refactor the Checkout class so that it no longer calls new StripePaymentProcessor() in its constructor. Instead, it should accept a PaymentProcessorInterface via dependency injection.
  5. Demonstrate the flexibility by instantiating the Checkout class twice: once using Stripe and once using PayPal.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.