categoryModel = new Category(); } /** * Get all categories with optional filtering and pagination * GET /categories */ public function index() { try { $params = $this->getRequestParams(); // Build query $query = []; $values = []; // Filter by parent (top-level categories) if (isset($params['parent_id'])) { if ($params['parent_id'] === 'null' || $params['parent_id'] === '0') { $query[] = 'parent_id IS NULL'; } else { $query[] = 'parent_id = ?'; $values[] = $params['parent_id']; } } // Filter by active status if (isset($params['active'])) { $query[] = 'is_active = ?'; $values[] = $params['active'] ? 1 : 0; } // Search by name if (isset($params['search'])) { $query[] = 'name LIKE ?'; $values[] = '%'.$params['search'].'%'; } // Build WHERE clause $whereClause = empty($query) ? '' : 'WHERE '.implode(' AND ', $query); // Order by $orderBy = $params['sort'] ?? 'sort_order, name'; $direction = strtoupper($params['direction'] ?? 'ASC'); if (!in_array($direction, ['ASC', 'DESC'])) { $direction = 'ASC'; } // Pagination $page = max(1, intval($params['page'] ?? 1)); $perPage = min(100, max(1, intval($params['per_page'] ?? 20))); $offset = ($page - 1) * $perPage; // Get total count $countSql = "SELECT COUNT(*) as total FROM categories $whereClause"; $totalResult = $this->categoryModel->query($countSql, $values); $total = $totalResult[0]['total'] ?? 0; // Get categories $sql = "SELECT * FROM categories $whereClause ORDER BY $orderBy $direction LIMIT $perPage OFFSET $offset"; $categories = $this->categoryModel->query($sql, $values); // If hierarchical is requested, build tree structure if (isset($params['hierarchical']) && $params['hierarchical']) { $categories = $this->buildCategoryTree($categories); } $this->jsonResponse([ 'success' => true, 'data' => $categories, 'pagination' => [ 'page' => $page, 'per_page' => $perPage, 'total' => $total, 'pages' => ceil($total / $perPage) ] ]); } catch (\Exception $e) { $this->jsonResponse([ 'success' => false, 'message' => 'Failed to fetch categories', 'error' => $e->getMessage() ], 500); } } /** * Get a single category by ID * GET /categories/{id} */ public function show($id) { try { $category = $this->categoryModel->find($id); if (!$category) { $this->jsonResponse([ 'success' => false, 'message' => 'Category not found' ], 404); return; } // Get children if requested $params = $this->getRequestParams(); if (isset($params['include_children']) && $params['include_children']) { $children = $this->categoryModel->where('parent_id', $id)->get(); $category['children'] = $children; } $this->jsonResponse([ 'success' => true, 'data' => $category ]); } catch (\Exception $e) { $this->jsonResponse([ 'success' => false, 'message' => 'Failed to fetch category', 'error' => $e->getMessage() ], 500); } } /** * Create a new category * POST /categories */ public function store() { try { $data = $this->getRequestBody(); // Validate required fields $required = ['name']; foreach ($required as $field) { if (empty($data[$field])) { $this->jsonResponse([ 'success' => false, 'message' => "Field '$field' is required" ], 400); return; } } // Generate slug if not provided if (empty($data['slug'])) { $data['slug'] = $this->generateSlug($data['name']); } // Validate parent category exists if (!empty($data['parent_id'])) { $parent = $this->categoryModel->find($data['parent_id']); if (!$parent) { $this->jsonResponse([ 'success' => false, 'message' => 'Parent category not found' ], 400); return; } } // Set defaults $data['is_active'] = $data['is_active'] ?? 1; $data['sort_order'] = $data['sort_order'] ?? 0; $data['created_at'] = date('Y-m-d H:i:s'); $data['updated_at'] = date('Y-m-d H:i:s'); $categoryId = $this->categoryModel->create($data); $category = $this->categoryModel->find($categoryId); $this->jsonResponse([ 'success' => true, 'message' => 'Category created successfully', 'data' => $category ], 201); } catch (\Exception $e) { $this->jsonResponse([ 'success' => false, 'message' => 'Failed to create category', 'error' => $e->getMessage() ], 500); } } /** * Update a category * POST /categories/{id} with _method=PUT */ public function update($id) { try { $data = $this->getRequestBody(); // Check if this is actually an update request if (isset($data['_method']) && strtoupper($data['_method']) !== 'PUT') { $this->jsonResponse([ 'success' => false, 'message' => 'Invalid method for update operation' ], 400); return; } // Remove _method from data unset($data['_method']); $category = $this->categoryModel->find($id); if (!$category) { $this->jsonResponse([ 'success' => false, 'message' => 'Category not found' ], 404); return; } $data = $this->getRequestBody(); // Generate slug if name changed if (!empty($data['name']) && empty($data['slug'])) { $data['slug'] = $this->generateSlug($data['name']); } // Validate parent category exists and prevent circular reference if (isset($data['parent_id'])) { if ($data['parent_id'] == $id) { $this->jsonResponse([ 'success' => false, 'message' => 'Category cannot be its own parent' ], 400); return; } if (!empty($data['parent_id'])) { $parent = $this->categoryModel->find($data['parent_id']); if (!$parent) { $this->jsonResponse([ 'success' => false, 'message' => 'Parent category not found' ], 400); return; } } } $data['updated_at'] = date('Y-m-d H:i:s'); $this->categoryModel->updateById($id, $data); $updatedCategory = $this->categoryModel->find($id); $this->jsonResponse([ 'success' => true, 'message' => 'Category updated successfully', 'data' => $updatedCategory ]); } catch (\Exception $e) { $this->jsonResponse([ 'success' => false, 'message' => 'Failed to update category', 'error' => $e->getMessage() ], 500); } } /** * Delete a category * POST /categories/{id}/delete with _method=DELETE */ public function destroy($id) { try { $data = $this->getRequestBody(); // Check if this is actually a delete request (optional since route is explicit) if (isset($data['_method']) && strtoupper($data['_method']) !== 'DELETE') { $this->jsonResponse([ 'success' => false, 'message' => 'Invalid method for delete operation' ], 400); return; } $category = $this->categoryModel->find($id); if (!$category) { $this->jsonResponse([ 'success' => false, 'message' => 'Category not found' ], 404); return; } // Check if category has children $children = $this->categoryModel->where('parent_id', $id)->get(); if (!empty($children)) { $this->jsonResponse([ 'success' => false, 'message' => 'Cannot delete category that has child categories' ], 400); return; } // Check if category has products (you may want to implement this) // $products = $this->categoryModel->getProductsInCategory($id); // if (!empty($products)) { // $this->jsonResponse([ // 'success' => false, // 'message' => 'Cannot delete category that contains products' // ], 400); // return; // } $this->categoryModel->deleteById($id); $this->jsonResponse([ 'success' => true, 'message' => 'Category deleted successfully' ]); } catch (\Exception $e) { $this->jsonResponse([ 'success' => false, 'message' => 'Failed to delete category', 'error' => $e->getMessage() ], 500); } } /** * Get category tree (hierarchical structure) * GET /categories/tree */ public function tree() { try { $params = $this->getRequestParams(); // Get all active categories or all categories $whereClause = ''; $values = []; if (!isset($params['include_inactive']) || !$params['include_inactive']) { $whereClause = 'WHERE is_active = 1'; } $sql = "SELECT * FROM categories $whereClause ORDER BY sort_order, name"; $categories = $this->categoryModel->query($sql, $values); // Build tree structure $tree = $this->buildCategoryTree($categories); $this->jsonResponse([ 'success' => true, 'data' => $tree ]); } catch (\Exception $e) { $this->jsonResponse([ 'success' => false, 'message' => 'Failed to fetch category tree', 'error' => $e->getMessage() ], 500); } } /** * Build hierarchical category tree */ private function buildCategoryTree($categories, $parentId = null) { $tree = []; foreach ($categories as $category) { if ($category['parent_id'] == $parentId) { $children = $this->buildCategoryTree($categories, $category['id']); if (!empty($children)) { $category['children'] = $children; } $tree[] = $category; } } return $tree; } /** * Generate URL-friendly slug from name */ private function generateSlug($name) { // Convert to lowercase and replace spaces with hyphens $slug = strtolower(trim($name)); // Handle Thai characters $slug = preg_replace('/[^\p{L}\p{N}\s\-_]/u', '', $slug); $slug = preg_replace('/[\s\-_]+/', '-', $slug); $slug = trim($slug, '-'); // If slug is empty or only contains non-ASCII, use timestamp if (empty($slug) || !preg_match('/[a-z0-9]/', $slug)) { $slug = 'category-'.time(); } // Ensure uniqueness $originalSlug = $slug; $counter = 1; while ($this->categoryModel->where('slug', $slug)->first()) { $slug = $originalSlug.'-'.$counter; $counter++; } return $slug; } }