PHP Design Patterns: Dependency Injection

Antonio Silva - Nov 7 '23 - - Dev Community

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.

1

Directory System

📦Dependency Injection
 ┣ 📂api
 ┃ ┗ 📜ExporterInterface.php
 ┣ 📂classes
 ┃ ┣ 📜JSONExporter.php
 ┃ ┣ 📜Product.php
 ┃ ┗ 📜XMLExporter.php
 ┗ 📜index.php
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Result:

{"name":"Juice","price":2.5}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

XMLExporter output:

<products>
  <product id="1">
    <name>Juice</name>
    <price>2.5</price>
  </product>
</products>
Enter fullscreen mode Exit fullscreen mode

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

2

<?php

namespace api;

interface ExporterInterface
{
    public function export(array $data): string;
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
  • 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);
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .