What is Dependency Injection?
It is a pattern of computer program development used when it is necessary to keep the coupling level between different modules in a system.
In this solution the dependencies between modules are not defined programically, but by configuring a software infrastructure that is responsible for "injecting" into each component its declared premises.
Dependency Injection is related to the Inversion of Control pattern but cannot be considered a synonym of it.
Directory System
📦Dependency Injection
┣ 📂api
┃ ┗ 📜ExporterInterface.php
┣ 📂classes
┃ ┣ 📜JSONExporter.php
┃ ┣ 📜Product.php
┃ ┗ 📜XMLExporter.php
┗ 📜index.php
Fixed form
The fixed form does not allow changing a functionality at run time.
- JSONExporter Class
<?php
namespace classes;
class JSONExporter
{
public function export(array $data): string
{
return json_encode($data);
}
}
The export
method receives the data array and returns it encoded in JSON
.
- Product Class
<?php
namespace classes;
class Product
{
private array $data;
public function __get(string $prop): mixed
{
return $this->data[$prop];
}
public function __set(string $prop, mixed $value): void
{
$this->data[$prop] = $value;
}
public function toJSON(): string
{
$je = new JSONExporter();
return $je->export($this->data);
}
}
The toJSON
method instances the JSONExporter
class and returns the result obtained by the exporter
method.
- Test
<?php
require_once 'classes/JSONExporter.php';
require_once 'classes/Product.php';
use classes\JSONExporter;
use classes\Product;
$product = new Product();
$product->name = 'Juice';
$product->price = 2.50;
print $product->toJSON();
Result:
{"name":"Juice","price":2.5}
In this example we simulate a class(Product
) that depends on another class(JSONExporter
).
The problem with this example is that the Product
class has a fixed dependency created at development time, so there is no way to change this dependency.
Flexible Form (Dependency Injection)
- JSONExporter Class
<?php
namespace classes;
class JSONExporter
{
public function export(array $data): string
{
return json_encode($data);
}
}
The JSONExporter
class has not undergone any changes.
- XMLExporter Class
<?php
namespace classes;
use DOMDocument;
class XMLExporter
{
public function export(array $data): string
{
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$products = $dom->createElement('products');
$product = $dom->createElement('product');
$attr = $dom->createAttribute('id');
$attr->value = 1;
$product->appendChild($attr);
$product->appendChild($dom->createElement('name', $data['name']));
$product->appendChild($dom->createElement('price', $data['price']));
$products->appendChild($product);
return $dom->saveXML($products);
}
}
XML DOM was used in the XMLExporter
class.
- Product Class
<?php
namespace classes;
class Product
{
private array $data;
public function __get(string $prop): mixed
{
return $this->data[$prop];
}
public function __set(string $prop, mixed $value): void
{
$this->data[$prop] = $value;
}
public function export(object $exporter): string
{
return $exporter->export($this->data);
}
}
Instead of explicitly using the class name, we now pass it as a parameter to the export
method.
- Test
<?php
require_once 'classes/JSONExporter.php';
require_once 'classes/XMLExporter.php';
require_once 'classes/Product.php';
use classes\JSONExporter;
use classes\XMLExporter;
use classes\Product;
$product = new Product();
$product->name = 'Juice';
$product->price = 2.50;
// print $product->export(new JSONExporter());
print $product->export(new XMLExporter());
In the export
method we can define which class we want to use to perform the export, this definition is injecting a dependency into the class at run time.
JSONExporter output:
{"name":"Juice","price":2.5}
XMLExporter output:
<products>
<product id="1">
<name>Juice</name>
<price>2.5</price>
</product>
</products>
But we still have a problem, because the class passed to the export
method of the Product
class must contain an export
method, if a class is passed without this method we will have an error at run time.
To solve this problem, we use the concept of interface to sign a contract with the classes ensuring that they have the export
method.
Applying Interface
<?php
namespace api;
interface ExporterInterface
{
public function export(array $data): string;
}
Changing the classes
- JSONExporter
<?php
namespace classes;
require_once 'api/ExporterInterface.php';
use api\ExporterInterface;
class JSONExporter implements ExporterInterface
{
public function export(array $data): string
{
return json_encode($data);
}
}
- XMLExporter
<?php
namespace classes;
require_once 'api/ExporterInterface.php';
use api\ExporterInterface;
use DOMDocument;
class XMLExporter implements ExporterInterface
{
public function export(array $data): string
{
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$products = $dom->createElement('products');
$product = $dom->createElement('product');
$attr = $dom->createAttribute('id');
$attr->value = 1;
$product->appendChild($attr);
$product->appendChild($dom->createElement('name', $data['name']));
$product->appendChild($dom->createElement('price', $data['price']));
$products->appendChild($product);
return $dom->saveXML($products);
}
}
- Product
<?php
namespace classes;
use api\ExporterInterface;
class Product
{
private array $data;
public function __get(string $prop): mixed
{
return $this->data[$prop];
}
public function __set(string $prop, mixed $value): void
{
$this->data[$prop] = $value;
}
public function export(ExporterInterface $exporter): string
{
return $exporter->export($this->data);
}
}
We place ExporterInterface in front of the object, this is done to ensure that the object passed as a parameter has the export
method.
So dependency injection is a feature that allows you to inject an object into a class, usually via a parameter and this class calls a method of that injected object