Стандартизация ответов API без трейтов
Проблема
Заметил я, что большая часть библиотек, созданных для апи респонса, реализованы через трейты, остальная часть — огромные библиотеки. В этих трейтах реализованы методы под всё, что только можно (response, accepted, created, forbidden…)
Таким образом, если в моём контроллере 1–2 метода, то, подключая такой трейт, я имею в классе кучу ненужного мусора. В паре больших библиотек на 700+ звёзд я вижу overengineering на уровне UX (для себя, как для пользователя библиотеки)
Что делать?
Написать свою библиотеку!
Я решил создать такую логику обработки данных, чтобы на пользовательском уровне требовалось:
- минимум действий
 - имелась простота использования
 - читаемость
 
То есть для получение стандартизированного респонса, всё, что нам нужно, это вернуть респонс через объект библиотеки
composer require pepperfm/api-responder-for-laravel
Итого, базовый минимум, который у нас есть сразу после установки библиотеки, это:
Успешный ответ:
{
  response: {
    data: {
      entities,
      meta: []|{},
      message: 'Success'
    }
  }
}
Ответ с ошибкой:
{
  response: {
    data: {
      errors: null,
      message: 'Error'
    }
  }
}
public function __construct(public ResponseContract $json)
{
}
public function index(Request $request)
{
    $users = User::query()->get();
    return $this->json->response($users);
}
public function store(UserService $service)
{
    try {
        app('db')->beginTransaction();
        
        $service->update(request()->input());
        
        app('db')->commit();
    } catch (\Exception $e) {
        app('db')->rollback();
        logger()->debug($e->getMessage());
        return $this->json->error(
            message: $e->getMessage(),
            httpStatusCode: $e->getCode()
        );
    }
    return $this->json->response($users);
}
На выходе при успешном ответе имеем распаковку формата в виде: response.data.entities
По-умолчанию формат актуален в контексте REST, то есть для методов show() и update() ответ будет формата: response.data.entity
Глубокое погружение
Конечно, для любителей кастомизации и копания в конфигах я так же создал песочницу кода, с которым можно поиграться
Возможности
Любимый нами сахар
Обёртка над response() метдом для пагинации:
/*
 * Generate response.data.meta.pagination from first argument of paginated() method  
 */
public function index(Request $request)
{
    $users = User::query()->paginate();
    return $this->json->paginated($users);
}
Метод paginated() принимает два основных параметра:
array|\Illuminate\Pagination\LengthAwarePaginator $data,
array|\Illuminate\Pagination\LengthAwarePaginator $meta = [],
В своей логике резолвит их и добавляет в ответ по ключу meta — ключ pagination
Интерфейсы ответа в соответствии с форматом, возвращаемым Laravel:
export interface IPaginatedResponse<T> {
    current_page: number
    per_page: number
    last_page: number
    data: T[]
    from: number
    to: number
    total: number
    prev_page_url?: any
    next_page_url: string
    links: IPaginatedResponseLinks[]
}
export interface IPaginatedResponseLinks {
    url?: any
    label: string
    active: boolean
}
В итоге ответ получается формата:
{
  response: {
    data: {
      entities,
      meta: {
        pagination: ...
      },
      message: 'Success'
    }
  }
}
Обёртка над response() метдом для кодов ответа:
public function store(UserService $service)
{
    ...
    // message: 'Stored', httpStatusCode: JsonResponse::HTTP_CREATED
    return $this->json->stored();
}
public function destroy()
{
    ...
    // message: 'Deleted', httpStatusCode: JsonResponse::HTTP_NO_CONTENT
    return $this->json->deleted();
}
Работа с разными типами параметра
Первый аргумент метода response() может быть типов array|Arrayable, поэтому можно маппить данные перед передачей в метод в рамках этих типов. К примеру:
public function index()
{
    $users = User::query()->paginate();
    $dtoCollection = $users->getCollection()->mapInto(UserDto::class);
    return resolve(ResponseContract::class)->paginated(
        data: $dtoCollection,
        meta: $users
    );
}
public function index()
{
    $users = SpatieUserData::collect(User::query()->get());
    return \ApiBaseResponder::response($users);
}
Кастомизация через конфиг
Сам конфиг:
return [
    'plural_data_key' => 'entities',
    'singular_data_key' => 'entity',
    'using_for_rest' => true,
    'methods_for_singular_key' => ['show', 'update'],
    'force_json_response_header' => true,
];
- отключение 
using_for_restоставляет возвращаемый формат всегдаresponse.data.entities(мн. ч.) не зависимо от метода, из которого происходит вызов - с помощью 
methods_for_singular_keyможно пополнить список методов, в которых будет возвращаться ключ в ед. ч. methods_for_singular_key, собственно, добавляет заголовок в запросы по классике:$request->headers->set('Accept', 'application/json');
Кастомизация через атрибуты
Блокировка значений using_for_rest и methods_for_singular_key в конфиге для установки ключа ответа в соответствии с singular_data_key
#[ResponseDataKey]
public function attributeWithoutParam(): JsonResponse
{
    // response.data.entity
    return BaseResponse::response($this->user);
}
По аналогии можно передать своё назавние ключа
#[ResponseDataKey('random_key')]
public function attributeWithParam(): JsonResponse
{
    // response.data.random_key
    return BaseResponse::response($this->user);
}
В итоге
Осноанвя потребность закрыта: я хотел иметь возможность просто поставить библиотеку, и просто иметь из коробки лаконичный базис для стандартизации формата ответа. Без лишних движений.
И, конечно, как возможностей для лишних движений (кастомизация), так и ненасыпанного сахара — ещё очень много, так что в доработке библиотеки всё впереди) но основной посыл я точно сохраню! Ибо, как гласят великие мемы истории:
Красота в простате