Контроллеры и их истинное предназначение
Примеры кода взяты из исходного кода этого блога. Пожалуйста, перейдите в репозиторий, чтобы увидеть, как различные части связаны друг с другом.
Рефакторинг (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]);
}
}
Мы предприняли следующие действия:
- Правила валидации форм были перенесены в класс
FormRequest
- Компонент
Bus
теперь используется через статический прокси контейнер - Компонент
ResponseFactory
теперь используется через статический контейнерный прокси
Теперь, когда мы успешно провели рефакторинг (UI) Controller
для использования SL, мы можем начать говорить о его предназначении.
(UI) Controller как Корень Композиции/Composition Root
На самом деле контроллер (UI) Controller играет важную роль в жизненном цикле нашего приложения. Обычно веб-сервер получает запрос, передаёт его процессу PHP, загружающему фреймворк, и, наконец, фреймворк передаёт запрос нам — (UI) Controller
. Исходя из этого, можно утверждать, что (UI) Controller
— это Корень Композиции/Composition Root нашего приложения. Это первый вызываемый фрагмент кода, над которым мы имеем полный контроль. Именно поэтому мне не важно, используете ли вы DI или SL в своём (UI) Controller
. Граф объектов должен быть каким-то образом составлен, поэтому смело используйте любой из них.
Зачем постоянно добавлять префикс "UI" к Controller
Я рад, что вы спросили. Это потому, что я хотел бы сделать акцент именно на этом факте: задача UI Controller
— оркестровать жизненный цикл Request
— Response
, обычно инициируемый пользователем через пользовательский интерфейс. Ключевое слово здесь — оркестрация. Основной задачей контроллера должно быть решение проблем, связанных с 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
, все будет работать и ничего не сломается.
Резюме
- Основная задача
Controller
— работа с пользовательским интерфейсом. - Все остальное
Controller
должен делегировать приложению. Controller
должен возвращать удобные для пользователя сообщения об ошибках в случае сбоев.
Присоединяйтесь к обсуждению на сайте X (бывший Twitter)! Я буду рад узнать, что вы думаете об этой статье.
Спасибо за внимание!