[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 14: Bộ lọc Sản phẩm Nâng cao (Category, Status & Price Range)
Chào các bạn, mình đã quay trở lại!
Ở bài viết trước, chúng ta đã xử lý tìm kiếm theo tên và slug. Nhưng hãy tưởng tượng khách hàng vào website của bạn và nói: "Tôi muốn tìm điện thoại Samsung, đang còn hàng (Active), và giá chỉ từ 5 đến 10 triệu". Nếu API của bạn không hỗ trợ lọc linh hoạt, bạn sẽ đánh mất khách hàng ngay lập tức.
Trong bài học này, chúng ta sẽ nâng cấp hàm getAll() để xử lý các điều kiện lọc động một cách chuyên nghiệp và tối ưu nhất.
1. Tầng Model: Xử lý SQL Động với Khoảng giá (Product.php)
Chúng ta tiếp tục tận dụng kỹ thuật "WHERE 1" và mảng $params để tránh SQL Injection. Điểm mới ở đây là việc xử lý các toán tử so sánh >= và <= cho khoảng giá.
File: app/Models/Product.php (Bản nâng cấp toàn diện)
<?php
namespace App\Models;
use App\Core\Database;
class Product
{
protected $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function getAll($filters = []) {
$sql = "SELECT p.*, c.name AS category_name
FROM Products p
LEFT JOIN Categories c ON p.category_id = c.id
WHERE 1";
$params = [];
// 1. Tìm kiếm theo tên hoặc slug (Từ Phần 13)
if (!empty($filters['search'])) {
$sql .= " AND (p.name LIKE ? OR p.slug LIKE ?)";
$searchTerm = '%' . $filters['search'] . '%';
$params[] = $searchTerm;
$params[] = $searchTerm;
}
// 2. Lọc theo Danh mục (ID)
if (!empty($filters['category_id'])) {
$sql .= " AND p.category_id = ?";
$params[] = $filters['category_id'];
}
// 3. Lọc theo Trạng thái (active, draft, hidden)
if (!empty($filters['status'])) {
$sql .= " AND p.status = ?";
$params[] = $filters['status'];
}
// 4. Lọc theo Khoảng giá (Price Range)
if (!empty($filters['price_min'])) {
$sql .= " AND p.price >= ?";
$params[] = $filters['price_min'];
}
if (!empty($filters['price_max'])) {
$sql .= " AND p.price <= ?";
$params[] = $filters['price_max'];
}
// 5. Phân trang
$limit = isset($filters['limit']) ? (int)$filters['limit'] : 10;
$page = isset($filters['page']) ? (int)$filters['page'] : 1;
$offset = ($page - 1) * $limit;
$sql .= " ORDER BY p.created_at DESC LIMIT $limit OFFSET $offset";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
2. Tầng Controller: Tiếp nhận Filter từ Query String
Tại ProductController, chúng ta chỉ cần cập nhật danh sách các tham số lấy từ $_GET. Việc để giá trị mặc định cho limit và page giúp API luôn hoạt động ổn định ngay cả khi client không gửi gì lên.
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 tiêu chí lọc từ URL
$filters = [
'search' => $_GET['search'] ?? null,
'category_id' => $_GET['category_id'] ?? null,
'status' => $_GET['status'] ?? null,
'price_min' => $_GET['price_min'] ?? null,
'price_max' => $_GET['price_max'] ?? null,
'limit' => $_GET['limit'] ?? 10,
'page' => $_GET['page'] ?? 1,
];
$productModel = new Product();
$products = $productModel->getAll($filters);
Response::json([
'status' => 'success',
'results' => count($products),
'filters' => $filters, // Trả về filter để frontend dễ debug
'data' => $products
]);
}
}
3. Kiểm thử các kịch bản thực tế (Test with Curl)
Hãy thử sức mạnh của bộ lọc mới thông qua Terminal của bạn:
Lọc sản phẩm có giá từ 5 triệu đến 10 triệu:
curl "http://localhost:8000/api/products?price_min=5000000&price_max=10000000"
Lọc sản phẩm đang hoạt động (active) trong danh mục ID 2:
curl "http://localhost:8000/api/products?status=active&category_id=2"
Kết hợp "tất tay": Tìm iPhone, giá dưới 30 triệu, đang bán, trang 1:
curl "http://localhost:8000/api/products?search=iphone&price_max=30000000&status=active&page=1&limit=10"
Gợi ý nâng cao cho Blogger thực chiến
Để bài viết trên Viblo của bạn thêm phần "Pro", hãy nhắc nhở độc giả về các lưu ý sau:
Sắp xếp (Sorting): Bạn có thể thêm tham số ?sort=price_asc hoặc ?sort=price_desc. Trong Model, hãy nối thêm ORDER BY p.price ASC/DESC tương ứng.
Mặc định Trạng thái: Trong các API dành cho khách hàng (End-user), bạn nên mặc định status = 'active' nếu người dùng không truyền filter status, để tránh lộ các sản phẩm đang nháp (Draft) hoặc đã xóa.
Index Database: Đừng quên nhắc người dùng đánh Index cho các cột price, status, và category_id để tốc độ lọc không bị "rùa bò" khi dữ liệu lớn dần.
Tạm kết
Vậy là chúng ta đã xây dựng xong một hệ thống lọc sản phẩm cực kỳ mạnh mẽ và đúng chuẩn RESTful. Khách hàng giờ đây có thể dễ dàng tìm thấy món đồ ưng ý giữa hàng nghìn sản phẩm trên website của bạn.
Hãy để lại bình luận phía dưới nhé! Đừng quên Upvote để tiếp thêm động lực cho "bố đời" ra bài đều đặn. Chúc bạn code vui vẻ!
All rights reserved