Генерируемые фабрики
Nette DI умеет автоматически порождать код фабрик на основе интерфейсов, избавляя вас от написания кода.
Фабрика – это класс, отвечающий за создание объектов и передачу их зависимостей. Не путайте это с шаблоном проектирования factory method, который описывает конкретный способ использования фабрик и с этой темой не связан.
Как выглядит такая фабрика, мы показали во вводной главе:
class ArticleFactory
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function create(): Article
{
return new Article($this->db);
}
}
Nette DI умеет порождать код фабрики автоматически. Вам достаточно
создать интерфейс, а реализацию сгенерирует Nette DI. У интерфейса должен
быть ровно один метод с именем create и объявленным типом
возвращаемого значения:
interface ArticleFactory
{
function create(): Article;
}
Итак, у фабрики ArticleFactory есть метод create, создающий
объекты Article. Класс Article может выглядеть, например, так:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
}
Добавьте фабрику в конфигурационный файл:
services:
- ArticleFactory
Nette DI сгенерирует соответствующую реализацию фабрики.
В коде, который использует фабрику, запрашивайте объект по его интерфейсу, и Nette DI предоставит порождённую реализацию:
class UserController
{
public function __construct(
private ArticleFactory $articleFactory,
) {
}
public function foo()
{
// пусть фабрика создаст объект
$article = $this->articleFactory->create();
}
}
Фабрика с параметрами
Метод фабрики create может принимать параметры, которые он затем
передаёт в конструктор. Например, добавим в класс Article
идентификатор автора статьи:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
private int $authorId,
) {
}
}
Добавим параметр и в фабрику:
interface ArticleFactory
{
function create(int $authorId): Article;
}
Поскольку имя параметра в конструкторе ($authorId) совпадает с
именем параметра в методе фабрики, Nette DI передаёт его автоматически.
Расширенное определение
Определение можно записать и в многострочной форме с помощью ключа
implement:
services:
articleFactory:
implement: ArticleFactory
Такая более длинная форма позволяет указать дополнительные
аргументы конструктора через ключ arguments и дальнейшую настройку
через setup, как у обычных определений сервисов.
Пример: если бы метод create() не принимал параметр $authorId,
мы могли бы задать в конфигурации фиксированное значение,
передаваемое в конструктор Article:
services:
articleFactory:
implement: ArticleFactory
arguments:
authorId: 123
И наоборот, если бы create() принимал $authorId, но тот не входил
бы в конструктор, а передавался бы методом вроде Article::setAuthorId(), мы
сослались бы на параметр в секции setup:
services:
articleFactory:
implement: ArticleFactory
setup:
- setAuthorId($authorId)
Аксессор
Помимо фабрик Nette умеет порождать и так называемые аксессоры. Это
объекты с методом get(), возвращающим определённый сервис из
DI-контейнера. Повторные вызовы get() всегда возвращают один и тот
же экземпляр.
Аксессоры дают отложенную загрузку зависимостей. Представьте класс,
который записывает ошибки в отдельную базу данных. Если бы этот класс
получал соединение с базой через внедрение в конструктор, соединение
устанавливалось бы всегда, даже если ошибки случаются редко и
соединение почти всё время не используется. Вместо этого класс может
получить аксессор. Объект базы данных (соединение) создаётся только
тогда, когда метод get() аксессора вызывается впервые.
Как создать аксессор? Достаточно написать интерфейс, а реализацию
сгенерирует Nette DI. У интерфейса должен быть ровно один метод с именем
get, без параметров и с объявленным типом возвращаемого
значения:
interface PDOAccessor
{
function get(): PDO;
}
Добавьте аксессор в конфигурационный файл вместе с определением сервиса, который он должен возвращать:
services:
- PDOAccessor
- PDO(%dsn%, %user%, %password%)
Поскольку аксессор возвращает сервис PDO, а такой сервис в
конфигурации определён только один, аксессор вернёт именно его. Если
сервисов такого типа несколько, укажите по имени, какой из них должен
вернуть аксессор, например - PDOAccessor(@db1).
Мультифабрика и мультиаксессор
До сих пор наши фабрики и аксессоры умели создавать или возвращать
только один тип объектов. Однако вы легко можете создать
мультифабрики, сочетающие возможности фабрик и аксессоров. Интерфейс
такой составляющей может содержать несколько методов с именами
create<Name>() и get<Name>(), например:
interface MultiFactory
{
function createArticle(): Article;
function getDb(): PDO;
}
Так что вместо внедрения нескольких отдельных фабрик и аксессоров вы можете внедрить одну более всеобъемлющую составляющую.
Как вариант, вместо нескольких методов можно использовать get()
с параметром:
interface MultiFactoryAlt
{
function get($name): PDO;
}
Тогда MultiFactory::getDb() делает то же самое, что
MultiFactoryAlt::get('db'). Однако у этой альтернативной записи есть
недостаток: из сигнатуры интерфейса не видно явно, какие значения
$name поддерживаются. Кроме того, в интерфейсе нельзя задать
разные типы возвращаемых значений для разных значений $name.
Вместо get($name) интерфейс может объявить create($name), который
при каждом вызове возвращает новый экземпляр (тогда как get()
возвращает общий). В интерфейсе может быть только один такой метод с
параметром. Если тип возвращаемого значения метода nullable (например,
?PDO), для неизвестного $name он возвращает null вместо
выбрасывания исключения.
Определение списком
Мультифабрику можно определить в конфигурации списком, записав сервисы прямо в нём:
services:
- MultiFactory(
article: Article() # задаёт createArticle()
db: PDO(%dsn%, %user%, %password%) # задаёт getDb()
)
Как вариант, в определении мультифабрики можно сослаться на существующие сервисы через ссылки:
services:
article: Article
- PDO(%dsn%, %user%, %password%)
- MultiFactory(
article: @article # задаёт createArticle()
db: @\PDO # задаёт getDb()
)
Определение тегами
Ещё один способ определить мультифабрику – использовать теги. Значение тега определяет имя соответствующего метода:
services:
article:
create: Article
tags: {multi: article} # задаёт createArticle()
db:
create: PDO(%dsn%, %user%, %password%)
tags: {multi: db} # задаёт getDb()
- MultiFactory(tagged: multi)