[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 19: Xây dựng hệ thống Giỏ hàng (Shopping Cart)
Chào các bạn, mình đã quay trở lại!
Giỏ hàng chính là "trạm trung chuyển" quan trọng nhất trước khi khách hàng tiến đến bước thanh toán. Khác với các website PHP truyền thống thường dùng $_SESSION để lưu giỏ hàng (vốn sẽ mất khi tắt trình duyệt hoặc khó đồng bộ trên Mobile), chúng ta sẽ sử dụng Database-driven Cart.
Trong bài này, chúng ta sẽ thực hiện trọn bộ logic: Thêm mới, cập nhật số lượng (Upsert), liệt kê và xóa sản phẩm khỏi giỏ hàng. Đặc biệt, chúng ta sẽ tận dụng lại hệ thống JWT Auth Middleware đã viết để đảm bảo giỏ hàng của ai thì chỉ người đó mới thấy.
1. Cấu trúc bảng dữ liệu (Cart_Items)
Chúng ta cần một bảng để lưu trữ mối quan hệ giữa Người dùng và Sản phẩm.
CREATE TABLE Cart_Items (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
product_id INT,
quantity INT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES Products(id) ON DELETE CASCADE
);
2. Tầng Model: Xử lý Logic "Upsert" (CartItem.php)
Một điểm tinh tế trong giỏ hàng là: Nếu người dùng thêm một sản phẩm đã có sẵn, chúng ta chỉ cần cộng dồn số lượng (Update), ngược lại mới thêm mới (Insert). Kỹ thuật này thường được gọi là Upsert.
File: app/Models/CartItem.php
<?php
namespace App\Models;
use App\Core\Database;
class CartItem {
protected $db;
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Lấy danh sách sản phẩm trong giỏ hàng của User
*/
public function getUserCart($userId) {
$stmt = $this->db->prepare("
SELECT ci.id, p.name, p.price, p.image, ci.quantity, (p.price * ci.quantity) as total
FROM Cart_Items ci
JOIN Products p ON ci.product_id = p.id
WHERE ci.user_id = ?
");
$stmt->execute([$userId]);
return $stmt->fetchAll();
}
/**
* Kiểm tra sản phẩm đã tồn tại trong giỏ chưa
*/
public function findItem($userId, $productId) {
$stmt = $this->db->prepare("SELECT * FROM Cart_Items WHERE user_id = ? AND product_id = ?");
$stmt->execute([$userId, $productId]);
return $stmt->fetch();
}
/**
* Thêm mới hoặc cập nhật số lượng (Upsert)
*/
public function addOrUpdate($userId, $productId, $quantity) {
$existing = $this->findItem($userId, $productId);
if ($existing) {
// Nếu đã có, cộng dồn số lượng
$stmt = $this->db->prepare("UPDATE Cart_Items SET quantity = quantity + ? WHERE id = ?");
return $stmt->execute([$quantity, $existing['id']]);
} else {
// Nếu chưa có, thêm mới
$stmt = $this->db->prepare("INSERT INTO Cart_Items (user_id, product_id, quantity) VALUES (?, ?, ?)");
return $stmt->execute([$userId, $productId, $quantity]);
}
}
public function updateQuantity($itemId, $quantity) {
$stmt = $this->db->prepare("UPDATE Cart_Items SET quantity = ? WHERE id = ?");
return $stmt->execute([$quantity, $itemId]);
}
public function delete($itemId) {
$stmt = $this->db->prepare("DELETE FROM Cart_Items WHERE id = ?");
return $stmt->execute([$itemId]);
}
}
3. Tầng Controller: Xác thực và Điều phối (CartController.php)
Tại đây, chúng ta sẽ bắt buộc người dùng phải đăng nhập (AuthMiddleware) để lấy được user_id từ Token.
File: app/Controllers/CartController.php
<?php
namespace App\Controllers;
use App\Core\Response;
use App\Models\CartItem;
use App\Models\Product;
use App\Middleware\AuthMiddleware;
class CartController
{
/**
* Thêm sản phẩm vào giỏ
*/
public function add() {
$data = json_decode(file_get_contents("php://input"), true);
// 1. Kiểm tra xác thực
$user = AuthMiddleware::check();
$userId = $user->sub;
if (empty($data['product_id']) || empty($data['quantity'])) {
Response::json(['error' => 'Vui lòng chọn sản phẩm và số lượng'], 422);
}
// 2. Kiểm tra sản phẩm có tồn tại thật không
$productModel = new Product();
if (!$productModel->findById($data['product_id'])) {
Response::json(['error' => 'Sản phẩm không tồn tại'], 404);
}
// 3. Thực hiện Upsert
$cart = new CartItem();
$cart->addOrUpdate($userId, $data['product_id'], $data['quantity']);
Response::json(['message' => 'Đã thêm sản phẩm vào giỏ hàng thành công']);
}
/**
* Xem giỏ hàng cá nhân
*/
public function index() {
$user = AuthMiddleware::check();
$items = (new CartItem())->getUserCart($user->sub);
// Tính tổng tiền toàn giỏ hàng
$cartTotal = array_sum(array_column($items, 'total'));
Response::json([
'status' => 'success',
'cart_total' => $cartTotal,
'items' => $items
]);
}
public function update($id) {
$data = json_decode(file_get_contents("php://input"), true);
if (empty($data['quantity']) || $data['quantity'] <= 0) {
Response::json(['error' => 'Số lượng không hợp lệ'], 422);
}
(new CartItem())->updateQuantity($id, $data['quantity']);
Response::json(['message' => 'Đã cập nhật số lượng']);
}
public function destroy($id) {
(new CartItem())->delete($id);
Response::json(['message' => 'Đã xóa sản phẩm khỏi giỏ hàng']);
}
}
4. Cấu hình Route (index.php)
Đăng ký các Endpoint cho giỏ hàng. Lưu ý tất cả các route này đều nên được bảo vệ bởi Token.
File: public/index.php
use App\Controllers\CartController;
$cartController = new CartController();
// Nhóm API Giỏ hàng
if ($uri === '/api/cart/add' && $method === 'POST') {
$cartController->add();
} elseif ($uri === '/api/cart' && $method === 'GET') {
$cartController->index();
} elseif (preg_match('#^/api/cart/(\d+)$#', $uri, $matches) && $method === 'PUT') {
$cartController->update($matches[1]);
} elseif (preg_match('#^/api/cart/(\d+)$#', $uri, $matches) && $method === 'DELETE') {
$cartController->destroy($matches[1]);
}
5. Kiểm thử API (Test with Curl)
Đừng quên đính kèm JWT Token nhận được từ API Login nhé!
Thêm sản phẩm:
curl -X POST http://localhost:8000/api/cart/add \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"product_id": 1, "quantity": 2}'
Xem giỏ hàng:
curl http://localhost:8000/api/cart -H "Authorization: Bearer YOUR_JWT_TOKEN"
Tạm kết
Vậy là chúng ta đã hoàn thành module Giỏ hàng – bước chuẩn bị quan trọng nhất trước khi "chốt đơn". Hệ thống của chúng ta đang ngày càng hoàn thiện và mang dáng dấp của một trang TMĐT thực thụ.
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 mình ra bài đều đặn. Chúc bạn code vui vẻ, bố đời!
All rights reserved