В фреймворке 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
захламляет код.
Таким образом, получаем метод ради метода (прим переводчика).
Всех благ!
От переводчика
Оригинальная статья имеет юмор с уклоном во вселенную "Властелин Колец", который я решил не переводить. Кроме того, помимо самого текста, были оптимизированы и блоки кода.