0

[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 18: Quản lý Bộ sưu tập Ảnh Sản phẩm (Product Gallery)

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

Ở các phần trước, chúng ta đã có thông tin cơ bản của sản phẩm. Nhưng thực tế, một sản phẩm thường cần nhiều góc chụp: mặt trước, mặt bên, chi tiết linh kiện... Đó là lý do chúng ta cần một bảng riêng biệt để lưu trữ danh sách hình ảnh này thay vì nhồi nhét vào bảng Products.

Trong bài viết này, chúng ta sẽ xây dựng các Endpoint để Thêm, Xem và Xóa ảnh sản phẩm. Đây là nền tảng để sau này chúng ta tích hợp module Upload file thực thụ.

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

Chúng ta sử dụng bảng Product_Images để lưu trữ đường dẫn ảnh. Lưu ý khóa ngoại product_id để biết ảnh đó thuộc về "vị chủ nhân" nào.

CREATE TABLE Product_Images (
  id INT PRIMARY KEY AUTO_INCREMENT,
  product_id INT,
  image VARCHAR(255),
  created_at DATETIME,
  FOREIGN KEY (product_id) REFERENCES Products(id) ON DELETE CASCADE
);

2. Tầng Model: Thao tác với Ảnh (ProductImage.php)

Model này sẽ xử lý các tác vụ truy vấn và ghi dữ liệu cho bộ sưu tập ảnh.

File: app/Models/ProductImage.php

<?php
namespace App\Models;

use App\Core\Database;

class ProductImage {
    protected $db;

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

    /**
     * Lấy danh sách ảnh của một sản phẩm cụ thể
     */
    public function getImagesByProduct($productId) {
        $stmt = $this->db->prepare("SELECT * FROM Product_Images WHERE product_id = ?");
        $stmt->execute([$productId]);
        return $stmt->fetchAll();
    }

    /**
     * Lưu đường dẫn ảnh vào Database
     */
    public function addImage($productId, $imageUrl) {
        $stmt = $this->db->prepare("
            INSERT INTO Product_Images (product_id, image, created_at)
            VALUES (?, ?, NOW())
        ");
        return $stmt->execute([$productId, $imageUrl]);
    }

    /**
     * Xóa một ảnh cụ thể theo ID
     */
    public function delete($id) {
        $stmt = $this->db->prepare("DELETE FROM Product_Images WHERE id = ?");
        return $stmt->execute([$id]);
    }
}

3. Tầng Controller: Điều phối Bộ sưu tập (ProductImageController.php)

Controller sẽ kiểm tra sự tồn tại của sản phẩm trước khi cho phép thêm ảnh, đảm bảo dữ liệu không bị "mồ côi".

File: app/Controllers/ProductImageController.php

<?php
namespace App\Controllers;

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

class ProductImageController
{
    public function store($productId) {
        $data = json_decode(file_get_contents("php://input"), true);

        if (empty($data['image'])) {
            Response::json(['error' => 'Vui lòng cung cấp URL hình ảnh'], 422);
        }

        // Kiểm tra xem sản phẩm có tồn tại hay không
        $productModel = new Product();
        if (!$productModel->findById($productId)) {
            Response::json(['error' => 'Sản phẩm không tồn tại'], 404);
        }

        $imgModel = new ProductImage();
        $imgModel->addImage($productId, $data['image']);

        Response::json(['message' => 'Đã thêm ảnh vào bộ sưu tập thành công']);
    }

    public function index($productId) {
        $imgModel = new ProductImage();
        $images = $imgModel->getImagesByProduct($productId);
        Response::json(['status' => 'success', 'images' => $images]);
    }

    public function destroy($id) {
        $imgModel = new ProductImage();
        $imgModel->delete($id);
        Response::json(['message' => 'Đã xóa ảnh thành công']);
    }
}

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

Chúng ta cần định nghĩa các Route để xử lý ảnh theo ID của sản phẩm hoặc ID của chính tấm ảnh đó.

File: public/index.php

use App\Controllers\ProductImageController;

$productImageController = new ProductImageController();

// 1. Thêm ảnh cho sản phẩm (POST /api/products/{id}/images)
if (preg_match('#^/api/products/(\d+)/images$#', $uri, $matches) && $method === 'POST') {
    $productImageController->store($matches[1]);

// 2. Lấy danh sách ảnh của sản phẩm (GET /api/products/{id}/images)
} elseif (preg_match('#^/api/products/(\d+)/images$#', $uri, $matches) && $method === 'GET') {
    $productImageController->index($matches[1]);

// 3. Xóa một ảnh cụ thể (DELETE /api/product-images/{id})
} elseif (preg_match('#^/api/product-images/(\d+)$#', $uri, $matches) && $method === 'DELETE') {
    $productImageController->destroy($matches[1]);
}

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

Hãy thử "bơm" một vài tấm ảnh cho sản phẩm đầu tiên của bạn nhé:

Thêm ảnh:

curl -X POST http://localhost:8000/api/products/1/images \
  -H "Content-Type: application/json" \
  -d '{"image": "https://hasaki.vn/ip15-white.jpg"}'

Xem bộ sưu tập:

curl http://localhost:8000/api/products/1/images

Tạm kết

Vậy là module Sản phẩm của chúng ta đã "đủ đầy" từ thông tin đến hình ảnh. Đây là một bước đệm cực kỳ quan trọng trước khi chúng ta tiến tới những tính năng "nặng đô" hơ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 mình ra bài đều đặn. Chúc các bạn code vui vẻ, "bố đời"!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.