Генерируемые фабрики
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
ID автора
статьи:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
private int $authorId,
) {
}
}
Параметр добавим также в фабрику:
interface ArticleFactory
{
function create(int $authorId): Article;
}
Благодаря тому, что параметр в конструкторе и параметр в фабрике называются одинаково, 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)
Accessor
Nette умеет кроме фабрик генерировать и так называемые accessory. Это
объекты с методом get()
, который возвращает определенный сервис
из DI-контейнера. Повторный вызов get()
возвращает все тот же
экземпляр.
Accessor предоставляют зависимостям lazy-loading. Представим класс, который
записывает ошибки в специальную базу данных. Если бы этот класс
получал подключение к базе данных как зависимость через конструктор,
подключение всегда должно было бы создаваться, хотя на практике ошибка
возникает лишь изредка, и, следовательно, в большинстве случаев
соединение оставалось бы неиспользованным. Вместо этого класс
передаст себе accessor, и только когда будет вызван его get()
,
произойдет создание объекта базы данных:
Как создать accessor? Достаточно написать интерфейс, и Nette DI сгенерирует
реализацию. Интерфейс должен иметь ровно один метод с именем get
и объявлять возвращаемый тип:
interface PDOAccessor
{
function get(): PDO;
}
Accessor добавим в файл конфигурации, где также находится определение сервиса, который он будет возвращать:
services:
- PDOAccessor
- PDO(%dsn%, %user%, %password%)
Поскольку accessor возвращает сервис типа PDO
, а в конфигурации
есть единственный такой сервис, он будет возвращать именно его. Если бы
сервисов данного типа было больше, мы бы определили возвращаемый
сервис с помощью имени, например, - PDOAccessor(@db1)
.
Множественная фабрика/аксессор
Наши фабрики и accessory до сих пор умели всегда производить или
возвращать только один объект. Но можно очень легко создать и
множественные фабрики, комбинированные с accessory. Интерфейс такого
класса будет содержать любое количество методов с именами
create<name>()
и get<name>()
, например:
interface MultiFactory
{
function createArticle(): Article;
function getDb(): PDO;
}
Так что вместо того, чтобы передавать себе несколько сгенерированных фабрик и accessory, мы передадим одну более комплексную фабрику, которая умеет больше.
Альтернативно, вместо нескольких методов можно использовать
get()
с параметром:
interface MultiFactoryAlt
{
function get($name): PDO;
}
Тогда верно, что MultiFactory::getArticle()
делает то же самое, что и
MultiFactoryAlt::get('article')
. Однако альтернативная запись имеет тот
недостаток, что неясно, какие значения $name
поддерживаются, и
логически также нельзя в интерфейсе различить разные возвращаемые
значения для разных $name
.
Определение списком
Таким образом можно определить множественную фабрику в конфигурации:
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:
- App\Core\RouterFactory::createRouter
- App\Model\DatabaseAccessor(
db1: @database.db1.explorer
)