Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.15% covered (danger)
46.15%
12 / 26
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiController
46.15% covered (danger)
46.15%
12 / 26
16.67% covered (danger)
16.67%
1 / 6
50.13
0.00% covered (danger)
0.00%
0 / 1
 respond
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 respondCreated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 respondNoContent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 respondError
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 normalize
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
5.58
 paginationMeta
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
10.75
1<?php
2
3namespace App\Http\Controllers\Api;
4
5use App\Http\Controllers\Controller;
6use Illuminate\Contracts\Support\Arrayable;
7use Illuminate\Http\JsonResponse;
8use Illuminate\Http\Resources\Json\JsonResource;
9use Illuminate\Http\Resources\Json\ResourceCollection;
10use Illuminate\Pagination\AbstractPaginator;
11
12/**
13 * Base controller for every /api/v1 endpoint.
14 *
15 * Response envelope (consistent across the whole API so the future Next.js
16 * frontend has one shape to code against):
17 *
18 *   success →  { "data": ..., "meta": {...}? }
19 *   error   →  { "message": "...", "errors": {...}? }
20 */
21abstract class ApiController extends Controller
22{
23    protected function respond(mixed $data, array $meta = [], int $status = 200): JsonResponse
24    {
25        $payload = ['data' => $this->normalize($data)];
26
27        $meta = array_merge($this->paginationMeta($data), $meta);
28        if ($meta !== []) {
29            $payload['meta'] = $meta;
30        }
31
32        return response()->json($payload, $status);
33    }
34
35    protected function respondCreated(mixed $data, array $meta = []): JsonResponse
36    {
37        return $this->respond($data, $meta, 201);
38    }
39
40    protected function respondNoContent(): JsonResponse
41    {
42        return response()->json(null, 204);
43    }
44
45    protected function respondError(string $message, int $status = 400, array $errors = []): JsonResponse
46    {
47        $payload = ['message' => $message];
48        if ($errors !== []) {
49            $payload['errors'] = $errors;
50        }
51
52        return response()->json($payload, $status);
53    }
54
55    private function normalize(mixed $data): mixed
56    {
57        if ($data instanceof JsonResource || $data instanceof ResourceCollection) {
58            return $data;
59        }
60
61        if ($data instanceof AbstractPaginator) {
62            return $data->items();
63        }
64
65        if ($data instanceof Arrayable) {
66            return $data->toArray();
67        }
68
69        return $data;
70    }
71
72    private function paginationMeta(mixed $data): array
73    {
74        if (! $data instanceof AbstractPaginator) {
75            return [];
76        }
77
78        return [
79            'current_page' => $data->currentPage(),
80            'per_page' => $data->perPage(),
81            'total' => method_exists($data, 'total') ? $data->total() : null,
82            'last_page' => method_exists($data, 'lastPage') ? $data->lastPage() : null,
83        ];
84    }
85}