การสร้าง REST API ด้วย PHP 8 และ Best Practices ที่ควรรู้

·
11 พฤศจิกายน 2024
·
Programming, การพัฒนาเว็บไซต์

สวัสดีครับเพื่อนๆ นักพัฒนาทุกคน! 🖐️ วันนี้เรามาคุยกันเรื่องการสร้าง REST API ด้วย PHP 8 กันนะครับ ถ้าคุณเป็นนักพัฒนาที่ต้องทำงานกับเว็บแอปพลิเคชันสมัยใหม่ คงหนีไม่พ้นการพัฒนา API แน่นอน (เหมือนหนีเมียไปเที่ยว ยังไงก็ต้องกลับมาเจอ 😅)

การสร้าง REST API ด้วย PHP 8 และ Best Practices ที่ควรรู้

ความสำคัญของ REST API

การพัฒนาแอปพลิเคชันในปัจจุบันต้องรองรับหลากหลายแพลตฟอร์ม ไม่ว่าจะเป็นเว็บไซต์ แอปพลิเคชันมือถือ หรืออุปกรณ์ IoT REST API จึงเป็นมาตรฐานสำคัญที่ช่วยให้ระบบต่างๆ สามารถสื่อสารกันได้อย่างมีประสิทธิภาพ เปรียบเสมือนภาษากลางที่ทุกแพลตฟอร์มเข้าใจร่วมกัน

ทำไมต้อง PHP 8?

PHP 8 มาพร้อมกับฟีเจอร์ใหม่ๆ ที่ช่วยให้การเขียน API มีประสิทธิภาพมากขึ้น

  • Named Arguments: ช่วยให้โค้ดอ่านง่ายขึ้น
  • Constructor Property Promotion: ลดโค้ดซ้ำซ้อน
  • Match Expression: เขียน switch-case แบบสวยๆ
  • Nullsafe Operator: จัดการ null ได้ปลอดภัยขึ้น
  • Union Types: type hint ที่ยืดหยุ่นขึ้น
Key Concepts Of Rest Api

แนวคิดหลักของ REST API

REST (Representational State Transfer) คือแนวคิดในการออกแบบ API ที่ใช้ HTTP protocol เป็นพื้นฐาน โดยมีหลักการสำคัญคือ

  • Stateless: ทุก request ต้องมีข้อมูลครบถ้วนในตัวเอง
  • Client-Server: แยกส่วน frontend และ backend ชัดเจน
  • Cacheable: สามารถ cache ข้อมูลได้
  • Uniform Interface: ใช้รูปแบบการเรียกที่เป็นมาตรฐาน

HTTP Methods ที่ใช้บ่อย

การใช้ HTTP Methods ที่ถูกต้องเป็นหัวใจสำคัญของ REST API ซึ่งแต่ละ method มีจุดประสงค์ที่ชัดเจน

// GET: ใช้สำหรับดึงข้อมูล ไม่ควรเปลี่ยนแปลงข้อมูลในฐานข้อมูล
GET /api/users           // ดึงรายการ users ทั้งหมด
GET /api/users/1         // ดึงข้อมูล user ID 1

// POST: ใช้สำหรับสร้างข้อมูลใหม่
POST /api/users          // สร้าง user ใหม่

// PUT: ใช้สำหรับอัพเดทข้อมูลทั้งหมด
PUT /api/users/1         // อัพเดทข้อมูล user ID 1 ทั้งหมด

// PATCH: ใช้สำหรับอัพเดทข้อมูลบางส่วน
PATCH /api/users/1       // อัพเดทข้อมูล user ID 1 บางฟิลด์

// DELETE: ใช้สำหรับลบข้อมูล
DELETE /api/users/1      // ลบ user ID 1

HTTP Status Codes ที่ควรรู้

