json(ResponseService::apiPaginated($q->paginate(), BookResource::class)); * return response()->json(ResponseService::apiItem($book, BookResource::class)); * return response()->json(ResponseService::apiResponse(['token' => $token]), 201); */ class ResponseService { /** * Build a raw `{ data, meta }` envelope with whatever meta you pass. * * Lowest-level primitive — every other method here ultimately produces * this same shape. Prefer the higher-level helpers * (`apiResponse`, `apiItem`, `apiCollection`, `apiPaginated`) which * auto-fill the standard meta block. */ public static function response(mixed $data = null, array $meta = []): array { return [ 'data' => $data, 'meta' => $meta, ]; } /** * Available content languages for the running app. * * Resolution order: * 1. `config('languages.languages')` — Blax convention, list of * `{ code, ... }` records. * 2. `config('app.available_locales')` — plain array of codes. * 3. Fallback to `[app()->getLocale()]`. * * @return array */ public static function availableLanguages(): array { $configured = config('languages.languages'); if (is_array($configured) && $configured) { return collect($configured) ->map(fn ($l) => is_array($l) ? ($l['code'] ?? $l['lang'] ?? null) : $l) ->filter() ->values() ->toArray(); } $locales = config('app.available_locales'); if (is_array($locales) && $locales) { return array_values($locales); } return [app()->getLocale()]; } /** * Standard meta block: `url`, `locale`, `languages`, plus any extras. * * Pagination keys are merged in by {@see apiPaginated()}; * {@see apiItem()} and {@see apiCollection()} skip them. * * @param array $extra * @return array */ public static function apiMeta(array $extra = []): array { return array_merge([ 'url' => optional(request())->fullUrl(), 'locale' => app()->getLocale(), 'languages' => self::availableLanguages(), ], $extra); } /** * Single-item envelope. Use for any `show`-style endpoint. * * Pass the resource class to wrap the model in a `JsonResource`, or * omit it to serialize the value directly (this is also the form to * use for arbitrary payloads — login responses, action acks, etc.). * * @param class-string<\Illuminate\Http\Resources\Json\JsonResource>|null $resourceClass * @param array $extraMeta * @return array{data: mixed, meta: array} */ public static function apiItem(mixed $item, ?string $resourceClass = null, array $extraMeta = []): array { return self::response( $resourceClass !== null ? $resourceClass::make($item) : $item, self::apiMeta($extraMeta), ); } /** * Plain envelope with the standard meta block. * * Alias of `apiItem($data, null, $extraMeta)` kept for callers whose * intent is "arbitrary payload" rather than "model" — login responses, * acknowledgements, etc. * * @param array $extraMeta * @return array{data: mixed, meta: array} */ public static function apiResponse(mixed $data = null, array $extraMeta = []): array { return self::apiItem($data, null, $extraMeta); } /** * Non-paginated collection envelope. * * Reserve this for genuinely tiny fixed lists (an enum, a child * collection embedded in a parent show response). Most list endpoints * should use {@see apiPaginated()}. * * @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $resourceClass * @param array $extraMeta * @return array{data: mixed, meta: array} */ public static function apiCollection(iterable $items, string $resourceClass, array $extraMeta = []): array { $count = is_countable($items) ? count($items) : null; return self::response( $resourceClass::collection($items), self::apiMeta(array_merge(['total' => $count], $extraMeta)), ); } /** * Paginated envelope. Use for any `index`-style endpoint. * * Wire shape: * * { * "data": [...resource collection...], * "meta": { * "url", "locale", "languages", * "current_page", "per_page", "from", "to", * "total", "total_pages", "last_page", "has_more" * } * } * * `last_page` is exposed alongside `total_pages` as an alias so * consumers written against Laravel's native paginator key continue * to work without a migration. * * @param \Illuminate\Contracts\Pagination\Paginator|mixed $paginated * @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $resourceClass * @param array $extraMeta * @return array{data: mixed, meta: array} */ public static function apiPaginated(mixed $paginated, string $resourceClass, array $extraMeta = []): array { $arr = method_exists($paginated, 'toArray') ? $paginated->toArray() : []; $current = $arr['current_page'] ?? 1; $last = $arr['last_page'] ?? null; return self::response( $resourceClass::collection($paginated), self::apiMeta(array_merge([ 'current_page' => $current, 'per_page' => $arr['per_page'] ?? null, 'from' => $arr['from'] ?? null, 'to' => $arr['to'] ?? null, 'total' => $arr['total'] ?? null, 'total_pages' => $last, 'last_page' => $last, 'has_more' => $last !== null && $current < $last, ], $extraMeta)), ); } /** * Legacy paginated meta block: slimmer than {@see apiMeta()}, no * `url`/`locale`/`languages`, includes a passthrough `options` object * reflecting the request's filter/sort state. * * Retained for backward compatibility — new code should use * {@see apiPaginated()}. * * @param \Illuminate\Contracts\Pagination\Paginator|mixed $paginated * @param array $options * @param array $meta * @return array */ public static function paginationMeta(mixed $paginated, array $options = [], array $meta = []): array { $data = method_exists($paginated, 'toArray') ? $paginated->toArray() : []; $base = [ 'from' => $data['from'] ?? null, 'to' => $data['to'] ?? null, 'total' => $data['total'] ?? null, 'last_page' => $data['last_page'] ?? null, 'current_page' => $data['current_page'] ?? null, 'options' => (object) $options, ]; return $meta ? array_merge($base, $meta) : $base; } /** * Legacy paginated envelope. Same intent as {@see apiPaginated()} but * with the older meta shape — reads `options` off the current request * when none are supplied. * * Retained for backward compatibility — new code should use * {@see apiPaginated()}. * * @param \Illuminate\Contracts\Pagination\Paginator|mixed $paginated * @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $resourceClass * @param array $meta * @param array|null $options * @return array{data: mixed, meta: array} */ public static function asPaginated( mixed $paginated, string $resourceClass, array $meta = [], ?array $options = null, ): array { $resolvedOptions = $options ?? (is_array(request('options')) ? request('options') : []); $payload = self::response( $resourceClass::collection($paginated), self::paginationMeta($paginated, $resolvedOptions), ); if ($meta) { $payload['meta'] = array_merge($payload['meta'], $meta); } return $payload; } }