Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does PHP 5.4 object dereferencing successfully mitigate the drawbacks of static storage parameter in this DI container?

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.

Oh, wait ...

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.

A PHP 5.4 alternative

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:

  1. Eliminated the container dependency from the Widget and Kumquat objects, allowing their constructors to typehint the specific dependency objects they require;
  2. Prevented the downstream Widget and Kumquat objects having access to dependencies they shouldn't know exist;
  3. Retained static dependency storage capabilities.

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.

So the question is ...

In two parts:

  1. What concrete drawbacks do you see with this approach, and
  2. Is the static 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)?
like image 374
rdlowrey Avatar asked Feb 07 '12 16:02

rdlowrey


2 Answers

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.


EDIT:

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
like image 195
Crozin Avatar answered Sep 18 '22 23:09

Crozin


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.

like image 21
JvdBerg Avatar answered Sep 20 '22 23:09

JvdBerg