The Request Lifecycle
Every web hit begins at the “front controller,” the public/index.php file that bootstraps your app. It builds a Request object from the incoming HTTP message, loads your configuration, and passes control to the framework kernel. The kernel consults your route definitions to decide which controller method should handle the request. Before the controller runs, any “before” filters you attached to that route can short-circuit the flow (for example, by redirecting unauthenticated users). The controller then orchestrates business logic: reading input from $this->request, delegating to models or services, and preparing a Response. The response might be a rendered view, a JSON payload, or a file download. After the controller returns, any “after” filters can add headers or perform logging, and finally the framework sends the response back to the client.
Routing
Routing in CodeIgniter 4 is deliberately simple. You define routes in app/Config/Routes.php, mapping HTTP verbs and paths to controller methods. Place more specific rules toward the top so they match first. Route placeholders like (:num) and (:segment) extract path parts and pass them as parameters. Here’s a tiny slice of real routes:
$routes->get('/', 'Home::index');
Controllers
Controllers live under app/Controllers and typically extend BaseController. That base class wires in handy properties like $this->request, $this->response, and helpers. A controller’s job is to coordinate: validate input, call models or services, and return a response. Returning a view is as simple as return view(‘users/index’, [‘users’ => $users]);. Returning JSON is equally direct with $this->response->setJSON($data).
namespace App\Controllers;
class Home extends BaseController
{
public function index(): string
{
return 'Hello world!';
}
}
Views
Views are plain PHP templates located in app/Views. CodeIgniter favors simple, readable templates and a lightweight layout system. You can define a base layout and then “extend” it from individual views, injecting your page content into named sections. Always escape untrusted data with esc() to prevent XSS.
// app/Views/layouts/main.php
html>
<html>
<head><title> esc($title ?? 'My App') ?></title></head>
<body> $this->renderSection('content') ?></body>
</html>
// app/Views/users/index.php
$this->extend('layouts/main') ?>
$this->section('content') ?>
<h1>Users</h1>
<ul>
foreach ($users as $u): ?>
<li> esc($u['name']) ?></li>
endforeach ?>
</ul>
$this->endSection() ?>
Models
For database access you can use the Query Builder directly or subclass CodeIgniter\Model. A model class centralizes table configuration, allowed fields, and timestamp behavior, and gives you expressive methods like find(), findAll(), where(), and save(). Create and connect the database first by editing the env file.
namespace App\Models;
use CodeIgniter\Model;
class UserModel extends Model
{
protected $table = 'users';
protected $primaryKey = 'id';
protected $allowedFields = ['name', 'email'];
protected $useTimestamps = true;
}
The instance of this class can now be created in a controller, which greatly simplifies database access.
Filters
Filters are CodeIgniter’s middleware. You register them in app/Config/Filters.php and attach them globally or per route. A “before” filter might verify a session or a JWT and redirect if authentication fails; an “after” filter might append security headers or log audit data. CSRF protection, rate limiting, and role-based access are natural fits for filters. Here’s how you might protect an admin area:
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class AuthFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
if (! session('isLoggedIn')) {
return redirect()->to('/login');
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
}
}
$routes->get('admin/dashboard', 'Admin\Dashboard::index', ['filter' => 'auth']);
Other things
Beyond the basics, CI4 ships handy pieces that cover daily needs: Helpers (url, text, form) you load on demand; Services for validation, email, cache, session, and encryption; Migrations and Seeders to keep schema and sample data in code; the Query Builder and Entities for clean database work; an Events system to decouple side effects; CSRF, output escaping, and filters for security; Localization via language files; file uploads and a small image library; the Debug Toolbar for queries/timeline; and a testing package with HTTP feature tests and fake services. Spark (the CLI) ties it together for scaffolding, migrations, and cache tasks — getting you productive without pulling in extra libraries.
Conclusion
CodeIgniter 4 keeps PHP development simple, predictable, and quick. Every HTTP request passes the same flow — front controller → routing → filters → controller → model/query → view/response — so you always know where to look and how to fix things. Services unify dependencies, migrations keep your database honest, and Spark makes common tasks one command away. The trade-off for that speed and clarity is a smaller ecosystem and fewer out-of-the-box “big framework” features. If your project values control, performance, and low overhead, CI4 is still one of the most effective ways to ship reliable PHP software without getting in your way.
References:
Electronic counseling applications using framework code igniter — Scientific Figure on ResearchGate. Available from: https://www.researchgate.net/figure/Code-igniter-workflow-From-Figure-4-about-the-work-system-Code-Igniter-can-be-explained_fig4_349678000 [accessed 2 Sept 2025]