Learning Dependency Injection in PHP Was a Game Changer. Here's Why
Sayed Naweed Rizvi

Sayed Naweed Rizvi @navedrizv

About: Software Engineer, Cloud Architect, DevOps

Location:
India
Joined:
May 27, 2021

Learning Dependency Injection in PHP Was a Game Changer. Here's Why

Publish Date: Jul 3
0 2

Back in around 2013, I was neck-deep in a project where we had to upgrade a legacy PHP application built on Zend Framework. It was a mix of old-school PHP, some custom abstractions, and a whole lot of things just glued together to work.

At the time, I had heard about Dependency Injection (DI) in passing — mostly in conversations but during this upgrade, DI went from a buzzword to something I couldn’t live without.


What We Started With

The legacy codebase followed a pretty procedural approach. Controllers would instantiate models, helpers, and services directly — like this:

class ReportController extends Zend_Controller_Action {
    public function generateAction() {
        $db = new Database();
        $user = $db->getUser($this->_getParam('user_id'));

        $mailer = new Mailer();
        $mailer->send($user->email, "Your report is ready");

        echo "Report generated.";
    }
}
Enter fullscreen mode Exit fullscreen mode

Everything was tightly coupled. There was no way to replace the Mailer or Database without modifying the controller directly.

When we began upgrading to Zend Framework 2, it came with a proper ServiceManager — Zend’s way of introducing Dependency Injection Containers. That’s where things clicked for me.


My First Real Refactor

Instead of hardcoding dependencies, we started defining services in the module.config.php like this:

'service_manager' => [
    'factories' => [
        Mailer::class => MailerFactory::class,
        ReportService::class => ReportServiceFactory::class,
    ],
],
Enter fullscreen mode Exit fullscreen mode

And in the ReportServiceFactory:

class ReportServiceFactory implements FactoryInterface {
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null) {
        $db = $container->get(Database::class);
        $mailer = $container->get(Mailer::class);
        return new ReportService($db, $mailer);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, ReportService just looked like this:

class ReportService {
    protected $db;
    protected $mailer;

    public function __construct(Database $db, Mailer $mailer) {
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function generate($userId) {
        $user = $this->db->getUser($userId);
        $this->mailer->send($user->email, "Report ready.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Changed How I Write Code

Looking back, this was an important lesson I learned as a PHP developer. Here's why:

✅ It Made Testing Feasible

Before, testing ReportService meant spinning up a DB and suppressing email sending. Now, I could inject mocks and focus purely on logic:

$mockDb = new MockDatabase();
$mockMailer = new SpyMailer();

$service = new ReportService($mockDb, $mockMailer);
$service->generate(101);
Enter fullscreen mode Exit fullscreen mode

No real database. No real emails. Just clean, fast unit tests.


✅ It Forced Better Design

I started thinking in interfaces instead of concrete classes. When you pass dependencies in, you start asking: “What should this class really depend on?” That naturally pushed me toward Single Responsibility Principle and cleaner separation of concerns.


✅ It Simplified Maintenance

When a new requirement came in — say, queue emails instead of sending them instantly — I didn’t have to touch ReportService. I just changed the binding in the factory:

$container->set(Mailer::class, QueueMailer::class);
Enter fullscreen mode Exit fullscreen mode

Zero changes to business logic. That felt like magic.


Dependency Injection Isn’t a Framework Feature — It’s a Design Choice

I used to think DI was something Laravel or Symfony gave you. But in reality, it’s a principle: don’t create your dependencies; accept them from the outside.

Zend just gave me the tools to see what was possible — and once I got it, I started applying the same approach even in plain PHP projects.


Final Thoughts

That Zend upgrade taught me more about architecture, at the time, we were just trying to get the app to a newer version — but what I walked away with was a deeper appreciation for how good architecture enables flexibility, testability, and long-term maintainability.

Comments 2 total

  • david duymelinck
    david duymelinckJul 3, 2025

    Why did you think it was framework related? Frameworks made it popular, especially when auto-wiring was introduced.

    • Sayed Naweed Rizvi
      Sayed Naweed RizviJul 4, 2025

      Right, DI isn't inherently a framework feature but frameworks made it more popular and accessible.

Add comment