0

[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 21: Checkout & Xử lý Đơn hàng với Database Transaction

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

Năm 2026 rồi, việc xây dựng một hệ thống đặt hàng không chỉ đơn thuần là INSERT dữ liệu vào bảng. Hãy tưởng tượng: Bạn đã tạo xong đơn hàng nhưng khi đang xóa giỏ hàng thì... Server sập hoặc mất kết nối Database. Kết quả là khách hàng có đơn hàng mới nhưng giỏ hàng vẫn còn nguyên đồ. Thật là thảm họa đúng không?

Để giải quyết vấn đề "All or Nothing" (Tất cả hoặc không có gì), chúng ta sẽ sử dụng Database Transaction.

1. Khái niệm cốt lõi: Database Transaction

Transaction giúp nhóm nhiều câu lệnh SQL thành một khối duy nhất.

Commit: Nếu tất cả câu lệnh thành công, dữ liệu sẽ được lưu vĩnh viễn.

Rollback: Nếu chỉ cần một câu lệnh lỗi, toàn bộ các thay đổi trước đó trong khối sẽ bị hủy bỏ, đưa Database về trạng thái ban đầu.

2. Tầng Model: Xử lý Giao dịch Đặt hàng (Order.php)

Chúng ta sẽ thực hiện 3 bước trong một Transaction:

Tính tổng tiền và tạo bản ghi trong bảng Orders.

Lưu chi tiết từng sản phẩm vào bảng Order_Items.

Làm sạch giỏ hàng của người dùng.

File: app/Models/Order.php

<?php
namespace App\Models;

use App\Core\Database;
use Exception;

class Order {
    protected $db;

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

    /**
     * Tạo đơn hàng sử dụng Transaction
     */
    public function create($userId, $shippingAddressId, $items) {
        // Bắt đầu giao dịch
        $this->db->beginTransaction();

        try {
            $total = 0;
            foreach ($items as $item) {
                $total += $item['price'] * $item['quantity'];
            }

            // 1. Tạo đơn hàng chính
            $stmt = $this->db->prepare("
                INSERT INTO Orders (user_id, total_price, status, shipping_address_id, created_at)
                VALUES (?, ?, 'pending', ?, NOW())
            ");
            $stmt->execute([$userId, $total, $shippingAddressId]);
            $orderId = $this->db->lastInsertId();

            // 2. Thêm chi tiết đơn hàng (Order Items)
            $stmtItem = $this->db->prepare("
                INSERT INTO Order_Items (order_id, product_id, quantity, price)
                VALUES (?, ?, ?, ?)
            ");

            foreach ($items as $item) {
                $stmtItem->execute([
                    $orderId,
                    $item['product_id'],
                    $item['quantity'],
                    $item['price']
                ]);
            }

            // 3. Xóa giỏ hàng sau khi đặt thành công
            $this->db->prepare("DELETE FROM Cart_Items WHERE user_id = ?")->execute([$userId]);

            // Nếu mọi thứ tốt đẹp, xác nhận lưu dữ liệu
            $this->db->commit();
            return $orderId;

        } catch (Exception $e) {
            // Có lỗi xảy ra, hoàn tác toàn bộ thao tác
            $this->db->rollBack();
            error_log("Order Creation Error: " . $e->getMessage());
            throw $e;
        }
    }
}

3. Tầng Controller: Điều phối luồng Checkout (OrderController.php)

Controller sẽ đóng vai trò kiểm tra giỏ hàng có trống không và chuẩn bị dữ liệu trước khi gọi Model.

File: app/Controllers/OrderController.php

<?php
namespace App\Controllers;

use App\Core\Response;
use App\Models\CartItem;
use App\Models\Order;
use App\Middleware\AuthMiddleware;

class OrderController
{
    public function create() {
        // Xác thực người dùng qua JWT
        $user = AuthMiddleware::check();
        $userId = $user->sub;

        $data = json_decode(file_get_contents("php://input"), true);
        if (empty($data['shipping_address_id'])) {
            Response::json(['error' => 'Vui lòng chọn địa chỉ giao hàng'], 422);
        }

        // 1. Lấy dữ liệu giỏ hàng hiện tại của User
        $cartModel = new CartItem();
        $cartItems = $cartModel->getUserCart($userId);

        if (empty($cartItems)) {
            Response::json(['error' => 'Giỏ hàng của bạn đang trống'], 422);
        }

        // 2. Chuẩn bị dữ liệu để truyền vào Model
        $preparedItems = [];
        foreach ($cartItems as $item) {
            $preparedItems[] = [
                'product_id' => $item['product_id'],
                'quantity'   => $item['quantity'],
                'price'      => $item['price'] // Lưu giá tại thời điểm mua để tránh thay đổi sau này
            ];
        }

        try {
            $orderModel = new Order();
            $orderId = $orderModel->create($userId, $data['shipping_address_id'], $preparedItems);

            Response::json([
                'message' => 'Đặt hàng thành công!',
                'order_id' => $orderId
            ], 201);
        } catch (\Exception $e) {
            Response::json(['error' => 'Không thể tạo đơn hàng, vui lòng thử lại sau'], 500);
        }
    }
}

4. Cấu hình Route (index.php)

Đăng ký Endpoint mới cho việc tạo đơn hàng.

File: public/index.php

use App\Controllers\OrderController;

$orderController = new OrderController();

// API Tạo đơn hàng: POST /api/orders/create
if ($uri === '/api/orders/create' && $method === 'POST') {
    $orderController->create();
}

5. Kiểm thử API (Test with Curl)

Bạn cần gửi kèm ID địa chỉ giao hàng (Shipping Address ID) đã có trong hệ thống.

curl -X POST http://localhost:8000/api/orders/create \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"shipping_address_id": 2}'

Tạm kết

Vậy là hệ thống của chúng ta đã có thể xử lý luồng mua hàng trọn vẹn. Việc sử dụng Transaction là bước tiến lớn giúp ứng dụng của bạn trở nên chuyên nghiệp và tin cậy hơn. Hãy để lại bình luận 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
Let's register a Viblo Account to get more interesting posts.