Status Codes เป็นวิธีมาตรฐานในการบอกผลลัพธ์ของ request แบ่งเป็นหมวดหมู่ดังนี้

  • 2xx: Success
    • 200 OK: request สำเร็จ
    • 201 Created: สร้างข้อมูลสำเร็จ
    • 204 No Content: สำเร็จแต่ไม่มีข้อมูลส่งกลับ
  • 4xx: Client Error
    • 400 Bad Request: request ไม่ถูกต้อง
    • 401 Unauthorized: ไม่ได้ authentication
    • 403 Forbidden: ไม่มีสิทธิ์
    • 404 Not Found: ไม่พบข้อมูล
    • 429 Too Many Requests: request เยอะเกินไป
  • 5xx: Server Error
    • 500 Internal Server Error: เซิร์ฟเวอร์มีปัญหา
    • 503 Service Unavailable: เซิร์ฟเวอร์ไม่พร้อมให้บริการ

การเริ่มต้นสร้าง REST API

โครงสร้างโปรเจค

การจัดโครงสร้างโปรเจคที่ดีจะช่วยให้พัฒนาและดูแลรักษาง่ายขึ้น ต่อไปนี้เป็นโครงสร้างมาตรฐานที่แนะนำ

project/
├── public/              # เป็น entry point ของแอพ
│   └── index.php
├── src/                 # โค้ดหลักของแอพ
│   ├── Controllers/     # จัดการ request/response
│   ├── Models/          # โมเดลข้อมูล
│   ├── Services/        # business logic
│   └── Config/          # ไฟล์ config ต่างๆ
├── tests/               # ไฟล์ test ทั้งหมด
├── vendor/              # dependencies
├── .env                 # ตั้งค่า environment
└── composer.json        # กำหนด dependencies

การตั้งค่า Apache (.htaccess)

การตั้งค่า Apache ที่เหมาะสมจะช่วยให้ URL routing ทำงานได้อย่างถูกต้อง

# เปิดใช้งาน URL Rewriting
RewriteEngine On

# ถ้าไม่ใช่ไฟล์หรือโฟลเดอร์จริง ให้ส่งไปที่ index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

Router พื้นฐาน

Router ทำหน้าที่จัดการ requests และส่งต่อไปยัง controller ที่เหมาะสม

<?php
declare(strict_types=1);

class Router {
    private array $routes = [];
    
    // เพิ่ม route ใหม่
    public function addRoute(string $method, string $path, callable $handler): void {
        $this->routes[] = [
            'method' => $method,
            'path' => $path,
            'handler' => $handler
        ];
    }
    
    // จัดการ request ที่เข้ามา
    public function handleRequest(string $method, string $uri): mixed {
        foreach ($this->routes as $route) {
            if ($route['method'] === $method && $this->matchPath($route['path'], $uri)) {
                return ($route['handler'])();
            }
        }
        
        throw new RuntimeException('Route not found', 404);
    }
    
    // ตรวจสอบว่า path ตรงกับ route หรือไม่
    private function matchPath(string $routePath, string $uri): bool {
        $pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePath);
        return preg_match("#^$pattern$#", $uri) === 1;
    }
}
Best Practices

Best Practices

การออกแบบ Endpoints

การออกแบบ endpoints ที่ดีจะช่วยให้ API ใช้งานง่ายและเป็นมาตรฐาน

แนวทางที่ควรทำ

// ใช้ noun แทน resource เพื่อบ่งบอกว่ากำลังจัดการกับอะไร
GET /api/articles
GET /api/articles/123

// ใช้ query parameters สำหรับ filtering หรือ sorting
GET /api/articles?status=published&category=tech

// ใช้ nested routes เมื่อข้อมูลมีความสัมพันธ์กัน
GET /api/articles/123/comments

สิ่งที่ควรหลีกเลี่ยง

// ไม่ควรใช้ verb ใน URL
GET /api/getArticles
POST /api/createArticle

// ไม่ควรใส่ operation ใน URL
POST /api/articles/123/publish

Authentication

การยืนยันตัวตนเป็นส่วนสำคัญของ API ต่อไปนี้เป็นตัวอย่างการใช้ JWT

<?php
class JWTAuth {
    private string $secret;
    
    public function __construct(string $secret) {
        $this->secret = $secret;
    }
    
