Nette Documentation Preview

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

<div class=perex>

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

*   внедрение через конструктор
*   внедрение через метод (так называемое внедрение через сеттер)
*   внедрение в свойство
*   с помощью метода `inject*()` или атрибута `#[Inject]`

</div>

Покажем каждый вариант на конкретных примерах.


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

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

```php
class MyClass
{
	private Cache $cache;

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

$obj = new MyClass($cache);
```

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

Начиная с PHP 8.0 мы можем использовать более краткую запись ([продвижение свойств конструктора |https://blog.nette.org/ru/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), функционально равнозначную:

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

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

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

DI-контейнер передаёт зависимости в конструктор автоматически через [autowiring |autowiring]. Аргументы, которые так передать нельзя (например, строки, числа, логические значения), [указываются в конфигурации |services#Аргументы].


Ад конструкторов
----------------

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

```php
abstract class BaseClass
{
	private Cache $cache;

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

final class MyClass extends BaseClass
{
	private Database $db;

	// ⛔ АД КОНСТРУКТОРОВ
	public function __construct(Cache $cache, Database $db)
	{
		parent::__construct($cache);
		$this->db = $db;
	}
}
```

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

Как этого избежать? Решение - **предпочитать [композицию наследованию |faq#Почему композиция предпочтительнее наследования?]**.

Итак, мы проектируем код иначе. Мы обойдёмся без [абстрактных |nette:introduction-to-object-oriented-programming#Абстрактные классы] классов `Base*`. Вместо того чтобы `MyClass` получал определённую функциональность наследованием от `BaseClass`, эта функциональность будет передана ему как зависимость:

```php
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*()`, отсюда и название "сеттеры", но, разумеется, они могут называться иначе.

```php
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`.

```php
class MyClass
{
	private Cache $cache;

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

Вызов сеттера задаётся в конфигурации DI-контейнера в [ключе setup |services#Setup]. Здесь тоже используется автоматическая передача зависимостей через autowiring:

```neon
services:
	-	create: MyClass
		setup:
			- setCache
```


Внедрение в свойство
====================

Зависимости передаются записью прямо в свойство объекта:

```php
class MyClass
{
	public Cache $cache;
}

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

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

Присваивание свойства задаётся в конфигурации DI-контейнера в [секции setup |services#Setup]:

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


Inject
======

Три предыдущих подхода применимы вообще во всех объектно-ориентированных языках, а внедрение через методы `inject*()` или атрибут `#[Inject]` обычно используется с презентерами Nette, где оно включено по умолчанию; любой другой сервис может подключить его через [`inject: true` |services#Режим inject]. О них говорится в [отдельной главе |best-practices:inject-method-attribute].


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

- Конструктор подходит для обязательных зависимостей, без которых класс совершенно не может работать.
- Сеттер, наоборот, подходит для необязательных зависимостей или тех, которые позже может понадобиться поменять.
- Публичные свойства в целом не рекомендуются.

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

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

  • внедрение через конструктор
  • внедрение через метод (так называемое внедрение через сеттер)
  • внедрение в свойство
  • с помощью метода inject*() или атрибута #[Inject]

Покажем каждый вариант на конкретных примерах.

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

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

class MyClass
{
	private Cache $cache;

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

$obj = new MyClass($cache);

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

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

// 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;

	// ⛔ АД КОНСТРУКТОРОВ
	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 (isset($this->cache)) {
			throw new RuntimeException('The dependency has already been set');
		}
		$this->cache = $cache;
	}
}

Вызов сеттера задаётся в конфигурации DI-контейнера в ключе setup. Здесь тоже используется автоматическая передача зависимостей через autowiring:

services:
	-	create: MyClass
		setup:
			- setCache

Внедрение в свойство

Зависимости передаются записью прямо в свойство объекта:

class MyClass
{
	public Cache $cache;
}

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

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

Присваивание свойства задаётся в конфигурации DI-контейнера в секции setup:

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

Inject

Три предыдущих подхода применимы вообще во всех объектно-ориентированных языках, а внедрение через методы inject*() или атрибут #[Inject] обычно используется с презентерами Nette, где оно включено по умолчанию; любой другой сервис может подключить его через inject: true. О них говорится в отдельной главе.

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

  • Конструктор подходит для обязательных зависимостей, без которых класс совершенно не может работать.
  • Сеттер, наоборот, подходит для необязательных зависимостей или тех, которые позже может понадобиться поменять.
  • Публичные свойства в целом не рекомендуются.