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');