Контроллеры и их истинное предназначение
Примеры кода взяты из исходного кода этого блога. Пожалуйста, перейдите в репозиторий, чтобы увидеть, как различные части связаны друг с другом.
Рефакторинг (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)! Я буду рад узнать, что вы думаете об этой статье.
Спасибо за внимание!