Dependency Injection Containers are great for managing small, known numbers of services. For managing a larger set of services that needs to be discoverable, another solution is necessary.
Dependency Injection is a really good thing. By now, most developers will agree with that. It makes unit testing possible and it's a vital aspect of good, solid design. Dependencies Injection Containers play a big role in many frameworks and applications, taking care of passing on dependencies across modules, or from framework to module.
At Procurios, we have about four hundred modules, most of which expose some services (we call those API's). These services are often retrieved by other modules to work with. For example, many modules use the framework's StringApi to convert the character set of inbound data.
Most, if not all, DIC solutions boil down to a container class that acts as a key-value store. In our case you could imagine something like
$Container->get('lib', 'StringApi');
This poses a significant problem, though: how do you know which services are exposed?
To make development easy enough, we need a way to auto-complete which services a module exposes. So we opted for a static ApiFactory in every module, which only shows the API's it provides:
$StringApi = LibApiFactory::getStringApi();
Controllers can pick up these services, destroying dependency injection in the process.
Testing becomes very difficult with controllers pulling in their dependencies themselves. Integration testing is still possible, but unit testing is not. So in order to allow for unit testing, we null-pass the dependencies to the constructor of the controller, creating them if we did not receive them. This way we salvage auto-completion, strong typing (over string typing) and DI!
public function __construct(StringApi $StringApi = null)
{
$this->StringApi = $StringApi ?: LibApiFactory::getStringApi();
}