Тонкий контроллер (php)

от автора

Краткое объяснение тонкого контроллера

Краткое объяснение тонкого контроллера

"Тонкий контроллер" — это то, к чему надо стремиться ))). Подразумевается, что контроллер состоит из методов, а уже тонкость методов в целом и говорит о тонкости всего контроллера.

Речь пойдет о "тонких контроллерах" в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *