Один класс, что правит всеми или как улучшить респонс в Laravel

В фреймворке Laravel есть встроенный хелпер, который преобразует данные для вывода используя функцию json_encode и устанавливает в ответном заголовке Content-Type значение application/json. Например:

<?php

return response()->json([
    'id' => 100,
    'name' => 'Alexey Shatrov'
]);

Этот хелпер поддерживает HTTP коды, заголовки и JSON флаги, что делает его действительно полезным во многих случаях. Однако есть ситуации, когда такой подход становится неудобным.

Представьте, что Вам нужно вернуть 201-й код после создания ресурса. Звучит и выглядит легко. Просто передаём код вторым параметром:

<?php

return response()->json([
    'id' => 100,
    'name' => 'Alexey Shatrov'
], 201);

Теперь представьте, что, по мере роста API, Вам понадобится реализовать ещё одну модификацию ответа - например, добавить флаг JSON_UNESCAPED_UNICODE.

Опять же, нет проблем и такая модификация проста, но сам подход требует много времени, так как придётся вручную искать каждый вызов response()->json() в приложении и добавлять флаг параметр передачи флага:

<?php

return response()->json([
    'id' => 100,
    'name' => 'Alexey Shatrov'
], 201, options: JSON_UNESCAPED_UNICODE);

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

Преобразование

Давайте добавим немного магии создав класс с именем Response, который будет управлять всеми ответами. Данный класс будет реализовывать простую логику, основанную на переданном HTTP-коде: если код успешен, вернёт массив с ключом data, иначе - errors.

Итак, приступим. Создадим инвокабельный класс:

<?php

namespace App\Http\Responses;

use Illuminate\Http\JsonResponse;

class Response
{
    public function __invoke(mixed $data, int $statusCode): JsonResponse
    {
        return $this->payload($this->data($data, $statusCode), $statusCode);
    }

    protected function data(mixed $data, int $statusCode): array
    {
        return $statusCode < 400 ? compact('data') : ['errors' => $data];
    }

    protected function payload(array $data, int $statusCode): JsonResponse
    {
        return response()->json($data, $statusCode, options: JSON_UNESCAPED_UNICODE);
    }
}

Теперь мы можем заменить вызов функции response()->json() на наш класс:

<?php

return (new Response)([
    'id' => 100,
    'name' => 'Alexey Shatrov'
], 201);

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

Однако каждый раз указывать коды ответов будет неудобно, как и не иметь контроля над передаваемыми параметрами не говоря уже о внешнем виде вызова. Вы легко можете допустить ошибку передав, например, 400-й код с "успешными" данными или наоборот. Чтобы исправить это недоразумение, добавим публичные методы:

<?php

class Response
{
    public static function ok(mixed $data): JsonResponse
    {
        return (new static)($data, 200);
    }

    public static function created(mixed $data): JsonResponse
    {
        return (new static)($data, 201);
    }

    public static function notFound(string $message = 'Item Not Found'): JsonResponse
    {
        return (new static)($message, 404);
    }

    public function __invoke(mixed $data, int $statusCode): JsonResponse
    {
        return $this->payload($this->data($data, $statusCode), $statusCode);
    }

    protected function data(mixed $data, int $statusCode): array
    {
        return $statusCode < 400 ? compact('data') : ['errors' => $data];
    }

    protected function payload(array $data, int $statusCode): JsonResponse
    {
        return response()->json($data, $statusCode, options: JSON_UNESCAPED_UNICODE);
    }
}

Пример использования:

<?php

public function someAction()
{
    // some logic

    return $user ? Response::ok($user) : Response::notFound();
}

Такой код вполне понятен и удобен, а с классом Response легко работать. Его также можно расширить проверкой допустимых пределов HTTP кода (например, 200 <= x < 600), но это Вы можете сделать самостоятельно при необходимости, поэтому писать пример кода здесь не будем)

Где же Responsable интерфейс?

Я не рекомендую использовать интерфейс Responsable не только потому, что придётся реализовывать метод toResponse(Request $request), передавая ему объект реквеста, но ещё и потому, что:

Таким образом, получаем метод ради метода (прим переводчика).

Всех благ!

От переводчика

Оригинальная статья имеет юмор с уклоном во вселенную "Властелин Колец", который я решил не переводить. Кроме того, помимо самого текста, были оптимизированы и блоки кода.

@Helldar
19.02.2024 13:00 UTC
Первоисточник

Комментарии

@Dualse
19.02.2024 08:32 UTC
+3

А чем не подходят API Resource?

@shod
19.02.2024 09:01 UTC
+1

Вот как раз для этого они и существуют )

@deathcatcher
19.02.2024 10:50 UTC
+2

100% подходят и даже больше.
Нет смысла создавать кастомные вещи, и потом их поддерживать и мучаться во время обновлений фрйэмворка и тп.

@Helldar
19.02.2024 12:59 UTC
0

Встречал и тех кто им противится. Поэтому решил перевести статью чтобы была. Про ресурсы завтра дропну ?

@
19.02.2024 17:23 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@Helldar
19.02.2024 17:44 UTC
0

Не исключено. Но лично по моему опыту почти трёхлетней работы с Lumen и полным отказом от использования ресурсов (так исторически сложилось), делали обёртку вроде той что в статье. И нет, это было не в лохматых годах - в начале 20-х. Внезапно.

В том числе по этой причине я решил перевести статью и выложить на Хабре.

@yoda_code
20.02.2024 16:20 UTC
0

В Laravel есть прекрасный сервис контейнер, данную статью можно было бы дополнить как реализовать инъекцию зависимостей используя что-то вроде ResponseProvider. Т.к. статья о том как улучшить, чтобы затем грамотнее было работать, когда потребуются правки. А в итоговом коде в каждом методе классы создаются внутри. Соответственно, если решите поменять класс, нужно править каждый метод и проверять все места где они используются.

@TimsTims
20.02.2024 18:06 UTC
0

Теперь представьте, что, по мере роста API, Вам понадобится реализовать ещё одну модификацию ответа - например, добавить флаг JSON_UNESCAPED_UNICODE

Тогда идеи лесом обратная совместимость апи. Представьте, что ваши потребители давно ждут определенный формат ответа, а вы таким образом все свои апишки сломали. Обычно если и надо какое то апи расширить, то его точно и добавляют, но не ломают уже существующие.

Опять же если у вас не такая ситуация, что потребитель один - и это вы сами.