PUBLIC SERVICE UPDATE:
I've learned a lot since I originally posed this question. If you're reading this, please take my advice and avoid static
altogether. Just. Don't. Use. It. There is no way to dependency injection; dependency injection is the way.
I've recently spent a lot of time digging into various Inversion of Control (IOC) concepts. I totally agree with those who believe a Service Locator is an anti-pattern. I built one to tinker with and was aghast at the power it allowed for importing "global" entities in the middle of classes using static locator methods as well as the possibility for hiding the actual dependencies of an object.
Moving on from the service locator I set out to create a Dependency Injection (DI) container that gave me the flexibility of static dependency access without the concomitant drawbacks of static variables.
Here's a simple example of such an implementation:
<?php
class Container
{
protected static $params = [];
public function store($key, $val)
{
static::$params[$key] = $val;
return $this;
}
public function fetch($key)
{
if (isset(static::$params[$key])) {
return static::$params[$key];
}
$msg = "No parameter match found in container: $key";
throw new OutOfBoundsException($msg);
}
}
$container = new Container;
$container->store('widgetDep', new WidgetDependency);
$container->store('kumquatDep', new KumquatDependency);
// and somewhere else in the application without access to the global namespace
// (i.e. the $container instance we just created) ...
$widget = new Widget(new Container);
$kumquat = new Kumquat(new Container);
This seems a step in the right direction because the static $params
property is protected and no static methods exist to access or manipulate it in a "global" static scope: an object requires access to the container to access dependencies.
Unfortunately, storing dependencies in this container means that now every dependency-injected object has a faux-dependency on the container object, thus hiding its real dependencies. Another negative side-effect would be that every object would be given access to every available dependency in the container, and obviously, a Widget object shouldn't have access to a Kumquat object's dependencies. Also, using an abstract factory with this approach does nothing but move the fake dependency out of the Widget and Kumquat classes and into the factory.
With 5.4's new object construction dereferencing capabilities, we could do something like the following without needing access to the already created $container
instance that exists in the global namespace:
$widget = new Widget((new Container)->fetch('widgetDep'));
$kumquat = new Kumquat((new Container)->fetch('kumquatDep'));
Using this approach we've successfully:
Now, a possible drawback is that this approach means the developer must be disciplined enough to not pass a full Container
object as a dependency. This is crucial.
In two parts:
Container::$params
even necessary? Should it instead be a standard protected property accessed by top-of-the-object-graph factory classes/methods in the global namespace anyway (obviating the need for static
)?You shouldn't use static
here at all. Just create a container: $container = new DIContainer();
and use that object as a typical dependency. After all there's a very few places in the core of application that require access to the whole container.
Take a look at Symfony's Dependency Injection component - piece of quite good code.
According to the first comment. Yes, you've misunderstood me. Usually you'd need only several dependencies from the container, so you'll write something like:
$service = new Service($container->get('dep.a'), $container->get('dep.b'), 123);
My point was that you shouldn't use static property within the container, as it makes it nothing more but a global object. There would be no difference between:
global $container;
$widget = new Widget($container->fetch('widgetDep'));
$kumquat = new Kumquat($container->fetch('kumquatDep'));
$widget = new Widget(Container::getInstance()->fetch('widgetDep'));
$kumquat = new Kumquat(Container::getInstance()->fetch('kumquatDep'));
// You're using new objects but they share the same, **global** array.
// Therefore, they are actually global themselves.
$widget = new Widget((new Container())->fetch('widgetDep'));
$kumquat = new Kumquat((new Container())->fetch('kumquatDep'));
In other words, the Container itself should be a local variable, and if you'll need to access it somewhere else (some objects might need access to the entire container) then you should explicitly pass it as dependency to that object.
As I said before, take a look at Symfony DIC and the whole framework to see how to make a good, well-written DIC.
Simple container:
class Container {
private $services = array();
public function get($service) {
if (!array_key_exists($this->services, $service)) {
throw ...;
}
return $this->services[$service];
}
}
$containerA = new Container();
$containerB = new Container();
// $containerA and $containerB are completely different
// objects and don't share anything
I do not like the idea of creating a new Container and share a global array.
The solution of creating a facade object seems better to me:
class IoC
{
private static $container;
public static function Initialize ( IContainer $Container )
{
self::$container = $Container;
}
public static function Resolve( $type, array $parameters = array() )
{
return self::$container->Resolve( $type, $parameters );
}
}
In the bootstrap the IoC then can be initialized:
$container = new Container();
$container->Register( 'Logger', function() { return new Logger('somefile.log'); } );
IoC::Initialize ( $container );
And to use the container:
$log = IoC::Resolve( 'Logger' );
Imho a better solution then the symphony 'solution'. The container can easy be replaced by another implementation, without changing any of the other code. And for testing, just use a new instance of 'container', without the facade object.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With