    // สร้าง token
    public function createToken(array $payload): string {
        $header = base64_encode(json_encode(['typ' => 'JWT', 'alg' => 'HS256']));
        $payload = base64_encode(json_encode($payload));
        $signature = hash_hmac('sha256', "$header.$payload", $this->secret, true);
        $signature = base64_encode($signature);
        
        return "$header.$payload.$signature";
    }
    
    // ตรวจสอบ token
    public function validateToken(string $token): bool {
        [$header, $payload, $signature] = explode('.', $token);
        $expectedSignature = hash_hmac(
            'sha256',
            "$header.$payload",
            $this->secret,
            true
        );
        $expectedSignature = base64_encode($expectedSignature);
        
        return hash_equals($signature, $expectedSignature);
    }
}

Input Validation

การตรวจสอบข้อมูลที่รับเข้ามาเป็นสิ่งสำคัญเพื่อความปลอดภัยและความถูกต้อง

<?php
class UserValidator {
    public function validate(array $data): array {
        $errors = [];
        
        // ตรวจสอบ email
        if (empty($data['email'])) {
            $errors['email'] = 'Email is required';
        } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            $errors['email'] = 'Invalid email format';
        }
        
        // ตรวจสอบ password
        if (empty($data['password'])) {
            $errors['password'] = 'Password is required';
        } elseif (strlen($data['password']) < 8) {
            $errors['password'] = 'Password must be at least 8 characters';
        }
        
        return $errors;
    }
}

Error Handling

การจัดการ Error ที่เหมาะสมจะช่วยให้ API มีความน่าเชื่อถือและใช้งานง่ายขึ้น

<?php

class APIException extends Exception {
    private int $statusCode;
    private array $errors;
    
    public function __construct(
        string $message,
        int $statusCode = 500,
        array $errors = []
    ) {
        parent::__construct($message);
        $this->statusCode = $statusCode;
        $this->errors = $errors;
    }
    
    public function getStatusCode(): int {
        return $this->statusCode;
    }
    
    public function getErrors(): array {
        return $this->errors;
    }
}

function handleError(Throwable $e): void {
    if ($e instanceof APIException) {
        http_response_code($e->getStatusCode());
        echo json_encode([
            'error' => $e->getMessage(),
            'details' => $e->getErrors()
        ]);
    } else {
        http_response_code(500);
        echo json_encode([
            'error' => 'Internal Server Error',
            'message' => $e->getMessage()
        ]);
    }
}

Advanced Topics

Rate Limiting

การจำกัด request ต่อวินาทีจะช่วยป้องกันการถูกโจมตีและรักษาเสถียรภาพของ API

<?php

class RateLimiter {
    private Redis $redis;
    private int $limit;
    private int $window;
    
    public function __construct(Redis $redis, int $limit, int $window) {
        $this->redis = $redis;
        $this->limit = $limit;
        $this->window = $window;
    }
    
    public function checkLimit(string $key): bool {
        $current = $this->redis->get($key) ?? 0;
        
        if ($current >= $this->limit) {
            return false;
        }
        
        $this->redis->incr($key);
        $this->redis->expire($key, $this->window);
        
        return true;
    }
}

Caching

การใช้ Caching อย่างเหมาะสมจะช่วยเพิ่มประสิทธิภาพของ API

<?php

class APICache {
    private Redis $redis;
    private int $ttl;
    
    public function __construct(Redis $redis, int $ttl = 3600) {
        $this->redis = $redis;
        $this->ttl = $ttl;
    }
    
    public function get(string $key): ?string {
        return $this->redis->get($key);
    }
    
    public function set(string $key, string $value): void {
        $this->redis->setex($key, $this->ttl, $value);
    }
    
    public function invalidate(string $key): void {
        $this->redis->del($key);
    }
}

ตัวอย่างโค้ดที่สมบูรณ์

User API Controller

ต่อไปนี้เป็นตัวอย่างโค้ดของ User Controller ที่รวบรวม Best Practices ต่างๆ

<?php

declare(strict_types=1);

class UserController {
    private PDO $db;
    private JWTAuth $auth;
    private UserValidator $validator;
    private APICache $cache;
    
