Subscribe to PHP Freaks RSS

Road to dependency injection

syndicated from planet-php.net on June 12, 2018

Statically fetching dependencies



I've worked with several code bases that were littered with calls to Zend_Registry::get(), sfContext::getInstance(), etc. to fetch a dependency when needed. I'm a little afraid to mention façades here, but they also belong in this list. The point of this article is not to bash a certain framework (they are all lovely), but to show how to get rid of these "centralized dependency managers" when you need to. The characteristics of these things are:



  • They offer global access; they are not internal to classes, but external, and thereby reachable from any scope.
  • They offer static access; they don't require you to have an instance of the object.

This means they can and will be called all over the place. If someone feels the need to use the translator service in a domain model, they can simply call Zend_Registry::get('Zend_Translate'). If they need to send an e-mail in a JSON mapper, there's sfContext::getInstance()->getMailer().



Part of the issue is that these centralized dependency managers provide access to other things than services, like user session data, request parameters, etc. (see also my previous article about this topic - "Context passing"). But when it comes to fetching services from these global service registries/locators, the main issue is that we're coupling all our code to this specific singleton thing.



Fetching the service locator



When trying to get rid of that-framework-that-was-totally-it-in-2012, we are in big trouble because of all that coupling. The setup of our services is not completely under our control. So, when migrating to a more recent service container implementation, like Symfony's dependency injection container, the first migration step might be to reuse the old mechanism for retrieving dependencies, making the new service container available to all the old code:



/*
 * At some point, we'll build and boot the Symfony service container.
 * We then inject it right into the old `Zend_Registry`.
 */
$container = ...;
Zend_Registry::set('container', $container);

/* * Anywhere else, we can retrieve the container and get a service from it. */ $container = Zend_Registry::get('container'); $translator = $serviceLocator->get('translator');


Since we can now define the translator service in any way that the dependency injection component of our choice supports, we can now migrate all existing uses of Zend_Registry::get('Zend_Translate') to the slightly better Zend_Registry::get('container')->get('translator').



"Service container" vs "service locator"



It's good to point out that the code in the example above uses the service container as a service locator. This is by far inferior, since we're still fetching services manually. This requires us to know the service ID and be aware of the service retrieval mechanism itself. Dependency injection is more useful if you leave all the instantiation details and dependency resolving to the service container (also known as "dependency injection container") itself.



Injecting the service locator



Ideally, the framework should be able to route a request to a controller, defined as a service in the service container (or dependency injection container). I know that Symfony is capable of that. It would retrieve the fully instantiated controller, and no other object would ever need the service locator anymore. But this isn't how most frameworks work (at least the older ones). So we need to be a bit more realistic. As an example, most applications still have multiple actions per controller class. Since all these actions use a different set of dependencies, the controller gets the service locator injected, and the actions can all take out the services they need.



So, as a compromise, where should we allow the use of a service locator?



  • Inside controllers for (web or CLI)
  • Inside controller helpers (in the case of Zend Framework), since they are still close to the infrastructure/outer boundary of the application.

Definitely not in any other service, only as an intermediate step on the road to dependency injection.



Performance



The question always seems to be: if we inject everything upfront, this means we also need to instantiate everything upfront. Some dependency injection components allow lazy loading for this. What gets injected is a proxy of the real object, that still satisfies any type-hints used. Once a met

Truncated by Planet PHP, read more at the original (another 8689 bytes)