<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\Category;
/**
* Category Controller
* Handles CRUD operations for product categories
*/
class CategoryController extends BaseController
{
protected Category $categoryModel;
public function __construct()
{
parent::__construct();
$this->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;
}
}