R structure miscservice, A response service
This commit is contained in:
parent
a4ccd63fce
commit
817dd28b3a
|
|
@ -5,32 +5,131 @@ namespace Blax\Workkit\Services;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grab-bag of small utilities used across Blax host apps.
|
||||||
|
*
|
||||||
|
* Response-envelope helpers (`response`, `apiResponse`, `apiMeta`,
|
||||||
|
* `apiItem`, `apiCollection`, `apiPaginated`, `asPaginated`,
|
||||||
|
* `paginationMeta`, `availableLanguages`) have moved to
|
||||||
|
* {@see ResponseService} — they remain here as thin shims so existing
|
||||||
|
* callers continue to work without changes. Prefer calling
|
||||||
|
* {@see ResponseService} directly in new code.
|
||||||
|
*/
|
||||||
class MiscService
|
class MiscService
|
||||||
{
|
{
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────
|
||||||
|
* Response envelope (delegates to ResponseService)
|
||||||
|
* ────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a standard response payload envelope.
|
* Build a raw `{ data, meta }` envelope. See {@see ResponseService::response()}.
|
||||||
*/
|
*/
|
||||||
public static function response(
|
public static function response(mixed $data = null, array $meta = []): array
|
||||||
mixed $data = null,
|
{
|
||||||
array $meta = []
|
return ResponseService::response($data, $meta);
|
||||||
): array {
|
|
||||||
return [
|
|
||||||
'data' => $data,
|
|
||||||
'meta' => $meta,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available content languages for the running app.
|
||||||
|
* See {@see ResponseService::availableLanguages()}.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function availableLanguages(): array
|
||||||
|
{
|
||||||
|
return ResponseService::availableLanguages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard meta block (`url`, `locale`, `languages` + extras).
|
||||||
|
* See {@see ResponseService::apiMeta()}.
|
||||||
|
*/
|
||||||
|
public static function apiMeta(array $extra = []): array
|
||||||
|
{
|
||||||
|
return ResponseService::apiMeta($extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single-item envelope. See {@see ResponseService::apiItem()}.
|
||||||
|
*
|
||||||
|
* @param class-string<\Illuminate\Http\Resources\Json\JsonResource>|null $resource_class
|
||||||
|
*/
|
||||||
|
public static function apiItem(mixed $item, ?string $resource_class = null, array $extraMeta = []): array
|
||||||
|
{
|
||||||
|
return ResponseService::apiItem($item, $resource_class, $extraMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plain envelope with the standard meta block.
|
||||||
|
* See {@see ResponseService::apiResponse()}.
|
||||||
|
*/
|
||||||
|
public static function apiResponse(mixed $data = null, array $extraMeta = []): array
|
||||||
|
{
|
||||||
|
return ResponseService::apiResponse($data, $extraMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-paginated collection envelope.
|
||||||
|
* See {@see ResponseService::apiCollection()}.
|
||||||
|
*
|
||||||
|
* @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $resource_class
|
||||||
|
*/
|
||||||
|
public static function apiCollection(iterable $items, string $resource_class, array $extraMeta = []): array
|
||||||
|
{
|
||||||
|
return ResponseService::apiCollection($items, $resource_class, $extraMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated envelope. See {@see ResponseService::apiPaginated()}.
|
||||||
|
*
|
||||||
|
* @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $resource_class
|
||||||
|
*/
|
||||||
|
public static function apiPaginated(mixed $paginated, string $resource_class, array $extraMeta = []): array
|
||||||
|
{
|
||||||
|
return ResponseService::apiPaginated($paginated, $resource_class, $extraMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy paginated meta block.
|
||||||
|
* See {@see ResponseService::paginationMeta()}.
|
||||||
|
*/
|
||||||
|
public static function paginationMeta(mixed $paginated, array $options = [], array $meta = []): array
|
||||||
|
{
|
||||||
|
return ResponseService::paginationMeta($paginated, $options, $meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy paginated envelope.
|
||||||
|
* See {@see ResponseService::asPaginated()}.
|
||||||
|
*
|
||||||
|
* @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $resource_class
|
||||||
|
*/
|
||||||
|
public static function asPaginated(
|
||||||
|
mixed $paginated,
|
||||||
|
string $resource_class,
|
||||||
|
array $meta = [],
|
||||||
|
?array $options = null,
|
||||||
|
): array {
|
||||||
|
return ResponseService::asPaginated($paginated, $resource_class, $meta, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────
|
||||||
|
* Misc utilities
|
||||||
|
* ────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a controller payload to a normalized options array.
|
* Resolve a controller payload to a normalized options array.
|
||||||
*
|
*
|
||||||
* Supported payload shapes:
|
* Supported payload shapes:
|
||||||
* - ['options' => [...]]
|
* - `['options' => [...]]`
|
||||||
* - [...] (already flat)
|
* - `[...]` (already flat)
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @param array<string, mixed> $defaults
|
||||||
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public static function resolveOptions(
|
public static function resolveOptions(array $payload, array $defaults = []): array
|
||||||
array $payload,
|
{
|
||||||
array $defaults = []
|
|
||||||
): array {
|
|
||||||
$options = is_array($payload['options'] ?? null)
|
$options = is_array($payload['options'] ?? null)
|
||||||
? $payload['options']
|
? $payload['options']
|
||||||
: $payload;
|
: $payload;
|
||||||
|
|
@ -39,13 +138,12 @@ class MiscService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read an option value using camelCase or snake_case fallback.
|
* Read an option value using exact / snake_case / camelCase fallback.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $options
|
||||||
*/
|
*/
|
||||||
public static function option(
|
public static function option(array $options, string $key, mixed $default = null): mixed
|
||||||
array $options,
|
{
|
||||||
string $key,
|
|
||||||
mixed $default = null
|
|
||||||
): mixed {
|
|
||||||
if (array_key_exists($key, $options)) {
|
if (array_key_exists($key, $options)) {
|
||||||
return $options[$key];
|
return $options[$key];
|
||||||
}
|
}
|
||||||
|
|
@ -64,194 +162,9 @@ class MiscService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build pagination metadata in a consistent format.
|
* Format a byte count as a human-readable string (B, KB, MB, …).
|
||||||
*/
|
*/
|
||||||
public static function paginationMeta(
|
public static function bytesToHuman(int|float $bytes): string
|
||||||
$paginated,
|
|
||||||
array $options = [],
|
|
||||||
array $meta = []
|
|
||||||
): array {
|
|
||||||
$data = $paginated->toArray();
|
|
||||||
|
|
||||||
$base = [
|
|
||||||
'from' => @$data['from'],
|
|
||||||
'to' => @$data['to'],
|
|
||||||
'total' => @$data['total'],
|
|
||||||
'last_page' => @$data['last_page'],
|
|
||||||
'current_page' => @$data['current_page'],
|
|
||||||
'options' => (object) $options,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($meta) {
|
|
||||||
$base = array_merge($base, $meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $base;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function asPaginated(
|
|
||||||
$paginated,
|
|
||||||
$resource_class,
|
|
||||||
array $meta = [],
|
|
||||||
?array $options = null
|
|
||||||
) {
|
|
||||||
$resolvedOptions = $options;
|
|
||||||
if ($resolvedOptions === null) {
|
|
||||||
$resolvedOptions = is_array(request('options'))
|
|
||||||
? request('options')
|
|
||||||
: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$payload = [
|
|
||||||
'data' => $resource_class::collection($paginated),
|
|
||||||
'meta' => self::paginationMeta($paginated, $resolvedOptions),
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($meta) {
|
|
||||||
$payload['meta'] = array_merge($payload['meta'], $meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Available content languages for the running app.
|
|
||||||
*
|
|
||||||
* Tries (in order):
|
|
||||||
* 1. config('languages.languages') — Blax convention, list of {code, ...}
|
|
||||||
* 2. config('app.available_locales') — plain array of codes
|
|
||||||
* 3. fall back to [app()->getLocale()]
|
|
||||||
*
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
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 for an API response.
|
|
||||||
*
|
|
||||||
* Every api response in the workkit-shaped envelope carries this block
|
|
||||||
* so consumers always know:
|
|
||||||
* - which URL produced the payload (`url`)
|
|
||||||
* - which locale they got back (`locale`)
|
|
||||||
* - which other locales the same resource is available in (`languages`)
|
|
||||||
*
|
|
||||||
* Pagination keys (current_page, total, total_pages, etc.) are merged in
|
|
||||||
* by `apiPaginated()`; `apiItem()` / `apiCollection()` skip them.
|
|
||||||
*/
|
|
||||||
public static function apiMeta(array $extra = []): array
|
|
||||||
{
|
|
||||||
return array_merge([
|
|
||||||
'url' => optional(request())->fullUrl(),
|
|
||||||
'locale' => app()->getLocale(),
|
|
||||||
'languages' => self::availableLanguages(),
|
|
||||||
], $extra);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Paginated API envelope. Use for any list/index endpoint.
|
|
||||||
*
|
|
||||||
* Returns:
|
|
||||||
* {
|
|
||||||
* "data": [...resource collection...],
|
|
||||||
* "meta": {
|
|
||||||
* "url", "locale", "languages",
|
|
||||||
* "current_page", "per_page", "from", "to",
|
|
||||||
* "total", "total_pages", "has_more"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public static function apiPaginated(
|
|
||||||
$paginated,
|
|
||||||
string $resource_class,
|
|
||||||
array $extraMeta = []
|
|
||||||
): array {
|
|
||||||
$arr = method_exists($paginated, 'toArray') ? $paginated->toArray() : [];
|
|
||||||
|
|
||||||
$current = $arr['current_page'] ?? 1;
|
|
||||||
$last = $arr['last_page'] ?? null;
|
|
||||||
|
|
||||||
$pagination = [
|
|
||||||
'current_page' => $current,
|
|
||||||
'per_page' => $arr['per_page'] ?? null,
|
|
||||||
'from' => $arr['from'] ?? null,
|
|
||||||
'to' => $arr['to'] ?? null,
|
|
||||||
'total' => $arr['total'] ?? null,
|
|
||||||
'total_pages' => $last,
|
|
||||||
'has_more' => ($last !== null) ? ($current < $last) : false,
|
|
||||||
];
|
|
||||||
|
|
||||||
return [
|
|
||||||
'data' => $resource_class::collection($paginated),
|
|
||||||
'meta' => self::apiMeta(array_merge($pagination, $extraMeta)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single-item API envelope. Use for any show endpoint.
|
|
||||||
*/
|
|
||||||
public static function apiItem(
|
|
||||||
$item,
|
|
||||||
?string $resource_class = null,
|
|
||||||
array $extraMeta = []
|
|
||||||
): array {
|
|
||||||
return [
|
|
||||||
'data' => $resource_class ? $resource_class::make($item) : $item,
|
|
||||||
'meta' => self::apiMeta($extraMeta),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Non-paginated collection envelope. Use only when pagination is
|
|
||||||
* impractical (tiny fixed list like an enum or a child collection of a
|
|
||||||
* parent show response). Most list endpoints should use apiPaginated().
|
|
||||||
*/
|
|
||||||
public static function apiCollection(
|
|
||||||
$items,
|
|
||||||
string $resource_class,
|
|
||||||
array $extraMeta = []
|
|
||||||
): array {
|
|
||||||
$count = is_countable($items) ? count($items) : null;
|
|
||||||
|
|
||||||
return [
|
|
||||||
'data' => $resource_class::collection($items),
|
|
||||||
'meta' => self::apiMeta(array_merge([
|
|
||||||
'total' => $count,
|
|
||||||
], $extraMeta)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plain envelope (data + meta) for arbitrary payloads — login responses,
|
|
||||||
* action acknowledgements, etc. Always carries the standard apiMeta block.
|
|
||||||
*/
|
|
||||||
public static function apiResponse(
|
|
||||||
mixed $data = null,
|
|
||||||
array $extraMeta = []
|
|
||||||
): array {
|
|
||||||
return [
|
|
||||||
'data' => $data,
|
|
||||||
'meta' => self::apiMeta($extraMeta),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function bytesToHuman($bytes)
|
|
||||||
{
|
{
|
||||||
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
|
||||||
|
|
@ -262,39 +175,53 @@ class MiscService
|
||||||
return round($bytes, 2) . ' ' . $units[$i];
|
return round($bytes, 2) . ' ' . $units[$i];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function deterministicEncrypt($data)
|
/**
|
||||||
|
* AES-128-ECB encrypt — deterministic (same input → same output).
|
||||||
|
* Use only where determinism is required; prefer Laravel's `Crypt`
|
||||||
|
* facade for general-purpose encryption.
|
||||||
|
*/
|
||||||
|
public static function deterministicEncrypt(string $data): string
|
||||||
{
|
{
|
||||||
return base64_encode(openssl_encrypt($data, 'AES-128-ECB', config('app.key'), OPENSSL_RAW_DATA));
|
return base64_encode(openssl_encrypt($data, 'AES-128-ECB', config('app.key'), OPENSSL_RAW_DATA));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function deterministicDecrypt($encrypted)
|
/**
|
||||||
|
* Inverse of {@see deterministicEncrypt()}.
|
||||||
|
*/
|
||||||
|
public static function deterministicDecrypt(string $encrypted): string|false
|
||||||
{
|
{
|
||||||
return openssl_decrypt(base64_decode($encrypted), 'AES-128-ECB', config('app.key'), OPENSSL_RAW_DATA);
|
return openssl_decrypt(base64_decode($encrypted), 'AES-128-ECB', config('app.key'), OPENSSL_RAW_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function logExecutionTime(
|
/**
|
||||||
string $logtext,
|
* Time a callable and log its duration at debug level. Returns the
|
||||||
$callable = null
|
* callable's return value (or null when no callable is given).
|
||||||
) {
|
*/
|
||||||
|
public static function logExecutionTime(string $logtext, ?callable $callable = null): mixed
|
||||||
|
{
|
||||||
$start = microtime(true);
|
$start = microtime(true);
|
||||||
|
|
||||||
if (! $callable) {
|
if (! $callable) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $callable();
|
$result = $callable();
|
||||||
$end = microtime(true);
|
$end = microtime(true);
|
||||||
|
|
||||||
$executionTime = $end - $start;
|
|
||||||
|
|
||||||
Log::debug($logtext, [
|
Log::debug($logtext, [
|
||||||
'execution_time' => $executionTime
|
'execution_time' => $end - $start,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getIpInformation($ip)
|
/**
|
||||||
|
* Look up geolocation/ISP info for an IP via ipapi.co.
|
||||||
|
* Cached per-request via `once()` and per-IP via flexible cache.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public static function getIpInformation(string $ip): ?array
|
||||||
{
|
{
|
||||||
return once(function () use ($ip) {
|
return once(function () use ($ip) {
|
||||||
return cache()->flexible('ipapi-' . $ip, [60 * 60 * 24 * 2, 60 * 60 * 24 * 7], function () use ($ip) {
|
return cache()->flexible('ipapi-' . $ip, [60 * 60 * 24 * 2, 60 * 60 * 24 * 7], function () use ($ip) {
|
||||||
|
|
@ -309,7 +236,10 @@ class MiscService
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function countryToCode($country_long): ?string
|
/**
|
||||||
|
* Map a German/native country name to its ISO 3166-1 alpha-2 code.
|
||||||
|
*/
|
||||||
|
public static function countryToCode(string $country_long): ?string
|
||||||
{
|
{
|
||||||
return match (str()->lower($country_long)) {
|
return match (str()->lower($country_long)) {
|
||||||
'deutschland' => 'de',
|
'deutschland' => 'de',
|
||||||
|
|
@ -323,7 +253,11 @@ class MiscService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function codeToCountry($country_code, string|null $locale = null)
|
/**
|
||||||
|
* Map an ISO 3166-1 alpha-2 code to a localized country name.
|
||||||
|
* Supports `de`, `es`, `uk` and falls through to English.
|
||||||
|
*/
|
||||||
|
public static function codeToCountry(string $country_code, ?string $locale = null): ?string
|
||||||
{
|
{
|
||||||
$country_code = str()->lower($country_code);
|
$country_code = str()->lower($country_code);
|
||||||
$locale ??= app()->getLocale();
|
$locale ??= app()->getLocale();
|
||||||
|
|
@ -379,10 +313,12 @@ class MiscService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function parseIncompleteJson(
|
/**
|
||||||
string $json,
|
* Parse partial/streaming JSON best-effort. Delegates to
|
||||||
bool $associative = true
|
* {@see IncompleteJsonService}.
|
||||||
): array|object|null {
|
*/
|
||||||
|
public static function parseIncompleteJson(string $json, bool $associative = true): array|object|null
|
||||||
|
{
|
||||||
return (new IncompleteJsonService())->parse($json, $associative);
|
return (new IncompleteJsonService())->parse($json, $associative);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Workkit\Services;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Pagination\Paginator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API response envelope builder.
|
||||||
|
*
|
||||||
|
* Every method here produces the workkit response shape `{ data, meta }`.
|
||||||
|
* The richer helpers (`apiItem`, `apiCollection`, `apiPaginated`,
|
||||||
|
* `apiResponse`) auto-fill a standard meta block via {@see apiMeta()},
|
||||||
|
* which carries the request `url`, the active `locale`, and the list of
|
||||||
|
* available `languages`. Pagination metadata is merged on top by
|
||||||
|
* {@see apiPaginated()}.
|
||||||
|
*
|
||||||
|
* This service is the canonical home for these helpers. The matching
|
||||||
|
* methods on {@see MiscService} remain as thin shims for backward
|
||||||
|
* compatibility — existing callers do not need to change.
|
||||||
|
*
|
||||||
|
* Lifecycle in a controller:
|
||||||
|
*
|
||||||
|
* return response()->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<int, string>
|
||||||
|
*/
|
||||||
|
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<string, mixed> $extra
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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<string, mixed> $extraMeta
|
||||||
|
* @return array{data: mixed, meta: array<string, mixed>}
|
||||||
|
*/
|
||||||
|
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<string, mixed> $extraMeta
|
||||||
|
* @return array{data: mixed, meta: array<string, mixed>}
|
||||||
|
*/
|
||||||
|
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<string, mixed> $extraMeta
|
||||||
|
* @return array{data: mixed, meta: array<string, mixed>}
|
||||||
|
*/
|
||||||
|
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<string, mixed> $extraMeta
|
||||||
|
* @return array{data: mixed, meta: array<string, mixed>}
|
||||||
|
*/
|
||||||
|
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<string, mixed> $options
|
||||||
|
* @param array<string, mixed> $meta
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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<string, mixed> $meta
|
||||||
|
* @param array<string, mixed>|null $options
|
||||||
|
* @return array{data: mixed, meta: array<string, mixed>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue