Bare-bone PHP Framework #2 Dependency Injection & Container

Bare-bone PHP Framework #2 Dependency Injection & Container

Dependency Injection

Dependency Injection is a pattern that allows us to break apart dependants from their dependencies and have the power to change/replace them easily.

For example, if we would have two types of products, digital ones and physical ones, and we would be using different databases for storing them, we could use and "inject" the necessary database into correct models.

interface DbInterface {
    public function connect();
}

class Mysql implements DbInterface {
    public function connect(){...}
}

class Postgresql implements DbInterface {
    public function connect(){...}
} 

class book {
    private $database;
    public function __construct($database) {
        $this->database = $database;
    }

    public function get() {
        $connection =  $this->database->connect();
        return $connection->fetch();
    }
}

class Software {
    private $database;
    public function __construct($database) {
        $this->database = $database;
    }

    public function get() {
        $connection =  $this->database->connect();
        return $connection->fetch();
    }
}

$book = (new Book(new Mysql()))->get();
$software = (new Software(new Postgresql()))->get();

This pattern can be easily implemented, and you can manually do injections whenever needed. But often, your class depends on multiple dependencies, and these dependencies also have their dependencies and so on, so fairly quickly, you will end up with code like this.

$book = new BooksService(new BooksRepository(new Book(new Database(new Adaptor, new Configuration)))), new Authors(new Database(new Adaptor));

This is when Container comes into play.

Container

Container is used to resolve dependencies. Instead of passing everything manually, you use it to get instances that you need.

$book = (new Container())->get(Book::class);

Create Container.php in the Core folder, define method make that will take a class name, call private method resolve that will resolve dependencies, and return an instance of that necessary class.

<?php
namespace Core;
class Container
{
    // This will be used to store container instance
    protected static $instance;
    // Mathod that will be used to get class instances
    public function make($class)
    {
        return $this->resolve($class);
    }
    public function call($class, $method, $args = []) {}
    private function resolve($class) {}
    private function getDependencies($parameters, $args = []) {}
}

First, resolve method

private function resolve($class)
{
    // Create reflection object for our class
    $reflector = new \ReflectionClass($class);
    // Check if this class can be instantiated
    if (!$reflector->isInstantiable()) {
        throw new \Exception("Class {$class} is not instantiable");
    }
    // Check if class has constructor method,
    // if not then class has no dependencies and can be initialize
    $constructor = $reflector->getConstructor();
    if (is_null($constructor)) {
        return $reflector->newInstance();
    }
    // Get constructor parameters
    $parameters = $constructor->getParameters();
    // Get dependencies for thous parameters
    // resolve dependencies recursively
    $dependencies = $this->getDependencies($parameters);
    // Create new instance of our class and pass it dependencies
    return $reflector->newInstanceArgs($dependencies);
}

Second getDependencies method

private function getDependencies($parameters, $args = [])
{
    $dependencies = [];
    foreach ($parameters as $parameter) {
        // If parameter is in passed arguments us it to resolve dependency
        if (array_key_exists($parameter->name, $args)) {
            $dependencies[] = $args[$parameter->name];
        } else {
            // Get dependency class name
            $dependency = $parameter->getClass();
            // if dependency class name is null it maybe primitive type
            if ($dependency === null) {
                // Then use default value for this parameter
                if ($parameter->isDefaultValueAvailable()) {
                    $dependencies[] = $parameter->getDefaultValue();
                } else {
                    throw new \Exception("Can not resolve class dependency {$parameter->name}");
                }
            } else {
                // Else resolve dependency class by again calling our make method,
                // that will call resolve method and will call getDependencies method
                // and all dependencies and dependencies of dependencies will be resolved recursively
                $dependencies[] = $this->make($dependency->name);
            }
        }
    }
    return $dependencies;
}

Third call method that will be used to invoke methods with dependencies

public function call($class, $method, $args = [])
{
    // Resolve class dependencies and get instance
    $classInstance = $this->make($class);
    // Create reflector object for method
    $reflector = new \ReflectionMethod($class, $method);
    // Get methods parameters
    $parameters = $reflector->getParameters();

    if ($parameters) {
        // If method has parameters resolve them
        $dependencies = $this->getDependencies($parameters, $args);
        // Invoke methods with resolved parameters
        return $classInstance->$method(...$dependencies);
    }

    // Invoke method
    return $classInstance->$method(...$args);
}

The last method that we will need is instance

public static function instance()
{
    // Check if container already was initialized
    if (is_null(static::$instance)) {
        //If no, initialize it and store
        static::$instance = new static();
    }
    // Return container instance
    return static::$instance;
}

Now Let's make Container globally available, in index.php

function container()
{
    return Core\Container::instance();
}

Now, if we would need to get an instance, we can

$object = container()->make('classname');