Website Backend Development with PHP (CodeIgniter)
CodeIgniter — the lightest full-featured PHP framework. CodeIgniter 4 is completely rewritten for PHP 8, gained PSR compatibility, built-in ORM (Query Builder + Model), HTTP layer through PSR-7, and maintained its main advantage: minimal overhead, simple documentation, no magic.
Right scenarios for CI4: small and medium-sized websites, hosting with restrictions (shared hosting, legacy infrastructure), teams with CI experience, projects with strict performance requirements on weak hardware.
Structure and Routing
// app/Config/Routes.php
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api\V1'], function ($routes) {
// Auth (public)
$routes->post('auth/login', 'AuthController::login');
$routes->post('auth/refresh', 'AuthController::refresh');
// Protected via filter
$routes->group('', ['filter' => 'jwt'], function ($routes) {
$routes->get('profile', 'UserController::profile');
$routes->resource('products', ['controller' => 'ProductController']);
// Generates: GET /, GET /:id, POST /, PUT /:id, DELETE /:id
});
// Admin-only
$routes->group('admin', ['filter' => 'jwt:admin'], function ($routes) {
$routes->resource('users', ['controller' => 'Admin\UserController']);
});
});
Controllers
namespace App\Controllers\Api\V1;
use App\Controllers\BaseController;
use App\Models\ProductModel;
use CodeIgniter\HTTP\ResponseInterface;
class ProductController extends BaseController
{
private ProductModel $productModel;
public function __construct()
{
$this->productModel = new ProductModel();
}
public function index(): ResponseInterface
{
$page = (int) ($this->request->getGet('page') ?? 1);
$limit = min((int) ($this->request->getGet('limit') ?? 20), 100);
$query = $this->productModel
->where('is_active', 1)
->orderBy('created_at', 'DESC');
if ($categoryId = $this->request->getGet('category_id')) {
$query->where('category_id', (int) $categoryId);
}
$total = $query->countAllResults(false);
$products = $query->paginate($limit, 'default', $page);
return $this->response->setJSON([
'data' => $products,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => (int) ceil($total / $limit),
],
]);
}
public function create(): ResponseInterface
{
$data = $this->request->getJSON(true);
if (!$this->validate($this->productModel->getValidationRules())) {
return $this->response->setStatusCode(422)->setJSON([
'errors' => $this->validator->getErrors(),
]);
}
$id = $this->productModel->insert($data);
$product = $this->productModel->find($id);
return $this->response->setStatusCode(201)->setJSON($product);
}
}
Model and Query Builder
namespace App\Models;
use CodeIgniter\Model;
class ProductModel extends Model
{
protected $table = 'products';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useSoftDeletes = true;
protected $useTimestamps = true;
protected $allowedFields = ['name', 'slug', 'price', 'category_id', 'description', 'is_active', 'attributes'];
protected $validationRules = [
'name' => 'required|min_length[2]|max_length[255]',
'price' => 'required|decimal|greater_than[0]',
'category_id' => 'permit_empty|integer|is_not_unique[categories.id]',
];
protected $validationMessages = [
'name' => ['required' => 'Name is required'],
'price' => ['greater_than' => 'Price must be greater than zero'],
];
protected $beforeInsert = ['generateSlug'];
protected $beforeUpdate = ['generateSlug'];
protected function generateSlug(array $data): array
{
if (isset($data['data']['name']) && empty($data['data']['slug'])) {
$data['data']['slug'] = url_title($data['data']['name'], '-', true);
}
return $data;
}
// Custom methods
public function getActiveByCategory(int $categoryId, int $limit = 20, int $offset = 0): array
{
return $this->where('category_id', $categoryId)
->where('is_active', 1)
->orderBy('created_at', 'DESC')
->findAll($limit, $offset);
}
public function getWithCategory(): array
{
return $this->db->table($this->table . ' p')
->select('p.*, c.name as category_name')
->join('categories c', 'c.id = p.category_id', 'left')
->where('p.is_active', 1)
->get()
->getResultArray();
}
}
JWT Authentication Filter
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
$authHeader = $request->getHeaderLine('Authorization');
if (!str_starts_with($authHeader, 'Bearer ')) {
return service('response')->setStatusCode(401)->setJSON(['error' => 'Unauthorized']);
}
try {
$token = substr($authHeader, 7);
$payload = JWT::decode($token, new Key(getenv('JWT_SECRET'), 'HS256'));
// Check role if passed in arguments
if ($arguments && !in_array($payload->role, $arguments)) {
return service('response')->setStatusCode(403)->setJSON(['error' => 'Forbidden']);
}
// Save in request for controllers
$request->user = $payload;
} catch (\Exception $e) {
return service('response')->setStatusCode(401)->setJSON(['error' => 'Invalid token']);
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) {}
}
Caching
// Via cache() helper
$cacheKey = "products_cat_{$categoryId}_page_{$page}";
$products = cache($cacheKey);
if ($products === null) {
$products = $this->productModel->getActiveByCategory($categoryId, 20, ($page - 1) * 20);
cache()->save($cacheKey, $products, 300); // 5 minutes
}
// Tagged cache — Redis driver only
$cache = \Config\Services::cache();
$cache->save("product_{$id}", $product, 3600, ['products', "category_{$product['category_id']}"]);
// Invalidate by tag
$cache->deleteMatching('products*'); // wildcard for Redis
File Upload
public function upload(): ResponseInterface
{
$file = $this->request->getFile('image');
if (!$file->isValid()) {
return $this->response->setStatusCode(400)->setJSON(['error' => $file->getErrorString()]);
}
$rules = [
'image' => 'uploaded[image]|max_size[image,10240]|is_image[image]|mime_in[image,image/jpg,image/jpeg,image/png,image/webp]',
];
if (!$this->validate($rules)) {
return $this->response->setStatusCode(422)->setJSON(['errors' => $this->validator->getErrors()]);
}
$newName = $file->getRandomName();
$file->move(WRITEPATH . 'uploads', $newName);
// Or to S3 via custom storage
$url = $this->uploadToS3($file->getTempName(), $newName);
return $this->response->setJSON(['url' => $url]);
}
Development Timeline
CodeIgniter 4 starts quickly:
- Configuration + models + migrations — 2–4 days
- Routes + controllers + auth — 1–1.5 weeks
- Business logic — 1–3 weeks
- Tests (phpunit + CI Test Tools) — 3–5 days
Small or medium website: 3–7 weeks. CI4 is a pragmatic choice when you need a working backend without extra dependencies and a high learning curve.







