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.";
}
}
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,
],
],
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);
}
}
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.");
}
}
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);
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);
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)
Why did you think it was framework related? Frameworks made it popular, especially when auto-wiring was introduced.
Right, DI isn't inherently a framework feature but frameworks made it more popular and accessible.