Dependency injection

At the end of the chapter, we will cover one of the most interesting and controversial of the topics that come with, not only the MVC pattern, but OOP in general: dependency injection. We will show you why it is so important, and how to implement a solution that suits our specific application, even though there are quite a few different implementations that can cover different necessities.

Why is dependency injection necessary?

We still need to cover the way to unit test your code, hence you have not experienced it by yourself yet. But one of the signs of a potential source of problems is when you use the new statement in your code to create an instance of a class that does not belong to your code base—also known as a dependency. Using new to create a domain object like Book or Sale is fine. Using it to instantiate models is also acceptable. But manually instantiating, which something else, such as the template engine, the database connection, or the logger, is something that you should avoid. There are different reasons that support this idea:

  • If you want to use a controller from two different places, and each of these places needs a different database connection or log file, instantiating those dependencies inside the controller will not allow us to do that. The same controller will always use the same dependency.
  • Instantiating the dependencies inside the controller means that the controller is fully aware of the concrete implementation of each of its dependencies, that is, the controller knows that we are using PDO with the MySQL driver and the location of the credentials for the connection. This means a high level of coupling in your application—so, bad news.
  • Replacing one dependency with another that implements the same interface is not easy if you are instantiating the dependency explicitly everywhere, as you will have to search all these places, and change the instantiation manually.

For all these reasons, and more, it is always good to provide the dependencies that a class such as a controller needs instead of letting it create its own. This is something that everybody agrees with. The problem comes when implementing a solution. There are different options:

  • We have a constructor that expects (through arguments) all the dependencies that the controller, or any other class, needs. The constructor will assign each of the arguments to the properties of the class.
  • We have an empty constructor, and instead, we add as many setter methods as the dependencies of the class.
  • A hybrid of both, where we set the main dependencies through a constructor, and set the rest of the dependencies via setters.
  • Sending an object that contains all the dependencies as a unique argument for the constructor, and the controller gets the dependencies that it needs from that container.

Each solution has its pros and cons. If we have a class with a lot of dependencies, injecting all of them via the constructor would make it counterintuitive, so it would be better if we inject them using setters, even though a class with a lot of dependencies looks like bad design. If we have just one or two dependencies, using the constructor could be acceptable, and we will write less code. For classes with several dependencies, but not all of them mandatory, using the hybrid version could be a good solution. The fourth option makes it easier when injecting the dependencies as we do not need to know what each object expects. The problem is that each class should know how to fetch its dependency, that is, the dependency name, which is not ideal.

Implementing our own dependency injector

Open source solutions for dependency injectors are already available, but we think that it would be a good experience to implement a simple one by yourself. The idea of our dependency injector is a class that contains instances of the dependencies that your code needs. This class, which is basically a map of dependency names to dependency instances, will have two methods: a getter and a setter of dependencies. We do not want to use a static property for the dependencies array, as one of the goals is to be able to have more than one dependency injector with a different set of dependencies. Add the following class to src/Utils/DependencyInjector.php:

<?php

namespace BookstoreUtils;

use BookstoreExceptionsNotFoundException;

class DependencyInjector {
    private $dependencies = [];

    public function set(string $name, $object) {
        $this->dependencies[$name] = $object;
    }

    public function get(string $name) {
        if (isset($this->dependencies[$name])) {
            return $this->dependencies[$name];
        }
        throw new NotFoundException(
            $name . ' dependency not found.'
        );
    }
}

Having a dependency injector means that we will always use the same instance of a given class every time we ask for it, instead of creating one each time. That means that singleton implementations are not needed anymore; in fact, as mentioned in Chapter 4, Creating Clean Code with OOP, it is preferable to avoid them. Let's get rid of them, then. One of the places where we were using it was in our configuration reader. Replace the existing code with the following in the src/Core/Config.php file:

<?php

namespace BookstoreCore;

use BookstoreExceptionsNotFoundException;

class Config {
    private $data;

    public function __construct() {
        $json = file_get_contents(
            __DIR__ . '/../../config/app.json'
        );
        $this->data = json_decode($json, true);
    }

    public function get($key) {
        if (!isset($this->data[$key])) {
            throw new NotFoundException("Key $key not in config.");
        }
        return $this->data[$key];
    }
}

The other place where we were making use of the singleton pattern was in the DB class. In fact, the purpose of the class was only to have a singleton for our database connection, but if we are not making use of it, we can remove the entire class. So, delete your src/Core/DB.php file.

Now we need to define all these dependencies and add them to our dependency injector. The index.php file is a good place to have the dependency injector before we route the request. Add the following code just before instantiating the Router class:

$config = new Config();

$dbConfig = $config->get('db');
$db = new PDO(
    'mysql:host=127.0.0.1;dbname=bookstore',
    $dbConfig['user'],
    $dbConfig['password']
);

$loader = new Twig_Loader_Filesystem(__DIR__ . '/../../views');
$view = new Twig_Environment($loader);

$log = new Logger('bookstore');
$logFile = $config->get('log');
$log->pushHandler(new StreamHandler($logFile, Logger::DEBUG));

$di = new DependencyInjector();
$di->set('PDO', $db);
$di->set('UtilsConfig', $config);
$di->set('Twig_Environment', $view);
$di->set('Logger', $log);

$router = new Router($di);
//...

There are a few changes that we need to make now. The most important of them refers to the AbstractController, the class that will make heavy use of the dependency injector. Add a property named $di to that class, and replace the constructor with the following:

public function __construct(
    DependencyInjector $di,
    Request $request
) {
    $this->request = $request;
    $this->di = $di;

    $this->db = $di->get('PDO');
    $this->log = $di->get('Logger');
    $this->view = $di->get('Twig_Environment');
    $this->config = $di->get('UtilsConfig');

    $this->customerId = $_COOKIE['id'];
}

The other changes refer to the Router class, as we are sending it now as part of the constructor, and we need to inject it to the controllers that we create. Add a $di property to that class as well, and change the constructor to the following one:

public function __construct(DependencyInjector $di) {
    $this->di = $di;

    $json = file_get_contents(__DIR__ . '/../../config/routes.json');
    $this->routeMap = json_decode($json, true);
}

Also change the content of the executeController and route methods:

public function route(Request $request): string {
    $path = $request->getPath();

    foreach ($this->routeMap as $route => $info) {
        $regexRoute = $this->getRegexRoute($route, $info);
        if (preg_match("@^/$regexRoute$@", $path)) {
            return $this->executeController(
                $route, $path, $info, $request
            );
        }
    }

    $errorController = new ErrorController(
        $this->di,
        $request
    );
    return $errorController->notFound();
}

private function executeController(
    string $route,
    string $path,
    array $info,
    Request $request
): string {
    $controllerName = 'BookstoreControllers' 
        . $info['controller'] . 'Controller';
    $controller = new $controllerName($this->di, $request);

    if (isset($info['login']) && $info['login']) {
        if ($request->getCookies()->has('user')) {
            $customerId = $request->getCookies()->get('user');
            $controller->setCustomerId($customerId);
        } else {
            $errorController = new CustomerController(
                $this->di,
                $request
            );
            return $errorController->login();
        }
    }

    $params = $this->extractParams($route, $path);
    return call_user_func_array(
        [$controller, $info['method']], $params
    );
}

There is one last place that you need to change. The login method of CustomerController was instantiating a controller too, so we need to inject the dependency injector there as well:

$newController = new BookController($this->di, $this->request);
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.157.6