Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inversion of Control with PHP

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(...)...).

like image 266
LouieV Avatar asked May 15 '14 20:05

LouieV


1 Answers

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:

  • We created Logger
    • Logger creates MySQLAdapter
      • MySQLAdapter creates PDO

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.

Dependency Injection

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:

  1. To easily maintain the code: As you already saw, we don't need to modify the class if one of its dependencies changed.

  2. 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.

Dependency Injection Container:

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 ...

Inversion of Control

As you saw until now, the initialization process of the "classic" code was:

  • We created Logger
    • Logger creates MySQLAdapter
      • MySQLAdapter creates PDO

IoC simply replicates the above process, but in reverse order:

  • Create PDO
    • Create MySQLAdapter and give him PDO
      • Create Logger and give him MySQLAdapter

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:

  • Create Logger
  • Create MySQLAdapter
  • Create PDO`

The thing is that the code will be interpreted from the middle to the left. So the order will be:

  • Create PDO
    • Create MySQLAdapted and give him PDO
      • Create Logger and give him MySQLAdapter

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 the container can resolve the dependencies by itself, you would only need to instantiate the container during the boot process of your application. (see Inversion of Control container)
  • If the container can't resolve the dependencies by itself, you would need to manually provision the container with the objects needed to run your application. This provisioning usually happens during the boot process. (see Dependency Injection Container)

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.

like image 94
Alexandru Guzinschi Avatar answered Oct 09 '22 09:10

Alexandru Guzinschi