Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP Dependency Injection - Pimple, etc.. - Why use associative arrays vs getters?

We're looking at integrating a Dependency Injection Container into our project. Every DIC I've looked at uses associative arrays and/or magic methods. For example, here's a sample from the Pimple page:

$container['session_storage'] = function ($c) {
    return new $c['session_storage_class']($c['cookie_name']);
};

$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};

Is there a reason for this? I hate having strings in my code as anything other than a literal string that's going to be displayed somewhere. You lose so much of the power of the IDE (which makes the code harder to maintain, something we're trying to avoid!).

My preference would be something more like:

class Container {

    function getSessionStorage()
    {
        return new $this->getSessionStorageClass($this->getCookieName);
    }

    function getSession()
    {
        return new Session($this->getSessionStorage());
    }

}

Is there a reason not to do this? Am I missing some magic of Pimple that won't work if we go this route?

like image 234
Dan Avatar asked Feb 11 '13 13:02

Dan


4 Answers

The "magic" of the ArrayAccess extension in Pimple is that it's completely reusable and interoperable. One of the big features of Pimple as a DIC is that a defined service can make use of previously defined services and/or parameters. Let's say (for whatever reason) you had a Session object that required a Filter instance. Without a DIC you could write:

$session = new Session(new Filter);

With pimple you could write:

$pimple['filter'] = function($c) {
    return new Filter;
};
$pimple['session'] = function($c) {
    return new Session($c['filter']);
}

Pimple uses the previously registered 'Filter' service in the instantiation of the Session object. This benefit is not unique to a DIC that implements ArrayAccess, but the reusability is very useful for code reuse and sharing. You certainly can hard-code getters/setters for certain services, or all of them, but the benefit of reusability is all but lost.

The other option is to use magic methods as getters/setters. This will give the DIC an API more like what you want in your code, and you could even use them as a wrapper over the Pimple ArrayAccess code (though you might be better off writing a purpose-built DIC at that point). Wrapping over Pimple's existing methods could look something like this:

public function __call($method, $args) {
    if("set" === substr($method, 0, 3)) {
        return $this[substr($method, 3)];
    }
    if("get" === substr($method, 0, 3) && isset($args[0])) {
        return $this[substr($method, 3)] = $args[0];
    }
    return null;
}

You could also use __set and __get to give object-like access to the services & params in the DIC, like this: (still wrapping over Pimple's ArrayAccess methods)

public function __set($key, $value) {
    return $this[$key] = $value;
}

public function __get($key) {
    return $this[$key];
}

Beyond that you could rewrite the DIC entirely to use magic methods exclusively, and have an object-like API syntax instead of implementing ArrayAccess, but that should be fairly easy to figure out :]

like image 176
orourkek Avatar answered Nov 09 '22 14:11

orourkek


You care about IDE autocompletion because you are going to use your container as a Service locator, i.e. you are going to call your container.

You shouldn't do that ideally. The service locator pattern is an anti-pattern: instead of injecting the dependencies you need (dependency injection), you fetch them from the container. That means that your code is coupled to the container.

Pimple (and its array access) doesn't really solve that, so I'm not directly answering your question, but I hope it's making it clearer.


Side note: what's the "ideal" way? Dependency injection.

Never use or call the container, except at the root of your application (for example to create the controllers). Always inject the objects you need (the dependencies), instead of injecting the whole container.

like image 35
Matthieu Napoli Avatar answered Nov 09 '22 15:11

Matthieu Napoli


Pimple is designed to be accessed like an array (it implements the ArrayAccess interface). If you want a method-like interface instead, simply extend Pimple and use the __call() magic method:

class Zit extends Pimple
{
    public function __call($method, array $args)
    {
        $prefix = substr($method, 0, 3);
        $suffix = isset($method[3])
                ? substr($method, 3)
                : NULL;

        if ($prefix === 'get') {
            return $this[$suffix];
        } elseif ($prefix === 'set') {
            $this[$suffix] = isset($args[0])
                           ? $args[0]
                           : NULL;
        }
    }
}

Usage:

$zit = new Zit();

// equivalent to $zit['Foo'] = ...
$zit->setFoo(function() {
    return new Foo();
});

// equivalent to ... = $zit['Foo']
$foo = $zit->getFoo();

As for why Pimple doesn't come with this functionality out of the box, I have no idea. Probably just to keep it as simple as possible.


Edit:

Concerning IDE autocompletes, they also won't be available with magic methods like this. Some editors allow you to give doc-block hints to make up for this, using @property and @method, I believe.

like image 35
FtDRbwLXw6 Avatar answered Nov 09 '22 15:11

FtDRbwLXw6


Since you want hi-performance and keep configurability, the only option is to generate DI container code.

The simple option is to prepare methods you will need and write a generator. Something like this (untested code, for inspiration only):

$config_file = 'config.ini';
$di_file = 'var/di.php';
if (mtime($config_file) > mtime($di_file) // check if config changed
    || mtime(__FILE__) > mtime($di_file)  // check if generator changed
{ 
    $config = parse_ini_file($config_file, true); // get DI configuration
    ob_start(); // or use fopen($di_file) instead
    echo "<", "?php\n",
        "class DIContainer {\n";
    foreach ($config_file as $service_name => $service) {
        // generate methods you want, use configuration in $service as much as possible
        echo "function create", $service_name, "() {\n",
             "  return new ", $service['class'], "();\n\n";
    }
    echo "}\n";
    file_put_contents($di_file, ob_get_contents());
    ob_end_clean();
}

require($di_file);
$dic = new DIContainer();

Usage:

$service = $dic->createSomeService();
// Now you have instance of FooBar when example config is used

Example config file:

[SomeService]
class = "FooBar"

[OtherService]
class = "Dummy"
like image 1
Josef Kufner Avatar answered Nov 09 '22 16:11

Josef Kufner