สวัสดีครับเพื่อนๆ นักพัฒนาทุกคน! 🖐️ วันนี้เรามาคุยกันเรื่องการสร้าง REST API ด้วย PHP 8 กันนะครับ ถ้าคุณเป็นนักพัฒนาที่ต้องทำงานกับเว็บแอปพลิเคชันสมัยใหม่ คงหนีไม่พ้นการพัฒนา API แน่นอน (เหมือนหนีเมียไปเที่ยว ยังไงก็ต้องกลับมาเจอ 😅)
ความสำคัญของ 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 ที่ยืดหยุ่นขึ้น
แนวคิดหลักของ 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
การออกแบบ 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;
}
}
สรุป
ข้อควรระวัง
- Security First: อย่าลืมเรื่องความปลอดภัย
- ใช้ HTTPS เสมอ
- Validate input ทุกครั้ง
- ระวัง SQL Injection
- ใช้ Authentication ที่แข็งแรง
- Performance Matters: ระวังเรื่องประสิทธิภาพ
- ใช้ Caching อย่างเหมาะสม
- Optimize Database Queries
- ระวัง N+1 Query Problem
- จำกัดขนาดของ Response
- API Design: ระวังการออกแบบ API ที่ไม่ดี
- อย่าเปิดเผยข้อมูลภายในมากเกินไป
- ควรมี Rate Limiting เสมอ
- ออกแบบ Endpoints ให้เป็นมาตรฐาน
- ระวังการตั้งชื่อที่สับสน
- 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;
}
}
ข้อแนะนำสุดท้าย
- เริ่มต้นด้วยการออกแบบที่ดี ใช้เวลาในการวางแผน
- ทำ Documentation ตั้งแต่เริ่มต้น
- เขียน Tests ควบคู่ไปกับการพัฒนา
- รับฟัง Feedback จาก Frontend Developers
- ติดตาม Best Practices และ Security Updates
- อย่าลืมเรื่อง Performance ตั้งแต่แรก
สุดท้ายนี้ การพัฒนา REST API ที่ดีต้องอาศัยทั้งความรู้และประสบการณ์ อย่าลืมว่าทุกโปรเจคมีความต้องการที่แตกต่างกัน เลือกใช้เทคนิคและ tools ที่เหมาะสมกับโปรเจคของคุณ แล้วพบกันใหม่ในบทความหน้าครับ! 👋