Giới thiệu

Trong môi trường phát triển web ngày nay, API RESTful là cầu nối quan trọng giữa front‑end và back‑end. Khi làm việc với PHP và MySQL, việc thiết kế một API sạch, an toàn và hiệu năng cao không chỉ giúp giảm chi phí bảo trì mà còn nâng cao trải nghiệm người dùng.

Bài viết này sẽ đi sâu vào cách xây dựng một API RESTful hoàn chỉnh bằng PHP PDOMySQL. Chúng ta sẽ xem xét kiến trúc, cách kết nối an toàn, các mẫu CRUD, xử lý lỗi, bảo mật, phân trang, caching và một số công cụ kiểm thử. Cuối cùng, sẽ gợi ý cách học tập hệ thống để nắm vững nền tảng này.

1. Kiến trúc API RESTful cơ bản

Một API RESTful thường tuân thủ các nguyên tắc sau:

  • Địa chỉ (URL) mô tả tài nguyên, không mô tả hành động.
  • Sử dụng các phương thức HTTP chuẩn: GET, POST, PUT, PATCH, DELETE.
  • Trả về dữ liệu ở định dạng JSON (hoặc XML).
  • Trạng thái HTTP phản ánh kết quả (200, 201, 400, 404, 500, …).

Ví dụ, để truy xuất danh sách người dùng:

GET /api/users

Để tạo người dùng mới:

POST /api/users

Chúng ta sẽ xây dựng một router đơn giản dựa trên $_SERVER['REQUEST_URI']$_SERVER['REQUEST_METHOD'] để phân phối yêu cầu tới các controller tương ứng.

2. Kết nối PDO an toàn

PDO (PHP Data Objects) cung cấp một lớp trừu tượng cho các driver cơ sở dữ liệu, hỗ trợ prepared statements giúp ngăn chặn SQL injection. Đầu tiên, tạo một file Database.php chịu trách nhiệm khởi tạo kết nối.

<?php
class Database {
    private $host = 'localhost';
    private $db   = 'demo_api';
    private $user = 'root';
    private $pass = '';
    private $charset = 'utf8mb4';
    private $pdo;
    private $error;
    private $options = [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ];

    public function __construct() {
        $dsn = "mysql:host={$this->host};dbname={$this->db};charset={$this->charset}";
        try {
            $this->pdo = new PDO($dsn, $this->user, $this->pass, $this->options);
        } catch (PDOException $e) {
            $this->error = $e->getMessage();
            // Ở môi trường production, không nên echo lỗi trực tiếp
            die('Database connection error');
        }
    }

    public function getConnection() {
        return $this->pdo;
    }
}
?>

Trong các controller, chúng ta sẽ lấy đối tượng PDO bằng cách:

$db = (new Database())->getConnection();

3. Các thao tác CRUD mẫu

3.1. Đọc danh sách (GET /api/users)

Chúng ta sẽ trả về danh sách người dùng kèm theo phân trang. Đầu vào gồm các tham số pagelimit.

<?php
// usersController.php
require_once 'Database.php';

function getUsers() {
    $db = (new Database())->getConnection();
    $page  = isset($_GET['page'])  ? (int)$_GET['page']  : 1;
    $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
    $offset = ($page - 1) * $limit;

    $stmt = $db->prepare('SELECT id, name, email FROM users ORDER BY id DESC LIMIT :limit OFFSET :offset');
    $stmt->bindValue(':limit',  $limit, PDO::PARAM_INT);
    $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
    $stmt->execute();
    $users = $stmt->fetchAll();

    header('Content-Type: application/json');
    echo json_encode([
        'page'  => $page,
        'limit' => $limit,
        'data'  => $users
    ]);
}
?>

3.2. Tạo mới (POST /api/users)

Yêu cầu gửi JSON chứa nameemail. Chúng ta sẽ thực hiện validation cơ bản và sử dụng prepared statement.

<?php
function createUser() {
    $db = (new Database())->getConnection();
    $input = json_decode(file_get_contents('php://input'), true);
    if (!isset($input['name'], $input['email'])) {
        http_response_code(400);
        echo json_encode(['error' => 'Missing required fields']);
        exit;
    }
    // Kiểm tra email hợp lệ
    if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
        http_response_code(400);
        echo json_encode(['error' => 'Invalid email format']);
        exit;
    }
    $stmt = $db->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
    $stmt->execute([
        ':name'  => $input['name'],
        ':email' => $input['email']
    ]);
    $id = $db->lastInsertId();
    http_response_code(201);
    echo json_encode(['id' => $id, 'message' => 'User created']);
}
?>

3.3. Cập nhật (PUT /api/users/{id})

Thực hiện cập nhật một hoặc nhiều trường. Đối với PUT chúng ta thường yêu cầu toàn bộ đối tượng, nhưng có thể dùng PATCH cho cập nhật một phần.

<?php
function updateUser($id) {
    $db = (new Database())->getConnection();
    $input = json_decode(file_get_contents('php://input'), true);
    if (empty($input)) {
        http_response_code(400);
        echo json_encode(['error' => 'No data provided']);
        exit;
    }
    $fields = [];
    $params = [];
    if (isset($input['name'])) {
        $fields[] = 'name = :name';
        $params[':name'] = $input['name'];
    }
    if (isset($input['email'])) {
        if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
            http_response_code(400);
            echo json_encode(['error' => 'Invalid email format']);
            exit;
        }
        $fields[] = 'email = :email';
        $params[':email'] = $input['email'];
    }
    if (empty($fields)) {
        http_response_code(400);
        echo json_encode(['error' => 'No updatable fields']);
        exit;
    }
    $sql = 'UPDATE users SET ' . implode(', ', $fields) . ' WHERE id = :id';
    $params[':id'] = $id;
    $stmt = $db->prepare($sql);
    $stmt->execute($params);
    echo json_encode(['message' => 'User updated']);
}
?>

