DEV Community

Sayed Naweed Rizvi
Sayed Naweed Rizvi

Posted on

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

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.

Top comments (2)

Collapse
 
xwero profile image
david duymelinck

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

Collapse
 
navedrizv profile image
Sayed Naweed Rizvi

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