70 phút
REST API với CodeIgniter 4
API Structure
app/Controllers/Api/
├── BaseApiController.php
├── AuthController.php
├── PostController.php
└── ProductController.php
Base API Controller
<?php
namespace App\Controllers\Api;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
abstract class BaseApiController extends Controller
{
protected $format = 'json';
protected function success($data = null, string $message = 'OK', int $code = 200): ResponseInterface
{
return $this->response->setStatusCode($code)->setJSON([
'success' => true,
'message' => $message,
'data' => $data,
]);
}
protected function error(string $message, int $code = 400, array $errors = []): ResponseInterface
{
return $this->response->setStatusCode($code)->setJSON([
'success' => false,
'message' => $message,
'errors' => $errors,
]);
}
protected function paginated(array $items, object $pager): ResponseInterface
{
return $this->success([
'items' => $items,
'pagination' => [
'current_page' => $pager->getCurrentPage(),
'total_pages' => $pager->getPageCount(),
'per_page' => $pager->getPerPage(),
'total_items' => $pager->getTotal(),
],
]);
}
}
Resource Controller
<?php
namespace App\Controllers\Api;
use App\Models\ProductModel;
class ProductController extends BaseApiController
{
private ProductModel $model;
public function __construct()
{
$this->model = new ProductModel();
}
public function index(): ResponseInterface
{
$search = $this->request->getGet('q');
$sort = $this->request->getGet('sort') ?? 'id';
$order = $this->request->getGet('order') ?? 'ASC';
$page = (int) ($this->request->getGet('page') ?? 1);
if (!in_array($sort, ['id', 'name', 'price', 'created_at'])) {
$sort = 'id';
}
if (!in_array(strtoupper($order), ['ASC', 'DESC'])) {
$order = 'ASC';
}
if ($search) {
$this->model->groupStart()
->like('name', $search)
->orLike('description', $search)
->groupEnd();
}
$products = $this->model->orderBy($sort, $order)->paginate(20, 'default', $page);
return $this->paginated($products, $this->model->pager);
}
public function show($id = null): ResponseInterface
{
$product = $this->model->find((int) $id);
if (!$product) {
return $this->error('Product not found', 404);
}
return $this->success($product);
}
public function create(): ResponseInterface
{
$data = $this->request->getJSON(true);
if (!$this->model->validate($data)) {
return $this->error('Validation failed', 422, $this->model->errors());
}
$id = $this->model->insert($data);
$product = $this->model->find($id);
return $this->success($product, 'Product created', 201);
}
public function update($id = null): ResponseInterface
{
$product = $this->model->find((int) $id);
if (!$product) {
return $this->error('Product not found', 404);
}
$data = $this->request->getJSON(true);
if (!$this->model->validate($data)) {
return $this->error('Validation failed', 422, $this->model->errors());
}
$this->model->update((int) $id, $data);
return $this->success($this->model->find($id), 'Product updated');
}
public function delete($id = null): ResponseInterface
{
$product = $this->model->find((int) $id);
if (!$product) {
return $this->error('Product not found', 404);
}
$this->model->delete((int) $id);
return $this->success(null, 'Product deleted');
}
}
JWT Authentication
Cài đặt
composer require firebase/php-jwt
JWT Service
<?php
namespace App\Libraries;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtService
{
private string $secret;
private int $ttl;
private string $algo;
public function __construct()
{
$this->secret = env('JWT_SECRET', 'your-secret-key');
$this->ttl = (int) env('JWT_TTL', 3600);
$this->algo = 'HS256';
}
public function generate(array $payload): string
{
$issuedAt = time();
$expire = $issuedAt + $this->ttl;
$data = array_merge($payload, [
'iat' => $issuedAt,
'exp' => $expire,
]);
return JWT::encode($data, $this->secret, $this->algo);
}
public function decode(string $token): ?array
{
try {
$decoded = JWT::decode($token, new Key($this->secret, $this->algo));
return (array) $decoded;
} catch (\Exception $e) {
return null;
}
}
public function getBearerToken(): ?string
{
$header = service('request')->getHeaderLine('Authorization');
if (preg_match('/Bearer\s(\S+)/', $header, $matches)) {
return $matches[1];
}
return null;
}
}
Auth Controller
<?php
namespace App\Controllers\Api;
use App\Libraries\JwtService;
use App\Models\UserModel;
class AuthController extends BaseApiController
{
public function login(): ResponseInterface
{
$data = $this->request->getJSON(true);
if (empty($data['email']) || empty($data['password'])) {
return $this->error('Email and password required', 422);
}
$model = new UserModel();
$user = $model->where('email', $data['email'])->first();
if (!$user || !password_verify($data['password'], $user['password'])) {
return $this->error('Invalid credentials', 401);
}
$jwt = new JwtService();
$token = $jwt->generate([
'sub' => $user['id'],
'email' => $user['email'],
]);
return $this->success([
'token' => $token,
'type' => 'Bearer',
'expires_in' => 3600,
'user' => [
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
],
], 'Login successful');
}
public function me(): ResponseInterface
{
$user = $this->request->user ?? null;
return $this->success($user);
}
public function register(): ResponseInterface
{
$data = $this->request->getJSON(true);
$rules = [
'name' => 'required|min_length[2]|max_length[100]',
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|min_length[8]',
];
$validation = \Config\Services::validation();
$validation->setRules($rules);
if (!$validation->run($data)) {
return $this->error('Validation failed', 422, $validation->getErrors());
}
$model = new UserModel();
$id = $model->insert([
'name' => $data['name'],
'email' => $data['email'],
'password' => password_hash($data['password'], PASSWORD_DEFAULT),
]);
$jwt = new JwtService();
$token = $jwt->generate(['sub' => $id, 'email' => $data['email']]);
return $this->success(['token' => $token], 'Registration successful', 201);
}
}
JWT Filter
<?php
namespace App\Filters;
use App\Libraries\JwtService;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class JwtFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
$jwt = new JwtService();
$token = $jwt->getBearerToken();
if (!$token) {
return service('response')
->setStatusCode(401)
->setJSON(['success' => false, 'message' => 'Token required']);
}
$payload = $jwt->decode($token);
if (!$payload) {
return service('response')
->setStatusCode(401)
->setJSON(['success' => false, 'message' => 'Invalid or expired token']);
}
// Make user available to controller
$request->user = $payload;
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
}
}
Routes
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api'], function ($routes) {
// Public
$routes->post('auth/login', 'AuthController::login');
$routes->post('auth/register', 'AuthController::register');
// Protected
$routes->group('', ['filter' => 'jwt'], function ($routes) {
$routes->get('auth/me', 'AuthController::me');
$routes->get('products', 'ProductController::index');
$routes->get('products/(:num)', 'ProductController::show/$1');
$routes->post('products', 'ProductController::create');
$routes->put('products/(:num)', 'ProductController::update/$1');
$routes->delete('products/(:num)', 'ProductController::delete/$1');
});
});
API Response Format
{
"success": true,
"message": "OK",
"data": {
"id": 1,
"name": "Laptop",
"price": "1000.00"
}
}
Testing với cURL
# Login
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password"}'
# Get products with token
curl -X GET http://localhost:8080/api/v1/products \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# Create product
curl -X POST http://localhost:8080/api/v1/products \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"name":"New Product","price":99.99,"stock":10}'
Bài tập thực hành
Hãy build REST API hoàn chỉnh với JWT!