3.4. Xóa (DELETE /api/users/{id})

<?php
function deleteUser($id) {
    $db = (new Database())->getConnection();
    $stmt = $db->prepare('DELETE FROM users WHERE id = :id');
    $stmt->execute([':id' => $id]);
    if ($stmt->rowCount()) {
        echo json_encode(['message' => 'User deleted']);
    } else {
        http_response_code(404);
        echo json_encode(['error' => 'User not found']);
    }
}
?>

4. Xử lý lỗi và bảo mật

Trong môi trường production, việc trả về lỗi chi tiết cho người dùng là nguy cơ bảo mật. Chúng ta nên log lỗi vào file riêng và trả về thông báo chung.

<?php
function handleException(PDOException $e) {
    error_log($e->getMessage()); // Ghi log vào server
    http_response_code(500);
    echo json_encode(['error' => 'Internal server error']);
    exit;
}
?>

Đối với các endpoint, hãy bọc các thao tác DB trong try...catch và gọi handleException khi cần.

Thêm các biện pháp bảo mật:

  • Sử dụng prepared statements luôn.
  • Kiểm tra và ràng buộc dữ liệu đầu vào (type, length, pattern).
  • Áp dụng CORS hợp lý: chỉ cho phép domain tin cậy.
  • Thêm rate limiting (ví dụ: bằng mod_evasive hoặc middleware).

5. Phân trang và tối ưu truy vấn

Phân trang không chỉ giảm tải cho client mà còn giảm tải cho DB. Ngoài LIMIT/OFFSET, chúng ta có thể dùng keyset pagination (còn gọi là “cursor pagination”) để tránh vấn đề “slow offset”.

5.1. Keyset pagination mẫu

<?php
function getUsersAfterId($lastId, $limit = 10) {
    $db = (new Database())->getConnection();
    $stmt = $db->prepare('SELECT id, name, email FROM users WHERE id < :lastId ORDER BY id DESC LIMIT :limit');
    $stmt->bindValue(':lastId', $lastId, PDO::PARAM_INT);
    $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
    $stmt->execute();
    return $stmt->fetchAll();
}
?>

Front‑end sẽ truyền lastId của bản ghi cuối cùng nhận được để lấy trang tiếp theo.

6. Caching để giảm tải DB

Cache ở mức API có thể dùng Redis hoặc Memcached. Dưới đây là ví dụ nhanh sử dụng Redis để cache danh sách người dùng trong 60 giây.

<?php
function getUsersCached() {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    $cacheKey = 'users:list';
    $cached = $redis->get($cacheKey);
    if ($cached) {
        return json_decode($cached, true);
    }
    // Nếu không có cache, truy vấn DB
    $db = (new Database())->getConnection();
    $stmt = $db->query('SELECT id, name, email FROM users ORDER BY id DESC');
    $users = $stmt->fetchAll();
    $redis->setex($cacheKey, 60, json_encode($users));
    return $users;
}
?>

Trong thực tế, cần thiết lập chiến lược invalidation khi dữ liệu thay đổi (ví dụ: xóa cache sau INSERT/UPDATE/DELETE).

7. Kiểm thử API

Kiểm thử tự động giúp phát hiện regressions nhanh. Sử dụng PHPUnit kết hợp Guzzle để gửi request HTTP.

<?php
use PHPUnit\Framework\TestCase;
use GuzzleHttp\Client;

class UsersApiTest extends TestCase {
    private $client;
    protected function setUp(): void {
        $this->client = new Client(['base_uri' => 'http://localhost/api/']);
    }
    public function testCreateUser() {
        $response = $this->client->post('users', [
            'json' => ['name' => 'Test User', 'email' => '[email protected]']
        ]);
        $this->assertEquals(201, $response->getStatusCode());
        $data = json_decode($response->getBody(), true);
        $this->assertArrayHasKey('id', $data);
    }
    public function testGetUsers() {
        $response = $this->client->get('users');
        $this->assertEquals(200, $response->getStatusCode());
        $data = json_decode($response->getBody(), true);
        $this->assertIsArray($data['data']);
    }
}
?>

Chạy vendor/bin/phpunit để thực thi các test. Khi có thay đổi trong schema hoặc logic, các test sẽ cảnh báo ngay.

8. Triển khai và CI/CD

Đối với API PHP, Docker là công cụ tiện lợi để chuẩn hoá môi trường. Dưới đây là Dockerfile tối giản:

FROM php:8.2-apache
RUN docker-php-ext-install pdo pdo_mysql
COPY . /var/www/html/
EXPOSE 80

Sau khi build, chúng ta có thể dùng GitHub Actions để tự động build image và đẩy lên Docker Hub.

name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Build Docker image
        run: |
          docker build -t myorg/php-api:${{ github.sha }} .
      - name: Push to Docker Hub
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
        run: |
          echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
          docker push myorg/php-api:${{ github.sha }}

Quy trình CI/CD giúp giảm thiểu lỗi cấu hình môi trường và đảm bảo phiên bản luôn đồng nhất.

9. Kết luận

Việc xây dựng một API RESTful bằng PHP PDO và MySQL không chỉ đòi hỏi kiến thức ngôn ngữ mà còn cần hiểu sâu về bảo mật, tối ưu truy vấn, caching và quy trình kiểm thử. Khi nắm vững các khái niệm trên, bạn sẽ tự tin triển khai các dự án thực tế, từ micro‑service nội bộ tới các sản phẩm SaaS.

Để củng cố nền tảng và đi sâu hơn vào từng chủ đề, Tham khảo khóa học "Lập trình PHP & MySQL cơ bản dành cho người mới" tại đây.