📖 CodeIgniter 4 Framework - REST API với CodeIgniter
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!

📝 Bài tập (1)

  1. Build complete REST API