Контроллеры и их истинное предназначение

Источник: «Controllers and their true purpose»
На прошлой неделе я написал в твиттере о том, как выглядят контроллеры в моих приложениях и как я вообще к ним отношусь. Этот твит быстро стал вирусным и привлёк к себе много внимания, но, к сожалению, не по тем причинам. Поэтому в этой статье я хотел бы пролить свет на то, к чему я стремился, и объяснить, каким должен быть (UI) контроллер в целом.

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

Рефакторинг (UI) контроллера

Обсуждения под этим твитом в основном касались (DI) против (SL). Однако суть твита заключалась не в этом. Итак, давайте уделим минуту рефакторингу кода, чтобы использовать SL:

final readonly class SubmitContactFormController
{
public function __invoke(SubmitRequest $request): RedirectResponse
{
$command = new ContactMuhammed(
$request->input('email'),
$request->ip(),
$request->input('message'),
$request->input('name'),
);

Bus::dispatch($command);

return Redirect::route('contact', ['success' => true]);
}
}

Мы предприняли следующие действия:

Теперь, когда мы успешно провели рефакторинг (UI) Controller для использования SL, мы можем начать говорить о его предназначении.

(UI) Controller как Корень Композиции/Composition Root

На самом деле контроллер (UI) Controller играет важную роль в жизненном цикле нашего приложения. Обычно веб-сервер получает запрос, передаёт его процессу PHP, загружающему фреймворк, и, наконец, фреймворк передаёт запрос нам — (UI) Controller. Исходя из этого, можно утверждать, что (UI) Controller — это Корень Композиции/Composition Root нашего приложения. Это первый вызываемый фрагмент кода, над которым мы имеем полный контроль. Именно поэтому мне не важно, используете ли вы DI или SL в своём (UI) Controller. Граф объектов должен быть каким-то образом составлен, поэтому смело используйте любой из них.

Зачем постоянно добавлять префикс "UI" к Controller

Я рад, что вы спросили. Это потому, что я хотел бы сделать акцент именно на этом факте: задача UI Controller — оркестровать жизненный цикл RequestResponse, обычно инициируемый пользователем через пользовательский интерфейс. Ключевое слово здесь — оркестрация. Основной задачей контроллера должно быть решение проблем, связанных с UI (пользовательским интерфейсом), таких, как валидация формы, рендеринг представления, создание перенаправления и т.д. Если Controller соответствует своему истинному назначению, то не имеет значения, будет ли он состоять из 10 или 90 строк. Он должен решать проблемы UI, и решать их хорошо. Все остальное не должно находиться внутри Controller и должно быть передано Application.

Возможно, это звучит несколько нелогично, но CLI Command — это тоже Controller. Она также принимает пользовательский ввод и что-то с ним делает, хотя и несколько иным способом. Компоненты Livewire? Да, это тоже Controller. Только динамические, использующие XHR на фронтенде.

Передача сообщений в приложение

Когда в наш Controller поступает запрос, должно что-то произойти. Кто-то попытался вызвать определённое поведение в нашей системе. Намерение пользователя представлено объектом команды, или, другими словами, приложение представлено этим единственным объектом команды:

$command = new ContactMuhammed(
$request->input('email'),
$request->ip(),
$request->input('message'),
$request->input('name'),
);

Отношения Controller с Application начинаются и заканчиваются здесь. Он пересылает сообщение, в данном случае команду, приложению и заканчивает работу. ContactMuhammed — это контракт между Controller и Application, обрабатывающим эту команду. Пока этот контракт соблюдается и остаётся неизменным, все будет работать без сбоев. Это то, что называется "слабая связанность", и именно об этом шла речь в моем первоначальном твите.

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

Разве это не паттерн "Action"?

Нет, это не так. Паттерн Action — это переименованная версия паттерна GoF Command, представляющего собой самостоятельную команду.

Если мы внимательно посмотрим на ContactMuhammed, то увидим, что в него встроено 0 бизнес-логики:

final readonly class ContactMuhammed implements ShouldQueue
{
public function __construct(
public string $email,
public string $ipAddress,
public string $message,
public string $name,
) {}
}

ContactMuhammed — это то, что можно назвать EIP-командой. Она отражает намерение пользователя, и только. Больше ничего. Зоркий глаз читателя, возможно, уже заметил, что это также Data Transfer Object, хотя и более специфический.

А как насчёт стороны запросов

Правда, до сих пор мы говорили только о командах. Однако запрос некоторых данных и их возврат пользователю ничего не меняет в конструкции Controller. Если команды могут обрабатываться Application асинхронно, то запросы, как правило, синхронны, и, таким образом, мы имеем временную связность с нашим Application.

Именно по этой логике и создаётся эта запись в блоге:

final readonly class ReadBlogPostController
{
public function __construct(
private GetSinglePost $posts,
private ResponseFactory $response,
) {}

public function __invoke(string $slug): Response
{
$post = $this->posts->findBySlug($slug);

return $this->response->view('read-blog-post', $post->toArray());
}
}

GetSinglePost — это контракт между Controller и нашим Application. Пока он будет возвращать представление модели Post, все будет работать и ничего не сломается.

Резюме

Присоединяйтесь к обсуждению на сайте X (бывший Twitter)! Я буду рад узнать, что вы думаете об этой статье.

Спасибо за внимание!

Дополнительные материалы

Предыдущая Статья

Семь "трюков" с dd() в Laravel

Следующая Статья

Пакет Laravel htmx