    public function __construct(
        PDO $db,
        JWTAuth $auth,
        UserValidator $validator,
        APICache $cache
    ) {
        $this->db = $db;
        $this->auth = $auth;
        $this->validator = $validator;
        $this->cache = $cache;
    }
    
    public function index(): array {
        $cacheKey = 'users:list';
        $cached = $this->cache->get($cacheKey);
        
        if ($cached !== null) {
            return json_decode($cached, true);
        }
        
        $stmt = $this->db->query('SELECT * FROM users');
        $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        $this->cache->set($cacheKey, json_encode($users));
        
        return $users;
    }
    
    public function store(array $data): array {
        $errors = $this->validator->validate($data);
        
        if (!empty($errors)) {
            throw new APIException('Validation failed', 400, $errors);
        }
        
        $stmt = $this->db->prepare(
            'INSERT INTO users (email, password) VALUES (?, ?)'
        );
        
        $hashedPassword = password_hash($data['password'], PASSWORD_ARGON2ID);
        
        $stmt->execute([$data['email'], $hashedPassword]);
        
        $userId = $this->db->lastInsertId();
        
        $this->cache->invalidate('users:list');
        
        return [
            'id' => $userId,
            'email' => $data['email']
        ];
    }
    
    public function show(int $id): array {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$id]);
        
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$user) {
            throw new APIException('User not found', 404);
        }
        
        return $user;
    }
}

สรุป

ข้อควรระวัง

  1. Security First: อย่าลืมเรื่องความปลอดภัย
    • ใช้ HTTPS เสมอ
    • Validate input ทุกครั้ง
    • ระวัง SQL Injection
    • ใช้ Authentication ที่แข็งแรง
  2. Performance Matters: ระวังเรื่องประสิทธิภาพ
    • ใช้ Caching อย่างเหมาะสม
    • Optimize Database Queries
    • ระวัง N+1 Query Problem
    • จำกัดขนาดของ Response
  3. API Design: ระวังการออกแบบ API ที่ไม่ดี
    • อย่าเปิดเผยข้อมูลภายในมากเกินไป
    • ควรมี Rate Limiting เสมอ
    • ออกแบบ Endpoints ให้เป็นมาตรฐาน
    • ระวังการตั้งชื่อที่สับสน
  4. Error Messages: ระวังการส่ง Error Messages
    • อย่าส่ง Stack Trace ไปให้ Client
    • ใช้ HTTP Status Code ให้ถูกต้อง
    • ให้ข้อมูล Error ที่มีประโยชน์แต่ไม่เปิดเผยความลับ

แหล่งข้อมูลเพิ่มเติม

Official Documentation

Frameworks & Libraries ที่น่าสนใจ

  • Slim Framework – สำหรับสร้าง REST API ขนาดเล็กถึงกลาง
  • Laravel – Full-stack framework ที่มี API tools ครบครัน
  • Symfony – Enterprise framework ที่เน้นความยืดหยุ่น

Tools สำหรับ API Development

  • Postman – ทดสอบ API
  • Swagger– สร้าง API Documentation
  • PHP-CS-Fixer – จัดระเบียบโค้ด
  • PHPStan – Static Analysis
  • Xdebug – Debugging และ Profiling

ตัวอย่าง API Documentation

/**
 * @api {get} /api/v1/users Get Users List
 * @apiVersion 1.0.0
 * @apiName GetUsers
 * @apiGroup User
 *
 * @apiHeader {String} Authorization Bearer token
 *
 * @apiParam {Number} [page=1] Page number for pagination
 * @apiParam {Number} [limit=10] Number of records per page
 * @apiParam {String} [search] Search term for filtering users
 *
 * @apiSuccess {Object[]} users List of users
 * @apiSuccess {Number} users.id User ID
 * @apiSuccess {String} users.email User email
 * @apiSuccess {String} users.name User full name
 * @apiSuccess {String} users.created_at Creation timestamp
 *
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *       "data": [{
 *         "id": 1,
 *         "email": "john@example.com",
 *         "name": "John Doe",
 *         "created_at": "2024-01-01 12:00:00"
 *       }],
 *       "meta": {
 *         "current_page": 1,
 *         "total_pages": 10,
 *         "total_records": 100
 *       }
 *     }
 *
 * @apiError {Object} 401 Unauthorized
 * @apiError {Object} 400 Bad Request
 *
 * @apiErrorExample {json} Error-Response:
 *     HTTP/1.1 401 Unauthorized
 *     {
 *       "error": "Unauthorized",
 *       "message": "Invalid or expired token"
 *     }
 */

