"Тонкий контроллер" — это то, к чему надо стремиться ))). Подразумевается, что контроллер состоит из методов, а уже тонкость методов в целом и говорит о тонкости всего контроллера.
Речь пойдет о "тонких контроллерах" в PHP и о том — как лично я это вижу(возможно кто то будет со мной согласен, а кто то будет иметь иную точку зрения).
Вообще про "тонкие контроллеры" — много где слышал и в разные временные периоды изучения веб программирования — возникало разное понимание этой фразы "тонкий контроллер", обозначу, что в моем случае — это было практически синонимом — "хорошая практика". А вот как именно это реализовать и что именно реализовывать — постараюсь продемонстрировать ниже.
Стек: php, laravel, controller, немного следующего: SOLID, чистая архитектура, DDD, Hexagonal(Onion, порты и адаптеры), CQRS.Испытуемый: CRUD(создание, чтение, обновление и удаление сущности) записей(post). В основном для примера буду использовать метод обновления записи(update). Авторизацию и проверку доступа — специально пропущу(в реальном проекте — пропускать не нужно))) )
Описан Route вот так мы будем попадать в наш метод контроллера update во всех дальнейших примерах. PUT http://0.0.0.0/posts/123
<?php //web.php declare(strict_types=1); Route::put('/posts/{postId}', [PostController::class, 'update'])->name('posts.update');
Controller
Описание контроллера — зависит от контекста используемой архитектуры и/или уровня разработчика(даже если ни какой умышленной архитектуры не используется — это тоже архитектура, в остальном может быть например DDD).
В общем для всех вероятных ситуаций:
Контроллер — это класс, который обрабатывает HTTP-запросы(POST, GET, UPDATE, DELETE) и возвращает ответы(200, 404, 403, 500).
Дальше по статье будем немного видоизменять понимание его и от этого(понимания) уже говорить про "тонкость контроллеров" — в контексте каждого уровня разработки(по уровням разбил интуитивно, как сам вижу это).
Итоговая реализация кода в каждом из контекстов — зависит именно от мотивации, которой придерживается разработчик, а мотивация уже в свою очередь — зависит от понимания конечной цели.
Например: Для того, чтоб сделать "тоньше" — нужно уменьшить количество строк в этом методе, а для этого и только для этого мы выносим правила валидации в отдельный файл. (Это один из примеров, не является по моему мнению верным понимание)
В каждом из контекстов ниже опишу свою точку видения по поводу этого с точки зрения разработчика — занимающегося этой задачей.
Контекст 1: Мой первый проект ))
-
Дешево
-
Низкий порог вхождения (знание основ Laravel, php и хватит, «работает — значит все правильно»)
Тонкость контроллера — чем меньше строчек в методе — тем он "тоньше".
Пример до рефакторинга: В целом работает, но надо "улучшить" и как то сделать короче(поменьше строчек)
public function update1(Request $request, int $postId): Response // Без архитектуры { // Валидация вручную вместо Form Request $validator = Validator::make($request->all(), [ 'title' => 'required|string|max:255', 'content' => 'required|string', ]); $data = $validator->validated(); try { // Поиск и обновление поста $post = Post::findOrFail($postId); $post->title = $data['title']; $post->content = $data['content']; $post->save(); // Кеширование \Cache::forget("post_{$postId}"); return response()->noContent(200); } catch (ModelNotFoundException $e) { // Не нашли пост return response()->noContent(404); } catch (\Exception $e) { // Не получилось обновить return response()->json([ 'error' => 'Server error' ], 500); } }
Рефакторинг: Для того, чтоб сделать "тоньше" — нужно уменьшить количество строк в этом методе, а для этого и только для этого мы:
-
выносим правила валидациив отдельный файл. (случайно сделали код чище) -
укоротили процесс обновленияпоста, ура — наш код стал еще короче (случайно сделали код грязнее)
class UpdatePostRequest extends FormRequest { public function rules(): array { return [ 'title' => ['required', 'min:3', 'max:255'], 'content' => ['nullable', 'min:3'], ]; } }
public function update1(UpdatePostRequest $request, int $postId): Response // Без архитектуры { try { Post::findOrFail($postId)->update($request->validated()); // Кеширование \Cache::forget("post_{$postId}"); return response()->noContent(200); } catch (ModelNotFoundException $e) { // Не нашли пост return response()->noContent(404); } catch (\Exception $e) { // Не получилось обновить return response()->json([ 'error' => 'Server error' ], 500); } }
Итог контекста:
обновление записи — теперь напрямую зависит от названия полей в HTTP запросе. А важно ли нам это в текущем контексте?? Я не уверен. Так что тут с задачей мы справились))).
Контекст 2: UseCase + контроллер = минимализм
-
Сколько я наделал, сколько переделал, сколько сайтов запилил(Опытный Laravel разработчик, знание некоторых паттернов)
Тонкость контроллера: не просто короткий, а минимализм — несколько строк это моя цель.
Продолжаем обновлять предыдущий код, но теперь мы уже выносим логику прочь из контроллера
Рефакторинг:
-
обработка исключений — вынесли
-
формирование ответа для фронта — вынесли
-
кеширование — вынесли
public function update2(UpdatePostRequest $request, int $postId, PostService $handler): Response // UseCase + контроллер = минимализм { return $handler->createOrUpdate($postId,$request); }
Метод createOrUpdate описывать не буду, так как в текущей теме — его реализация только отведет в сторону от главной темы про «тонкость контроллеров».
Итог контекста: У нас метод контроллера в одну строку ))) Вот он тончайший и «идеальнейший контроллер» — относительно текущего контекста.
Контекст 3: Разбиение по слоям (конечный вариант)
Тут уже понимание слоистой архитектуры(слои: Презентация, Приложение, Инфраструктура, Доменная область), разграничение кода по слоям и направление зависимостей в них.
Тонкость контроллера — определяется наличием кода только из слоя презентации и только для конкретного клиента нашей системы. А разбиение кода презентации уже идет относительно других классов этого слоя(презентации).
Например UpdatePostRequest — в отдельном классе отвечающим за правила валидации, при необходимости можно отдельный класс для ответа написать, но именно касаемо слоя презентации.Метод контроллера — не знает за то, как что то сохраняется(каким образом обновляется запись в БД), кешируется, и в целом взаимодействует в нашей система. Должен уметь только через DTO используя UseCase передать на следующий слой(слой приложения) входные данные, а дальше конвертирует ответ в соответствующий контракт для клиента откуда пришел запрос, например: отдает статус сервера и какой то json если это RestApi (какое то React приложение) или просто вывод текста, если это консольная команда.
То есть, теперь количество строк — имеет второстепенное значение.
Что мы сделали:
-
Отдали
UseCaseобновления постовминимальнонеобходимый набор данных. Добавивпрозрачностьк ожидаемымвходным даннымдля слоя приложения(Application layer) -
Убрали привязку к объектам(уменьшили связность) теперь при необходимости — мы можем обновлять запись из
любого места, даже там,где нет объекта Request -
Обработали исключения на стороне контроллера, то есть какой
ответконкретно в этомпредставлениинеобходим(для шаблонизатора и для API(реакт например)), так какответы отличаются от клиента к клиенту(RestApi, Console, Web(Blade и другие шаблонизаторы)) -
В случае, если нужно добавить
особый ответ для клиента в слое представлении— мы так же составляем его вметоде контроллераиз результата метода$handler->update(...). Таким образом нашапрезентация зависит от логики, алогика уже становится не зависима от презентации. -
Улучшаем тестирование — в таком случае — у нас будет единая точка для тестирования — куда мы уже подставляем явные скаляры.
-
Понимание
ответственностиметода контроллераотносительно слояпрезентации(Получить запрос и вернуть ответ в соответствии сконтрактомдляконкретного типа клиента). Соответствуем принципу единой ответственности(SOLID буква S(SRP))
public function update3( UpdatePostRequest $request, int $postId, PostService $handler ): Response // Разбиение по слоям { // Получаем входные данные $requestData = $request->validated(); try { $handler->update( // Используем DTO для передачи данных на другой слой new Command( // передаем скалярные данные, только, которые необходимы сервису для выполнения операции $postId, $requestData['title'], $requestData['content'], ) ); return response()->noContent(200); } catch (ModelNotFoundException $e) { // Не нашли пост return response()->noContent(404); } catch (\Exception $e) { // Не получилось обновить return response()->json([ 'error' => 'Server error' ], 500); } }
Вывод:
К конечному пониманию "тонкого контроллера" пришел эволюционным подходом. Возможно кто то может иметь отличную от моей точку зрения.Контекст 3 — считаю наиболее валидным использованием этого подхода. Плюсы описал выше.
Так же этот подход является частью реализации Hexagonal(архитектура построенная по принципу портов и адаптеров). Где:
Порт — это UseCase(какое то действие, которое выполняем, использовал CQRS(Там поясняется название классов для DTO) ) — обновить пост
$handler->update( // Используем DTO для передачи данных на другой слой new Command( // передаем скалярные данные, только, которые необходимы сервису для выполнения операции $postId, $requestData['title'], $requestData['content'], ) );
Адаптер — это реализация порта, примеры ниже:
Сам метод контроллера
public function update3( UpdatePostRequest $request, int $postId, PostService $handler ): Response // Разбиение по слоям
Так же это может быть например какой ни будь другой адаптер например через консоль хотим обновить пост
/** * Обновить пост из консоли */ public function updateCondoleAdapter(int $postId, string $title, string $content): void { $this->postService->update( // Используем DTO для передачи данных на другой слой new Command( $postId, $title, $content, ) ); }
P.S. Мнение о том, что "Тонкий контроллер" — это малое количество строк — думаю связано с тем, что зачастую в проектах — можно встретить действительно внушительно многострочные методы контроллеров, где в одном месте находится вся реализация необходимой задачи.
И для более легкого порога вхождения(Или что б соответствовать корпоративному стилю, где все методы так описаны) в оптимизацию этого метода контроллера как раз и может быть представлено(разработчик может предположить), что уменьшение количества строк = оптимизация. Конечно косвенно — это может помочь, но на практике — убирает одни сложности и добавляет другие))
ссылка на оригинал статьи https://habr.com/ru/articles/916738/
Добавить комментарий