Pull to refresh

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

Level of difficultyEasy
Reading time3 min
Views4.4K
Original author: Alexey Shatrov

В фреймворке 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), передавая ему объект реквеста, но ещё и потому, что:

  • метод toResponse возвращает инстанс Symfony\Component\HttpFoundation\Response в то время как мы ожидаем JsonResponse;

  • крайне редко возникает необходимость в передаче объекта реквеста внутрь обрабатываемого класса;

  • передача ненужной переменной в класс только для реализации метода toResponse захламляет код.

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

Всех благ!

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

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 4: ↑2 and ↓20
Comments8

Articles