Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
CategoryController
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 7
702
0.00% covered (danger)
0.00%
0 / 1
 index
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 store
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 show
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 update
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 destroy
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 restore
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 forceDelete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace App\Http\Controllers\Api\v1;
4
5use App\Http\Controllers\Controller;
6use App\Http\Requests\StoreCategoryRequest;
7use App\Http\Requests\UpdateCategoryRequest;
8use App\Models\Category;
9use App\Responses\ApiResponse;
10use Illuminate\Http\Request;
11use Illuminate\Http\JsonResponse;
12use Illuminate\Support\Facades\Gate;
13
14/**
15 * CategoryController
16 * 
17 * Handles all category-related API operations including CRUD operations, search,
18 * and soft delete management. Implements role-based permissions where categories
19 * are global resources accessible to all authenticated users, but only staff+
20 * can create, update, and delete categories.
21 * 
22 * @package App\Http\Controllers\Api\v1
23 * @author TAFE Assessment
24 * @version 1.0.0
25 * @since 2025-10-04
26 * 
27 * @method JsonResponse index(Request $request) Display a listing of categories with search
28 * @method JsonResponse store(StoreCategoryRequest $request) Store a newly created category
29 * @method JsonResponse show(string $id) Display the specified category with 5 random jokes
30 * @method JsonResponse update(UpdateCategoryRequest $request, string $id) Update the specified category
31 * @method JsonResponse destroy(string $id) Remove the specified category (soft delete)
32 * @method JsonResponse restore(string $id) Restore a soft deleted category
33 * @method JsonResponse forceDelete(string $id) Permanently delete a category
34 * 
35 * @see \App\Policies\CategoryPolicy For authorization logic
36 * @see \App\Http\Requests\StoreCategoryRequest For creation validation
37 * @see \App\Http\Requests\UpdateCategoryRequest For update validation
38 * @see \App\Responses\ApiResponse For standardized JSON responses
39 */
40class CategoryController extends Controller
41{
42    /**
43     * Display a listing of categories with optional search functionality.
44     * 
45     * Retrieves all available categories with optional search filtering by name.
46     * Categories are global resources accessible to all authenticated users.
47     * Returns different response codes based on whether it's a search query or
48     * general browse operation.
49     * 
50     * @param Request $request The HTTP request containing optional query parameters:
51     *                        - q (string): Search term for category name
52     * 
53     * @return JsonResponse Standardized JSON response containing:
54     *                     - success: boolean indicating operation success
55     *                     - message: Human-readable status message
56     *                     - data: Object containing categories array
57     * 
58     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user lacks view permissions
59     * 
60     * @api GET /api/v1/categories
61     * @permission categories.browse (User level 100+)
62     * @permission categories.search (User level 100+) - for search functionality
63     */
64    public function index(Request $request): JsonResponse
65    {
66        // Check if user can view categories (User level 100+)
67        if (!Gate::allows('viewAny', Category::class)) {
68            return ApiResponse::error(null, 'Unauthorized to view categories', 403);
69        }
70
71        $query = Category::query();
72
73        // Check if user can search and apply search filter
74        if ($search = $request->string('q')->toString()) {
75            if (!Gate::allows('search', Category::class)) {
76                return ApiResponse::error(null, 'Unauthorized to search categories', 403);
77            }
78            $query->where('name', 'like', "%{$search}%");
79        }
80
81        $categories = $query->orderBy('name')->get();
82
83        // For search queries, return empty results with 200 status
84        if ($search && count($categories) === 0) {
85            return ApiResponse::success(['categories' => $categories], 'No categories found matching search criteria');
86        }
87
88        // For general browse, return 404 if no categories exist at all
89        if (!$search && count($categories) === 0) {
90            return ApiResponse::error($categories, "No categories found", 404);
91        }
92
93        return ApiResponse::success(['categories' => $categories], 'Categories retrieved');
94    }
95
96    /**
97     * Store a newly created category in storage.
98     * 
99     * Creates a new category with the provided data and assigns it to the authenticated user.
100     * Categories are global resources but ownership is tracked for administrative purposes.
101     * Only staff+ level users can create categories.
102     * 
103     * @param StoreCategoryRequest $request Validated request containing:
104     *                                      - name (required): Category name (max 255 chars, unique)
105     *                                      - description (optional): Category description (max 1000 chars)
106     * 
107     * @return JsonResponse Standardized JSON response containing:
108     *                     - success: boolean indicating operation success
109     *                     - message: Human-readable status message
110     *                     - data: Object containing the created category
111     * 
112     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user lacks create permissions
113     * @throws \Illuminate\Validation\ValidationException When validation fails
114     * 
115     * @api POST /api/v1/categories
116     * @permission categories.create (Staff level 500+)
117     */
118    public function store(StoreCategoryRequest $request): JsonResponse
119    {
120        // Check if user can create categories (Staff level 500+)
121        if (!Gate::allows('create', Category::class)) {
122            return ApiResponse::error(null, 'Unauthorized to create categories', 403);
123        }
124
125        // Add the authenticated user as the owner
126        $data = $request->validated();
127        $data['user_id'] = $request->user()->id;
128
129        $category = Category::create($data);
130
131        return ApiResponse::success(['category' => $category], 'Category created', 201);
132    }
133
134    /**
135     * Display the specified category with 5 random jokes.
136     * 
137     * Retrieves a single category by ID along with 5 random jokes from that category.
138     * Categories are global resources accessible to all authenticated users.
139     * The random jokes are included to provide sample content for the category.
140     * 
141     * @param string $id The category ID to retrieve
142     * 
143     * @return JsonResponse Standardized JSON response containing:
144     *                     - success: boolean indicating operation success
145     *                     - message: Human-readable status message
146     *                     - data: Object containing the category with 5 random jokes
147     * 
148     * @throws \Illuminate\Http\Exceptions\HttpResponseException When category not found or user lacks view permissions
149     * 
150     * @api GET /api/v1/categories/{id}
151     * @permission categories.read (User level 100+)
152     */
153    public function show(string $id): JsonResponse
154    {
155        $category = Category::query()->find($id);
156
157        if (!$category) {
158            return ApiResponse::error(null, "Category not found", 404);
159        }
160
161        // Check if user can view this category (User level 100+)
162        if (!Gate::allows('view', $category)) {
163            return ApiResponse::error(null, 'Unauthorized to view this category', 403);
164        }
165
166        // Load 5 random jokes for this category
167        $category->load(['jokes' => function ($q) {
168            $q->inRandomOrder()->limit(5);
169        }]);
170
171        return ApiResponse::success(['category' => $category], 'Category retrieved');
172    }
173
174    /**
175     * Update the specified category in storage.
176     * 
177     * Updates an existing category with the provided data. Only staff+ level users
178     * can update categories. Supports partial updates and maintains data integrity
179     * with unique name validation.
180     * 
181     * @param UpdateCategoryRequest $request Validated request containing:
182     *                                       - name (optional): Category name (max 255 chars, unique)
183     *                                       - description (optional): Category description (max 1000 chars)
184     * @param string $id The category ID to update
185     * 
186     * @return JsonResponse Standardized JSON response containing:
187     *                     - success: boolean indicating operation success
188     *                     - message: Human-readable status message
189     *                     - data: Object containing the updated category
190     * 
191     * @throws \Illuminate\Http\Exceptions\HttpResponseException When category not found or user lacks update permissions
192     * @throws \Illuminate\Validation\ValidationException When validation fails
193     * 
194     * @api PUT /api/v1/categories/{id}
195     * @permission categories.update (Staff level 500+)
196     */
197    public function update(UpdateCategoryRequest $request, string $id): JsonResponse
198    {
199        $category = Category::find($id);
200
201        if (!$category) {
202            return ApiResponse::error(null, "Category not found", 404);
203        }
204
205        // Check if user can update this category (Staff level 500+)
206        if (!Gate::allows('update', $category)) {
207            return ApiResponse::error(null, 'Unauthorized to update this category', 403);
208        }
209
210        $category->update($request->validated());
211
212        return ApiResponse::success(['category' => $category], 'Category updated');
213    }
214
215    /**
216     * Remove the specified category from storage (soft delete).
217     * 
218     * Performs a soft delete on the specified category, making it unavailable to users
219     * but preserving the data for potential restoration. Staff+ can delete any category,
220     * implementing a hybrid permission system where staff have global deletion rights.
221     * 
222     * @param string $id The category ID to delete
223     * 
224     * @return JsonResponse Standardized JSON response containing:
225     *                     - success: boolean indicating operation success
226     *                     - message: Human-readable status message
227     *                     - data: null (category is soft deleted)
228     * 
229     * @throws \Illuminate\Http\Exceptions\HttpResponseException When category not found or user lacks delete permissions
230     * 
231     * @api DELETE /api/v1/categories/{id}
232     * @permission categories.delete (Staff level 500+)
233     */
234    public function destroy(string $id): JsonResponse
235    {
236        $category = Category::find($id);
237
238        if (!$category) {
239            return ApiResponse::error(null, "Category not found", 404);
240        }
241
242        // Check if user can delete this category (Staff level 500+)
243        if (!Gate::allows('delete', $category)) {
244            return ApiResponse::error(null, 'Unauthorized to delete this category', 403);
245        }
246
247        $category->delete();
248
249        return ApiResponse::success(null, 'Category deleted');
250    }
251
252    /**
253     * Restore a soft deleted category.
254     * 
255     * Restores a previously soft deleted category, making it available again to users.
256     * This operation is restricted to staff+ level users and can only be performed
257     * on categories that are currently soft deleted.
258     * 
259     * @param string $id The category ID to restore
260     * 
261     * @return JsonResponse Standardized JSON response containing:
262     *                     - success: boolean indicating operation success
263     *                     - message: Human-readable status message
264     *                     - data: Object containing the restored category
265     * 
266     * @throws \Illuminate\Http\Exceptions\HttpResponseException When category not found, not deleted, or user lacks restore permissions
267     * 
268     * @api POST /api/v1/categories/{id}/restore
269     * @permission categories.restore (Staff level 500+)
270     */
271    public function restore(string $id): JsonResponse
272    {
273        $category = Category::withTrashed()->find($id);
274
275        if (!$category) {
276            return ApiResponse::error(null, "Category not found", 404);
277        }
278
279        // Check if user can restore this category (Staff level 500+)
280        if (!Gate::allows('restore', $category)) {
281            return ApiResponse::error(null, 'Unauthorized to restore this category', 403);
282        }
283
284        if (!$category->trashed()) {
285            return ApiResponse::error(null, "Category is not deleted", 400);
286        }
287
288        $category->restore();
289
290        return ApiResponse::success(['category' => $category], 'Category restored');
291    }
292
293    /**
294     * Permanently delete a category.
295     * 
296     * Permanently removes a category from the database, including soft deleted categories.
297     * This operation is irreversible and is restricted to admin+ level users.
298     * Use with extreme caution as this action cannot be undone and may affect
299     * jokes that reference this category.
300     * 
301     * @param string $id The category ID to permanently delete
302     * 
303     * @return JsonResponse Standardized JSON response containing:
304     *                     - success: boolean indicating operation success
305     *                     - message: Human-readable status message
306     *                     - data: null (category is permanently removed)
307     * 
308     * @throws \Illuminate\Http\Exceptions\HttpResponseException When category not found or user lacks force delete permissions
309     * 
310     * @api DELETE /api/v1/categories/{id}/force
311     * @permission categories.force-delete (Admin level 750+)
312     * @warning This action is irreversible and may affect related jokes
313     */
314    public function forceDelete(string $id): JsonResponse
315    {
316        $category = Category::withTrashed()->find($id);
317
318        if (!$category) {
319            return ApiResponse::error(null, "Category not found", 404);
320        }
321
322        // Check if user can permanently delete this category (Admin level 750+)
323        if (!Gate::allows('forceDelete', $category)) {
324            return ApiResponse::error(null, 'Unauthorized to permanently delete this category', 403);
325        }
326
327        $category->forceDelete();
328
329        return ApiResponse::success(null, 'Category permanently removed');
330    }
331}