diff --git a/src/Services/MiscService.php b/src/Services/MiscService.php index 0f568cb..0f2fdda 100644 --- a/src/Services/MiscService.php +++ b/src/Services/MiscService.php @@ -114,6 +114,143 @@ class MiscService 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 + */ + 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'];