การทดสอบ API

<?php

use PHPUnit\Framework\TestCase;

class UserApiTest extends TestCase
{
    private UserController $controller;
    
    protected function setUp(): void
    {
        $db = new PDO('sqlite::memory:');
        $auth = new JWTAuth('secret');
        $validator = new UserValidator();
        $cache = new APICache(new Redis());
        
        $this->controller = new UserController($db, $auth, $validator, $cache);
    }
    
    public function testCreateUserSuccess(): void
    {
        $userData = [
            'email' => 'test@example.com',
            'password' => 'secure123!'
        ];
        
        $result = $this->controller->store($userData);
        
        $this->assertArrayHasKey('id', $result);
        $this->assertEquals($userData['email'], $result['email']);
    }
    
    public function testCreateUserValidationFails(): void
    {
        $this->expectException(APIException::class);
        $this->expectExceptionCode(400);
        
        $userData = [
            'email' => 'invalid-email',
            'password' => '123' // too short
        ];
        
        $this->controller->store($userData);
    }
}

Tips จากประสบการณ์จริง

ผมได้รวบรวมข้อแนะนำเพิ่มเติมจากประสบการณ์การพัฒนา REST API ในส่วนท้ายของบทความ เช่น การจัดการ Dependencies, Configuration, Logging และ Monitoring

1.การจัดการ Dependencies

  • ใช้ Dependency Injection
  • แยก Business Logic ออกจาก Controllers
  • ใช้ Interface แทน Concrete Classes
interface UserRepositoryInterface
{
    public function findAll(): array;
    public function findById(int $id): ?array;
    public function create(array $data): int;
}

class UserController
{
    public function __construct(
        private UserRepositoryInterface $repository,
        private UserValidatorInterface $validator
    ) {}
    
    // Methods using interfaces
}

2.การจัดการ Configuration

  • ใช้ Environment Variables
  • แยก Config ตาม Environment
  • มี Default Values ที่ปลอดภัย
// config/database.php
return [
    'host' => $_ENV['DB_HOST'] ?? 'localhost',
    'database' => $_ENV['DB_NAME'] ?? 'api_db',
    'username' => $_ENV['DB_USER'] ?? 'root',
    'password' => $_ENV['DB_PASS'] ?? '',
    'options' => [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ],
];

3.การ Monitor และ Logging

  • ใช้ Request ID ในทุก Request
  • Log ข้อมูลที่สำคัญแต่ไม่ sensitive
  • ติดตาม Performance Metrics
class ApiLogger
{
    public function logRequest(string $requestId, array $data): void
    {
        $sanitizedData = $this->removeSensitiveData($data);
        error_log(sprintf(
            '[%s] Request: %s',
            $requestId,
            json_encode($sanitizedData)
        ));
    }
    
    private function removeSensitiveData(array $data): array
    {
        unset($data['password'], $data['token']);
        return $data;
    }
}

ข้อแนะนำสุดท้าย

  1. เริ่มต้นด้วยการออกแบบที่ดี ใช้เวลาในการวางแผน
  2. ทำ Documentation ตั้งแต่เริ่มต้น
  3. เขียน Tests ควบคู่ไปกับการพัฒนา
  4. รับฟัง Feedback จาก Frontend Developers
  5. ติดตาม Best Practices และ Security Updates
  6. อย่าลืมเรื่อง Performance ตั้งแต่แรก

สุดท้ายนี้ การพัฒนา REST API ที่ดีต้องอาศัยทั้งความรู้และประสบการณ์ อย่าลืมว่าทุกโปรเจคมีความต้องการที่แตกต่างกัน เลือกใช้เทคนิคและ tools ที่เหมาะสมกับโปรเจคของคุณ แล้วพบกันใหม่ในบทความหน้าครับ! 👋