Dependency Injection the best design pattern

11 mins
683

Deep understanding of the dependency injection design pattern the most important thing for an awesome developer.

Problem

<?php

namespace PluginName;

class Order {
	public $id;
}

class OrderProcessing {
	public function create_new_order() {
		/* Some kind of logic for create order */
		$this->log( 'Order created!' );
	}
	private function log( string $message ) {
		echo "Save log with message: {$message}" . PHP_EOL;
	}
}

$order_processing = new OrderProcessing();
$order_processing->create_new_order();

The first problem that you can see it is the single responsibility principle. In OrderProcessing we are using the logger logic. Let’s move this logic to the single class.

<?php

namespace PluginName;

class Logger {
	public function log( string $message ) {
		echo "Save log with message: {$message}" . PHP_EOL;
	}
}

class OrderProcessing {
	public function create_new_order() {
		/* Some kind of logic for create order */
		$logger = new Logger();
		$logger->log('Order created!');
	}
}

$order_processing = new OrderProcessing();
$order_processing->create_new_order();

Small refactoring and we can reuse the Logger class in the different parts of our application. But we can’t change the Logger storage so easily.

For understanding the dependencies of the OrderProcessing class need to read the whole class. Each external function or class that used inside our class called a dependency. Some dependencies are good, for example, some PHP function which sanitizes some property. But in our example, we have a bad dependency new Logger that called hard dependency.

Why hard dependency is bad? Because we can’t change logic without changing these classes. I mean If I want to change the storage of log records then I need to change both classes.

How do our example more flexible? Of course, use the dependency injection design pattern.

Introducing Dependency Injection

<?php

namespace PluginName;

class OrderProcessing {
	private $logger;
	public function __construct( Logger $loger ) {
		$this->logger = $logger;
	}
	public function create_new_order() {
		/* Some kind of logic for create order */
		$this->logger->log('Order created!');
	}
}

$logger           = new Logger();
$order_processing = new OrderProcessing( $logger );
$order_processing->create_new_order();

The best way it’s using the interface in the constructor instead of the Logger, but about it later.

More and more dependencies

<?php

namespace PluginName;

class OrderProcessing {
	private $logger;
	public function __construct( Logger $loger, OrderRepository $repository, SMSNotifier $sms_notifier ) {
		$this->logger       = $logger;
		$this->repository   = $repository;
 		$this->sms_notifier = $sms_notifier;
	}
	public function create_new_order() {
       		/* Some kind of logic for create order */
		$this->logger->log('Order created!');
	}
}

$repository       = new OrderRepository();
$sms_notifier     = new SMSNotifier();
$logger           = new Logger();
$order_processing = new OrderProcessing( $logger, $repository, $sms_notifier );
$order_processing->create_new_order();

The business logic grew and the complexity of our constructor is getting higher. A lot of works need to do for adding a new feature. To manage all dependencies we need to create the god object or move this logic to a different part. The best practice is to use the dependency injection container. Let’s try.

Dependency Injection Container

I hope you already know about autoloading your classes and files because this is the first thing to start with the dependency injection container.

There are many different packages for the dependency injection container. I prefer the package symfony/dependency-injection. Let’s installing it:

composer require symfony/dependency-injection
composer require symfony/config

Let’s create the config for dependencies services.php:

<?php

use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

return function ( ContainerConfigurator $configurator ) {
	$services = $configurator->services();

	$services->set( OrderRepository::class );
	$services->set( SMSNotifier::class );
	$services->set( Logger::class );
	$services
		->set( OrderProcessing::class )
		->args(
			[
				new Reference( Logger::class ),
				new Reference( Order_Repository::class ),
				new Reference( SMS_Notifier::class ),
			]
		);
};

We’ve described that OrderProcessing class has in the constructor arguments Logger, OrderRepository, and SMSNotifier.

But how to load the DIC?

<?php

use PluginName\OrderProcessing;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

require_once 'vendor/autoload.php';

$container_builder = new ContainerBuilder();
$loader            = new PhpFileLoader( $container_builder, new FileLocator( __DIR__ ) );
$loader->load( 'services.php' );
$container_builder->compile();

$container_builder->get( OrderProcessing::class );

We get objects from the container instead of creating new objects. All dependencies describe in one file.

Inversion Of Control (IoC)

Inversion of control (IoC) is a programming principle. IoC inverts the flow of control as compared to traditional control flow. In IoC, custom-written portions of a computer program receive the flow of control from a generic framework.

Wikipedia

Let’s update our dependency injection config for automatic loading and resolve dependencies.

<?php

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

return function ( ContainerConfigurator $configurator ) {
	$services = $configurator
		->services()
		->defaults()
		->public()
		->autowire()
		->autoconfigure();

	$services->load( 'PluginName\\', __DIR__ . '/src/*' );
};

This black magic helps you forget about new instances in your work. Just create a new file and describe the constructor with type hinting. Dependency injection container resolves all dependencies instead of you when you try to get the object from the container.

Once you’ve added this wonderful tool, you simplify the interaction between elements, replacing it with type hinting.

How to manage the dependencies if we are using the interface?

Let’s create the interface ILogger:

<?php

namespace PluginName;

interface ILogger {

	public function message();

}

And rename Logger to the DBLogger:

<?php

namespace PluginName;

class DBLogger implements ILogger {

	public function message() {
		// TODO: Implement message() method.
	}

}

And change the construct for the OrderProcessing:

<?php

namespace PluginName;

class OrderProcessing {
	// ...
	public function __construct( ILogger $logger ) {
		// ...
	}
	// ...
}

And what happens? DIC still resolves your problems. Why? Because only one class implementing the ILogger interface and for DIC easy to resolve your dependencies.

Let’s add the FileLogger class:

<?php

namespace PluginName;

class FileLogger implements ILogger {

	public function message() {
		// TODO: Implement message() method.
	}

}

And now DIC has a few ways to resolve this dependency. You can see the Uncaught Exception: Cannot autowire service... exception.

We need to resolve this problem manually:

<?php

use PluginName\FileLogger;
use PluginName\OrderProcessing;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

return function ( ContainerConfigurator $configurator ) {
	$services = $configurator
		->services()
		->defaults()
		->public()
		->autowire()
		->autoconfigure();

	$services->load( 'PluginName\\', __DIR__ . '/src/*' );

	$services
		->get( OrderProcessing::class )
		->args(
			[
				new Reference( FileLogger::class ),
			]
		);
};

This means we can load automatically all our dependencies but in hard cases resolve it manually.

Conclusion

If you avoid the hard dependencies this helps you create more quality and scalable solutions. To simplify your everyday development use the dependency injection container with autowire and autoconfiguration.

Leave a Reply

Your email address will not be published. Required fields are marked *