0

[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 16: Hoàn thiện bộ CRUD Sản phẩm (Create - Update - Delete)

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

Ở các phần trước, chúng ta đã làm rất tốt việc hiển thị danh sách và chi tiết sản phẩm. Nhưng một hệ thống không thể gọi là "quản lý" nếu thiếu đi khả năng Thêm, Sửa và Xóa.

Trong bài viết này, chúng ta sẽ hoàn thiện các Endpoint quản trị theo đúng chuẩn RESTful. Đặc biệt, mình sẽ hướng dẫn cách xử lý dữ liệu đầu vào (Validation) cơ bản để đảm bảo Database của bạn luôn "sạch" và an toàn.

1. Tầng Model: Hiện thực hóa các thao tác ghi (Product.php)

Chúng ta cần bổ sung 3 phương thức quan trọng: create, updatedelete. Lưu ý rằng chúng ta luôn sử dụng Prepared Statements để ngăn chặn SQL Injection — một kỹ năng sống còn của Backend Developer.

File: app/Models/Product.php (Bổ sung)

<?php
namespace App\Models;

// ... (Giữ nguyên các hàm getAll, findById từ phần trước)

    /**
     * Tạo sản phẩm mới
     */
    public function create($data) {
        $stmt = $this->db->prepare("
            INSERT INTO Products (name, slug, category_id, description, price, stock, status, unit, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
        ");
        $stmt->execute([
            $data['name'],
            $data['slug'],
            $data['category_id'],
            $data['description'],
            $data['price'],
            $data['stock'],
            $data['status'],
            $data['unit']
        ]);

        return $this->db->lastInsertId();
    }

    /**
     * Cập nhật thông tin sản phẩm
     */
    public function update($id, $data) {
        $stmt = $this->db->prepare("
            UPDATE Products SET 
                name = ?, slug = ?, category_id = ?, description = ?, 
                price = ?, stock = ?, status = ?, unit = ?, updated_at = NOW()
            WHERE id = ?
        ");
        return $stmt->execute([
            $data['name'],
            $data['slug'],
            $data['category_id'],
            $data['description'],
            $data['price'],
            $data['stock'],
            $data['status'],
            $data['unit'],
            $id
        ]);
    }

    /**
     * Xóa sản phẩm khỏi hệ thống
     */
    public function delete($id) {
        $stmt = $this->db->prepare("DELETE FROM Products WHERE id = ?");
        return $stmt->execute([$id]);
    }

2. Tầng Controller: Điều phối và Kiểm duyệt (ProductController.php)

Controller sẽ đóng vai trò "cảnh sát giao thông": Kiểm tra xem Client có gửi thiếu trường nào không, sản phẩm có tồn tại không trước khi yêu cầu Model thực thi.

File: app/Controllers/ProductController.php

<?php
namespace App\Controllers;

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

class ProductController
{
    // ... (Hàm index, show từ phần trước)

    /**
     * POST /api/products - Lưu sản phẩm mới
     */
    public function store() {
        $data = json_decode(file_get_contents("php://input"), true);
        
        // Validation cơ bản các trường bắt buộc
        $required = ['name', 'slug', 'category_id', 'description', 'price', 'stock', 'status', 'unit'];
        foreach ($required as $field) {
            if (empty($data[$field])) {
                Response::json(['error' => "Thiếu thông tin bắt buộc: $field"], 422);
            }
        }

        $productId = (new Product())->create($data);
        Response::json(['message' => 'Tạo sản phẩm thành công', 'id' => $productId], 201);
    }

    /**
     * PUT /api/products/{id} - Cập nhật sản phẩm
     */
    public function update($id) {
        $data = json_decode(file_get_contents("php://input"), true);
        $productModel = new Product();

        // Kiểm tra sản phẩm tồn tại trước khi sửa
        if (!$productModel->findById($id)) {
            Response::json(['error' => 'Không tìm thấy sản phẩm để cập nhật'], 404);
        }

        $productModel->update($id, $data);
        Response::json(['message' => 'Cập nhật sản phẩm thành công']);
    }

    /**
     * DELETE /api/products/{id} - Xóa sản phẩm
     */
    public function destroy($id) {
        $productModel = new Product();
        
        if (!$productModel->findById($id)) {
            Response::json(['error' => 'Không tìm thấy sản phẩm để xóa'], 404);
        }

        $productModel->delete($id);
        Response::json(['message' => 'Đã xóa sản phẩm khỏi hệ thống']);
    }
}

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

Chúng ta tiếp tục mở rộng Router để nhận diện các phương thức HTTP khác nhau (POST, PUT, DELETE).

File: public/index.php

// ... 

// 1. Tạo mới sản phẩm
if ($uri === '/api/products' && $method === 'POST') {
    $productController->store();
}

// 2. Cập nhật sản phẩm (Dùng Regex bắt ID)
elseif (preg_match('#^/api/products/(\d+)$#', $uri, $matches) && $method === 'PUT') {
    $productController->update($matches[1]);
}

// 3. Xóa sản phẩm
elseif (preg_match('#^/api/products/(\d+)$#', $uri, $matches) && $method === 'DELETE') {
    $productController->destroy($matches[1]);
}

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

Hãy thử nghiệm trực tiếp các Endpoint chúng ta vừa xây dựng:

Tạo sản phẩm:

curl -X POST http://localhost:8000/api/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "MacBook Pro M3",
    "slug": "macbook-pro-m3",
    "category_id": 2,
    "description": "Siêu phẩm Apple M3 mới nhất",
    "price": 52990000,
    "stock": 5,
    "status": "active",
    "unit": "chiếc"
}'

Cập nhật sản phẩm ID 5:

curl -X PUT http://localhost:8000/api/products/5 \
  -H "Content-Type: application/json" \
  -d '{"name": "MacBook Pro M3 Max (Updated)", "price": 62990000}'

Lời kết

Vậy là chúng ta đã hoàn thành trọn bộ CRUD cho module Sản phẩm. Hệ thống của chúng ta giờ đây đã có thể vận hành trơn tru từ khâu quản trị đến khâu hiển 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 "bố đời" ra bài đều đặn. Chúc các bạn code vui vẻ!


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í