CategoryController.php

13.34 KB
04/08/2025 06:04
PHP
CategoryController.php
<?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;
    }
}