Laravel: Классы Response

Источник: «Laravel Response Classes»
Ответ от Laravel приложения — это то, что я бы назвал жизненно важным, особенно когда вы создаёте API. Давайте посмотрим, как можно улучшить наши ответы.

Многие из нас обычно начинают с использования вспомогательных функций (хелперов) в приложениях, поскольку они используются в документации и во многих руководствах. С ними легко начать, и они делают именно то, что вы от них ожидаете. Давайте посмотрим, как они выглядят:

return response()->json(
data: [],
status: 200,
);

Это немного утрированный пример; обычно вы отправляете данные и пропускаете код состояния. Однако для меня привычки умирают с трудом!

Этот код создаст новый JsonResponse и передаст данные и код состояния, готовые для возврата. Это работает, и в использовании этого подхода нет ничего плохого. Если вы уже используете это, способ активизировать свою API игру — добавить код состояния, чтобы быть более декларативным в том, что вы возвращаете.

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

return new JsonResponse(
data: [],
status: 200,
);

Мне нравится этот подход, так как он меньше полагается на хелперы и более декларативен. Глядя на код, вы точно знаете, что возвращается, потому что он находится прямо перед вами, а не абстрагируется за хелпером. Вы можете повысить уровень используя константу или другой способ объявления кода состояния (status code), что делает его ещё более доступным для чтения и понимания для разработчиков, которые могут не знать все коды состояния наизусть. Давайте посмотрим ка это может выглядеть:

return new JsonResponse(
data: [],
status: JsonResponse::HTTP_OK,
);

Класс JsonResponse расширяет класс Symfony Response через несколько уровней абстракции, так что вы можете вызвать его напрямую — однако ваш статический анализатор может пожаловаться на это. Я создал пакет juststeveking/http-status-code, PHP Enum, которое будет возвращать что-то похожее, и его единственная задача — возвращать коды состояния. Я предпочитаю этот более лёгкий утилитарный подход к подобным вещам, поскольку вы точно знаете, что происходит и что может делать этот класс или пакет. Иногда проблема заключается в том, что класс, который вы используете, делает так много, что вам приходится загружать эту огромную вещь в память только для того, чтобы иметь возможность вернуть целочисленное значение. Это не имеет особого смысла, поэтому рекомендую использовать отдельный пакет или класс, для управления этим. Давайте посмотрим, как это будет выглядеть:

return new JsonResponse(
data: [],
status: Http::OK->value,
);

Это значительный шаг вперёд с точки зрения ясности того, насколько декларативным является наш код. Всё происходящее читается легко и понятно. Однако мы обнаруживаем, что раз за разом создаём один и тот же блок кода. Как же решить эту проблему?

Ответ прост — классы Response. В Laravel есть контракт Responsable, который мы можем использовать, сообщающий, что наш класс должен иметь метод toResponse. Мы можем вернуть его прямо из наших контроллеров, так как Laravel без проблем разберёт и поймёт эти классы. Давайте посмотрим на краткий базовый пример, того как выглядят эти классы:

class MyJsonResponse implements Responsable
{
public function __construct(
public readonly array $data,
public readonly Http $status = Http::OK,
) {}

public function toResponse($request): Response
{
return new JsonResponse(
data: $this->data,
status: $this->status->value,
);
}
}

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

class CollectionResponse implements Responsable
{
public function __construct(
public readonly JsonResourceCollection $data,
public readonly Http $status = Http::OK,
) {}

public function toResponse($request): Response
{
return new JsonResponse(
data: $this->data,
status: $this->status->value,
);
}
}

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

return new CollectionResponse(
data: UserResource::collection(
resource: User::query()->get(),
),
);

Он чище, в нём меньше дублирования кода, и его легко переопределить в случае необходимости. Это даёт нам преимущество, которое давали вспомогательные классы (хелперы) и класс JsonResponse, но он даёт больше контекста и предсказуемости.

Однако сейчас мы столкнулись с проблемой дублирования в других областях. В самих наших ответах. Многие из них выглядят одинаково, с той лишь разницей, что свойства конструктора будут разных типов. Мы хотим сохранить контекст использования пользовательских классов ответов, но мы хотим избежать создания чего-то с обширным аргументом типа union для свойства — когда мы можем добавить тип mixed и покончить с этим.

В этой ситуации вы можете либо использовать абстрактный класс для расширения, либо трейт, чтобы добавить поведение к классам, которые в этом нуждаются. Я предпочитаю композицию наследованию, поэтому использование трейта имеет для меня больше смысла.

trait SendsResponse
{
public function toResponse($request): Response
{
return new JsonResponse(
data: $this->data,
status: $this->status->value,
);
}
}

Самая большая проблема с этим подходом заключается в том, что статический анализ будет жаловаться на этот код, потому что трейт должен иметь или знать о свойствах этого класса. Однако это легко исправить.

/**
* @property-read mixed $data
* @property-read Http $status
*/

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

Теперь наши Response классы будут намного проще в использовании и создании, с меньшим количеством повторов в коде.

class MessageResponse implements Responsable
{
use SendsResponse;

public function __construct(
public readonly array $data,
public readonly Http $status = Http::OK,
) {}
}

Теперь мы легко можем создать все потенциальные ответы, которые нам нужно отправить, сохранив безопасность типов и снизив дублирование кода.

Дополнительные материалы

Предыдущая Статья

PHP: Добавьте ИИ в проект с помощью OpenAI

Следующая Статья

OpenAI PHP Client. Документация