0

[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 12: Danh sách Sản phẩm, Tìm kiếm và Phân trang

Chào các bạn, mình đã quay trở lại!

Khi làm việc tại các hệ thống lớn, danh sách sản phẩm không bao giờ chỉ đơn giản là SELECT * FROM products. Với hàng nghìn sản phẩm, việc trả về toàn bộ dữ liệu trong một request là "tự sát" về mặt hiệu năng.

Trong bài viết này, chúng ta sẽ cùng nhau giải quyết bài toán: Lọc (Filter) + Tìm kiếm (Search) + Phân trang (Pagination) ngay trong tầng Model của kiến trúc MVC.

1. Cấu trúc bảng dữ liệu (Database Schema)

Chúng ta giả định bảng Products của bạn đã có đầy đủ các trường "ăn tiền" như: id, name, price, stock, category_id, và status. Đặc biệt là cột slug để hỗ trợ SEO sau này.

2. Tầng Model: Xử lý SQL năng động (Product.php)

Thay vì viết nhiều hàm khác nhau, chúng ta sẽ xây dựng một hàm getAll() duy nhất có khả năng "biến hình" câu lệnh SQL dựa trên các tham số truyền vào.

File: app/Models/Product.php

<?php
namespace App\Models;

use App\Core\Database;

class Product
{
    protected $db;

    public function __construct() {
        $this->db = Database::getInstance();
    }

    /**
     * Lấy danh sách sản phẩm với bộ lọc động
     */
    public function getAll($filters = []) {
        // Sử dụng LEFT JOIN để lấy luôn tên danh mục
        $sql = "SELECT p.*, c.name AS category_name
                FROM Products p
                LEFT JOIN Categories c ON p.category_id = c.id
                WHERE 1"; // Mẹo "WHERE 1" giúp việc nối chuỗi AND phía sau dễ dàng hơn

        $params = [];

        // 1. Tìm kiếm theo tên (Search)
        if (!empty($filters['search'])) {
            $sql .= " AND p.name LIKE ?";
            $params[] = "%" . $filters['search'] . "%";
        }

        // 2. Lọc theo danh mục (Filter)
        if (!empty($filters['category_id'])) {
            $sql .= " AND p.category_id = ?";
            $params[] = $filters['category_id'];
        }

        // 3. Phân trang (Pagination)
        $limit = isset($filters['limit']) ? (int)$filters['limit'] : 10;
        $page = isset($filters['page']) ? (int)$filters['page'] : 1;
        $offset = ($page - 1) * $limit;

        // Sắp xếp sản phẩm mới nhất lên đầu
        $sql .= " ORDER BY p.created_at DESC LIMIT $limit OFFSET $offset";

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);

        return $stmt->fetchAll();
    }
}

Lưu ý bảo mật: Đối với LIMITOFFSET, mình ép kiểu sang int hoặc đưa trực tiếp vào chuỗi nếu đã chắc chắn là số sạch. Đối với các giá trị từ người dùng như search, chúng ta bắt buộc dùng prepareexecute để chống SQL Injection.

3. Tầng Controller: Tiếp nhận Query Parameters

Controller sẽ chịu trách nhiệm bóc tách các tham số từ URL (ví dụ: ?search=iphone&page=2) và gửi chúng xuống Model.

File: app/Controllers/ProductController.php

<?php
namespace App\Controllers;

use App\Models\Product;
use App\Core\Response;

class ProductController
{
    public function index() {
        // Thu thập các filter từ $_GET
        $filters = [
            'search'      => $_GET['search'] ?? null,
            'category_id' => $_GET['category_id'] ?? null,
            'limit'       => $_GET['limit'] ?? 10,
            'page'        => $_GET['page'] ?? 1,
        ];

        $productModel = new Product();
        $products = $productModel->getAll($filters);

        // Trả về JSON cho Frontend
        Response::json([
            'status' => 'success',
            'count'  => count($products),
            'data'   => $products
        ]);
    }
}

4. Định tuyến Route (index.php)

Đăng ký Endpoint mới vào "bản đồ" của chúng ta.

File: public/index.php

use App\Controllers\ProductController;

$productController = new ProductController();

// Route lấy danh sách sản phẩm: GET /api/products
if ($uri === '/api/products' && $method === 'GET') {
    $productController->index();
}

5. Kiểm thử thành quả (Test with Curl)

Hãy thử sức mạnh của API mới bằng các lệnh sau:

1. Lấy trang đầu tiên, mỗi trang 5 sản phẩm:

curl "http://localhost:8000/api/products?page=1&limit=5"

2. Tìm kiếm sản phẩm tên "iPhone" trong danh mục ID số 3:

curl "http://localhost:8000/api/products?search=iphone&category_id=3"

Gợi ý nâng cao cho dân chuyên

Để API của bạn đạt chuẩn "Enterprise", hãy cân nhắc các yếu tố sau:

Meta Data: Đừng chỉ trả về danh sách sản phẩm. Hãy trả về cả total_pages, current_page, và total_items để Frontend dễ dàng vẽ thanh phân trang.

Price Range: Thêm filter min_pricemax_price vào Model.

Sorting: Cho phép người dùng sắp xếp theo giá tăng/giảm dần (?sort=price_asc).

Eager Loading: Nếu sản phẩm có nhiều ảnh, hãy JOIN thêm bảng Product_Images để tránh lỗi N+1 query huyền thoại.

Tạm kết

Vậy là chúng ta đã hoàn thành API danh sách sản phẩm cực kỳ linh hoạt. Việc nắm vững cách build SQL động giúp bạn xử lý được mọi yêu cầu lọc dữ liệu phức tạp từ khách hàng.

Hãy để lại bình luận phía dưới nhé! Đừng quên Upvote để ủng hộ mình ra bài đều đặn. Chúc bạn code vui vẻ, "bố đời"!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí