I just started using Dependency Injection for obvious reasons and without reading about Inversion of Control (IoC) quickly stumble with the issue of being verbose when instantiate some of my classes. So, reading about IoC I have a question that have not found an concrete answer. When should class registration happen? in a bootstrap? before execution? How can I enforce the type of the dependencies?
I am not using any frameworks. For the sake of learning I wrote my own container.
This is a very lowbrow example of my container and some sample classes.
class DepContainer
{
private static $registry = array();
public static function register($name, Closure $resolve)
{
self::$registry[$name] = $resolve;
}
public static function resolve($name)
{
if (self::registered($name)) {
$name = static::$registry[$name];
return $name();
}
throw new Exception('Nothing bro.');
}
public static function registered($name)
{
return array_key_exists($name, self::$registry);
}
}
class Bar
{
private $hello = 'hello world';
public function __construct()
{
# code...
}
public function out()
{
echo $this->hello . "\n";
}
}
class Foo
{
private $bar;
public function __construct()
{
$this->bar = DepContainer::resolve('Bar');
}
public function say()
{
$this->bar->out();
}
}
With these already in the app structure. The Dependecy Injection way I would do type hint the incoming parameters, but without it I can do:
DepContainer::register('Bar', function(){
return new Bar();
});
$f = new Foo();
$f->say();
To me, makes sense in a bootsrap register all dependencies it would be the more clean way IMO. At run time like a showed you I think is just as ugly as doing new Foo(new Bar(...)...)
.
I will try to summarize a few things that you should know and (hopefully) will clarify some of your dilemmas .
Let's start from a basic example:
class MySQLAdapter
{
public function __construct()
{
$this->pdo = new PDO();
}
}
class Logger
{
public function __construct()
{
$this->adapter = new MySqlAdapter();
}
}
$log = new Logger();
As you can see, we are instantiating Logger
which has two dependencies: MySQLAdapter
and PDO.
This process works like this:
The above code works, but if tomorrow we decided that we need to log our data in a file instead of a database, we will need
to change the Logger
class and replace MySQLAdapter
with a brand new FileAdapter
.
// not good
class Logger
{
public function __construct()
{
$this->adapter = new FileAdapter();
}
}
This is the problem that Dependency Injection tries to solve: do not modify a class because a dependency has changed.
Di reefers to the process of instantiating a class by giving it's constructor all the dependencies it needs to function properly. If we apply Dependency Injection to our previous example, it will look like this:
interface AdapterInterface
{
}
class FileAdapter implements AdapterInterface
{
public function __construct()
{
}
}
class MySQLAdapter implements AdapterInterface
{
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
}
class Logger
{
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
}
// log to mysql
$log = new Logger(
new MySQLAdapter(
new PDO()
)
);
As you can see, we don't instantiate anything in constructor, but we pass the instantiated class to constructor. This allows us to replace any dependency without modifying the class:
// log to file
$log = new Logger(
new FileAdapter()
);
This helps us:
To easily maintain the code: As you already saw, we don't need to modify the class if one of its dependencies changed.
Makes the code more testable:
When you run your test suite against MySQLAdapter
you don't want to hit the database on each test, so the PDO object will be mocked in tests:
// test snippet
$log = new Logger(
new MySQLAdapter(
$this->getMockClass('PDO', [...])
)
);
Q: How does Logger
knows that you give him a class that it needs and not some garbage ?
A: This is the interface (AdapterInterface) job, which is a contract between Logger
and other classes. Logger "knows" that any class that implements
that particular interface will contain the methods it needs to do his job.
You can look at this class (ie: container) as a central place where you store all your objects needed to run your application. When you need one of them, you request the object from the container instead of instantiating yourself.
You can look at DiC as a dog who was trained to get out, get the newspaper and bring it back to you. The catch is that the dog was trained only with the front door opened. Everything would be fine as long as the dog's dependencies will not change (ie door opened). If one day the front door will be closed, the dog will not know how to get the newspaper.
But if the dog would have an IoC container, he could find a way ...
As you saw until now, the initialization process of the "classic" code was:
IoC simply replicates the above process, but in reverse order:
If you though that Dependency Injection is some kind of IoC, you are right. When we talked about Dependency Injection, we had this example:
// log to mysql
$log = new Logger(
new MySQLAdapter(
new PDO()
)
);
At a first look someone could say that the instantiation process is:
The thing is that the code will be interpreted from the middle to the left. So the order will be:
The IoC container simply automates this process. When you request Logger
from the container, it uses PHP Reflection and type hinting to analyze its dependencies (from constructor), instantiate all of them, sends them to the requested class and gives you back a Logger
instance.
NOTE: To find out what dependencies a class has, some IoC containers are using annotations instead of type hinting or a combination of both.
So to answer your question:
If your container can resolve the dependencies by itself, but for various reasons you also need to manually add more dependencies, you would do that in the boot process, after you initialize the container.
NOTE: In the wild there are all kind of mixes between these two principles, but I tried to explain you what is the main idea behind each of them. How your container will look depends only by you and don't be afraid to reinvent the wheel as long as you do it for educational purposes.
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