Dependency Injection
Purpose of the Dependecy Injection (DI) is to free classes from the responsibility for obtaining objects that they need for its operation (these objects are called services). To pass them these services on their instantiation instead. We'll talk over:
- What is the principle of the Dependency Injection.
- How to create dynamic and static DI containers.
- How to lazy-load services.
What is Dependency Injection?
Dependency Injection (DI) is nothing mysterious or baffling. It can be comprehended into one selfish sentence: “Don't seek
for anything, let someone else do it.” Now let's translate this into programmers' speech. We have a class
Article
that represents a blog post:
class Article
{
public $id;
public $title;
public $content;
function save()
{
// we'll save into the database
}
}
and the usage is this:
$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();
Method save()
saves the article into the database table articles
. It's easy to implement using Nette Database, except one thing: Where is Article
supposed to get the database
connection, ie. an object of the class Connection?
Well, we can place it into some global variable like $GLOBALS['connection']
or into some static member of a class.
But haven't you heard that use of global variables is bad? It's true, global variables are evil and static members are exactly
the same.
So where else will we get the database connection? DI has the answer: “Don't seek for anything, let someone else do it.” In other words, if I need a database, someone give it to me, it's not my job. Hah, it's devious, dear DI! Let's do it:
class Article
{
public $id;
public $title;
public $content;
private $database;
function __construct(Nette\Database\Connection $database)
{
$this->database = $database;
}
function save()
{
$this->database->query('INSERT INTO articles', array(
'title' => $this->title,
'content' => $this->content,
));
}
}
Use of the Article
class will slightly change:
$article = new Article($database);
$article->title = ...
$article->content = ...
$article->save();
Are you asking, where this code takes the $database
? DI gives a straight answer: “Let someone else do it.”
Database connection will be supplied by he who called the code. And so on, and so on. Sure you say that it's not possible to
eternalize the delegation of responsibility. That there must be a zero point. And you're right. There's a creator at the
beginning, he doesn't delegate anything and he creates objects. We call him system container. There's a separate chapter about him.
Why are global variables evil?
Good question. Class Article
needs the database connection anyway. But from the first example there's not at all
evident, from where and how it gets it. User of such code might be surprised, that the article really saves and he asks: “Where
did it save?” With the second example using DI, the code is self-explaining.
Imagine, that you're exploring some payment gateway and you write an example:
$cc = new CreditCard('4461510140804839', 12, 2013);
$cc->pay(1000, CreditCard::DOLLARS);
You run the code, with your card number, and later you'll find, that it really withdrew the money from your account! Shocked
you stare at the list and lament: “Where is my money, how could that happen, I didn't pair it with any payment gateway!”
Class CreditCard
did it by herself, found one in some global variable, as mysterious, as Article
got the
database connection. Such a thing you're not deducating from the code and you don't even know how to change the gateway to
another, like testing.
DI means more writing
You can object, that use of DI means more writing, that for creating of an instance of Article
you have to handle
the database connection and so. That's true, but don't forget the last time, when “less writing” costed you $1000! No, we
don't want to ease that. Objection is correct an we're gonna add one even bigger: When we find a need for Article
to
cache some data, in harmony with DI it will require one more argument with the cache repository. That would mean to adapt the
application at many places: At least everywhere, where Article
is instantiated.
What now? The thing has a solution: Instead of manual creating of Article
object we make a factory, ie. function
that creates these Article
objects. When Article
changes the constructor, only the factory has to
update, nothing more. And where to get the factory in our code? You know… let someone else do it. :-)
DI container and services
By the term “DI container” we mean that factory. More precisely it is an object containing any amount of factories, one for
each service. What are services? Ordinary objects, like that Connection
instance, for instance. Just with the DI
containers we call them services. Perhaps some consultants invented it to make DI look complicated so they could consult.
Example could be a container, that creates an Article
object, but also the required database connection::
class Container
{
function createConnection()
{
return new Nette\Database\Connection('mysql:', 'root', '***');
}
function createArticle()
{
return new Article($this->createConnection());
}
}
Usage would look as follows:
$container = new Container;
$article = $container->createArticle();
Advantage is obvious, we don't need to care, how exactly is the Article
instantiated, that's a job for the
factory. Anyway, the solution has still two drawbacks. First, there's entry data wired into the code, so we're gonna detach them
into a variable:
class Container
{
private $parameters;
function __construct(array $parameters)
{
$this->parameters = $parameters;
}
function createConnection()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function createArticle()
{
return new Article($this->createConnection());
}
}
$container = new Container(array(
'dsn' => 'mysql:',
'user' => 'root',
'password' => '***',
));
$article = $container->createArticle();
More important drawback is, that always, when we ask for an Article
creation, there's gonna start new database
connection. This needs to be avoided. We'll add method getConnection
that will keep once created service for
next usage:
class Container
{
private $parameters;
private $services = array();
function __construct(array $parameters)
{
$this->parameters = $parameters;
}
function createConnection()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function getConnection()
{
if (!isset($this->services['connection'])) {
$this->services['connection'] = $this->createConnection();
}
return $this->services['connection'];
}
function createArticle()
{
return new Article($this->getConnection());
}
}
And now we have fully functional DI container. As you see, it's no complicated to write it. It's notable that services alone don't know, that they're created by some container, therefor it is possible to create any PHP object that way. Without touching its own source code.
Nette\DI\Container
Class Nette\DI\Container is a flexible implementation of the universal DI container. It ensures automatically, that instance of services are created only once.
We can create custom containers either statically, ie. inheriting this class, or let them automatically generate, which is a very elegant and powerful solution.
Static container
Names of factory methods follow an uniform convention, they consist of the prefix createService
+ name of the
service starting with first letter upper-cased. If they are not supposed to be accesible from outside, it is possible to lower
their visibility to protected
. Note that the container has already defined the field $parameters
for
user parameters.
class MyContainer extends Nette\DI\Container
{
protected function createServiceConnection()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
protected function createServiceArticle()
{
return new Article($this->getService('connection'));
}
}
Now we create an instance of the container and pass parameters:
$container = new MyContainer(array(
'dsn' => 'mysql:',
'user' => 'root',
'password' => '***',
));
We get the service by calling the getService
method:
$article = $container->getService('article');
As have been said, all services are created in one container only once, but it would be more useful, if the container was
creating always a new instance of Article
. It could be achieved easily: Instead of the factory for the service
article
we'll create an ordinary method createArticle
:
class MyContainer extends Nette\DI\Container
{
function createServiceConnection()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function createArticle()
{
return new Article($this->getService('connection'));
}
}
$container = new MyContainer(...);
$article = $container->createArticle();
From the call of $container->createArticle()
is evident, that a new object is always created. It is then a
programmer's convention.
Dynamic container
We can add services into the Nette\DI\Container
even runtime using the addService
method. Factories
can be written as a PHP callbacks or closures. Note that the container itself is passed to them as a parameter so they could
easily access parameters and other services.
$container = new Nette\DI\Container;
$container->addService('connection', function($container) {
return new Nette\Database\Connection(
$container->parameters['dsn'],
$container->parameters['user'],
$container->parameters['password']
);
});
When service creation consists just of a simple instantiation, one can pass the class name directly as the second parameter of
the addService
method. And when we have already created the object, we can pass even it.
Besides the addService
we've got also hasService
for service existence checking and
removeService
serving for its removal.