Передача зависимостей

Аргументы, или «зависимости» в терминологии DI, могут быть переданы классам следующими основными способами:

  • passing by constructor
  • passing by method (called a setter)
  • by setting a property
  • методом, аннотацией или атрибутом inject

Теперь мы проиллюстрируем различные варианты на конкретных примерах.

Внедрение через конструктор

Зависимости передаются в качестве аргументов конструктору при создании объекта:

class MyClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass($cache);

Эта форма полезна для обязательных зависимостей, которые абсолютно необходимы классу для функционирования, так как без них экземпляр не может быть создан.

Начиная с версии PHP 8.0, мы можем использовать более короткую форму обозначения (constructor property promotion), которая функционально эквивалентна:

// PHP 8.0
class MyClass
{
	public function __construct(
		private Cache $cache,
	) {
	}
}

Начиная с версии PHP 8.1, свойство может быть помечено флагом readonly, который объявляет, что содержимое свойства не будет изменяться:

// PHP 8.1
class MyClass
{
	public function __construct(
		private readonly Cache $cache,
	) {
	}
}

DI контейнер передает зависимости в конструктор автоматически, используя autowiring. Аргументы, которые нельзя передавать таким образом (например, строки, числа, булевы) записать в конфигурации.

Конструкторский ад

Термин ад конструктора относится к ситуации, когда дочерний класс наследуется от родительского класса, конструктор которого требует зависимостей, и дочерний класс тоже требует зависимостей. Он также должен принять и передать зависимости родительского класса:

abstract class BaseClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

final class MyClass extends BaseClass
{
	private Database $db;

	// ⛔ CONSTRUCTOR HELL
	public function __construct(Cache $cache, Database $db)
	{
		parent::__construct($cache);
		$this->db = $db;
	}
}

Проблема возникает, когда мы хотим изменить конструктор класса BaseClass, например, когда добавляется новая зависимость. Тогда мы должны изменить все конструкторы дочерних классов. Что превращает такую модификацию в ад.

Как предотвратить это? Решение заключается в приоритете композиции над наследованием.

Поэтому давайте спроектируем код по-другому. Мы будем избегать абстрактных классов Base*. Вместо того чтобы MyClass получал некоторую функциональность, наследуя от BaseClass, ему будет передаваться эта функциональность как зависимость:

final class SomeFunctionality
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

final class MyClass
{
	private SomeFunctionality $sf;
	private Database $db;

	public function __construct(SomeFunctionality $sf, Database $db) // ✅
	{
		$this->sf = $sf;
		$this->db = $db;
	}
}

Внедрение через сеттеры

Зависимости передаются путем вызова метода, который хранит их в приватном свойстве. Обычно эти методы именуются set*(), поэтому их называют сеттерами, но, конечно, они могут называться и по-другому.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass;
$obj->setCache($cache);

Этот метод полезен для необязательных зависимостей, которые не нужны для функционирования класса, поскольку не гарантируется, что объект действительно получит их (т. е. что пользователь вызовет метод).

В то же время, этот метод позволяет неоднократно вызывать сеттер для изменения зависимости. Если это нежелательно, добавьте проверку в метод, или, начиная с PHP 8.1, пометьте свойство $cache флагом readonly.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		if ($this->cache) {
			throw new RuntimeException('The dependency has already been set');
		}
		$this->cache = $cache;
	}
}

The setter call is defined in the DI container configuration in section setup. Also here the automatic passing of dependencies is used by autowiring:

services:
	-	create: MyClass
		setup:
			- setCache

Внедрение через свойства

Зависимости передаются непосредственно в свойство:

class MyClass
{
	public Cache $cache;
}

$obj = new MyClass;
$obj->cache = $cache;

Этот метод считается неприемлемым, поскольку свойство должно быть объявлено как public. Следовательно, мы не можем контролировать, будет ли переданная зависимость действительно иметь указанный тип (это было верно до версии PHP 7.4), и мы теряем возможность реагировать на новую назначенную зависимость своим собственным кодом, например, чтобы предотвратить последующие изменения. В то же время, свойство становится частью публичного интерфейса класса, что может быть нежелательно.

Настройка переменной определяется в конфигурации контейнера DI в разделе section setup:

services:
	-	create: MyClass
		setup:
			- $cache = @\Cache

Инъекция

В то время как предыдущие три метода в целом применимы во всех объектно-ориентированных языках, инъектирование методом, аннотацией или атрибутом inject специфично для презентаторов Nette. Они рассматриваются в отдельной главе.

Какой путь выбрать?

  • конструктор подходит для обязательных зависимостей, которые необходимы классу для функционирования
  • сеттер, с другой стороны, подходит для необязательных зависимостей, или зависимостей, которые могут быть изменены
  • публичные переменные не рекомендуются
версия: 3.x