Top 100 CodeIgniter Interview Questions
Top 100 CodeIgniter interview questions covering MVC, routing, models, libraries, CI4 features, security, testing, and database operations.
CodeIgniter is a powerful, lightweight PHP framework for developing web applications. Created by EllisLab and first released in 2006, it is now maintained by the British Columbia Institute of Technology (BCIT). CodeIgniter follows the MVC (Model-View-Controller) pattern and is known for its small footprint, excellent performance, zero configuration needed to get started, and a very short learning curve. CodeIgniter 4 (CI4), released in 2020, was a complete rewrite with modern PHP features: namespaces, PSR compliance, type declarations, dependency injection, and a proper ORM-like model. It is an excellent choice for developers who want a simple, fast framework without the complexity of Laravel or Symfony.
CodeIgniter follows the MVC (Model-View-Controller) architectural pattern. The Model handles data access — it interacts with the database and contains data-related logic. The View contains the HTML presentation layer — what the user sees. The Controller is the glue — it receives HTTP requests, calls models for data, passes data to views, and returns responses. In CodeIgniter, the URL structure maps directly to controllers and methods: /blog/index calls the index() method of the Blog controller. The flow: Client → Router → Controller → Model → View → Controller → Response. This separation makes code organized, reusable, and testable, especially for large applications.
CodeIgniter 4 has a clean directory structure: app/ (your application code — Controllers, Models, Views, Config, Filters, Libraries), public/ (web root — index.php, CSS, JavaScript, images — the only publicly accessible folder), system/ (the CI4 framework core — do not modify), writable/ (logs, cache, sessions, uploads — must be writable by the web server), tests/ (test files), vendor/ (Composer dependencies). The app/Config/ directory contains all configuration files as PHP classes. The app/Views/ directory contains view templates. Unlike older frameworks, CI4 separates the web root from the application code — the application never lives inside the public folder, improving security.
In CodeIgniter 4, a Controller handles HTTP requests and returns responses. Controllers are stored in app/Controllers/ and extend CodeIgniter\Controller (or BaseController for common functionality). Example: namespace App\Controllers; class Blog extends BaseController { public function index() { return view("blog/index"); } }. Access request data with $this->request->getPost("field"), getGet(), or getVar(). Return a view: return view("viewname", $data). Return JSON: return $this->response->setJSON($data). The BaseController in app/Controllers/BaseController.php is where you initialize helpers, libraries, and properties shared by all controllers. Every public method can be a routable action.
CodeIgniter 4 ships with a powerful Model class that provides a rich set of database interaction methods. Extend CodeIgniter\Model and configure the model with properties: protected $table = "users", protected $primaryKey = "id", protected $allowedFields = ["name", "email", "password"], protected $useTimestamps = true. The model provides: find($id), findAll(), where("active", 1)->findAll(), insert($data), update($id, $data), delete($id). It also has built-in validation via the $validationRules property and business rule callbacks (beforeInsert, afterFind, etc.). CI4 models are lighter than Laravel's Eloquent but sufficient for most use cases.
Views in CodeIgniter are simple PHP files stored in app/Views/ that contain HTML mixed with PHP. Load a view from a controller: return view("blog/index", ["title" => "My Blog", "posts" => $posts]). The second argument passes data as an array — these become variables in the view ($title, $posts). Views can include other views: <?= view("partials/header") ?>. Unlike Laravel's Blade, CI4 does not have a dedicated templating language by default — views are plain PHP. However, CI4 does include a simple View Parser for pseudo-variables ({title}), conditionals, and loops without PHP tags. For richer templating, third-party libraries or Twig can be integrated.
CodeIgniter 4 routing is defined in app/Config/Routes.php. Routes map URL patterns to controller methods: $routes->get("users", "UserController::index"). HTTP verb methods: get(), post(), put(), patch(), delete(), match(). URL parameters: $routes->get("users/(:num)", "UserController::show/$1") — (:num), (:alpha), (:alphanum), (:any), (:segment) are built-in placeholders. Named routes: $routes->get("users", "UserController::index", ["as" => "users.index"]). Generate URL by name: route_to("users.index"). CI4's auto-routing (off by default) can map URLs directly to controllers without explicit route definitions — but explicit routes are safer and recommended.
The base_url() function returns the base URL of your application as configured in app/Config/App.php ($baseURL). Example: if your app is at https://example.com, then base_url("assets/css/style.css") returns https://example.com/assets/css/style.css. Use it in views for links and asset URLs: <link href="<?= base_url("css/style.css") ?>">. Related helper: site_url() prepends the index file if used. current_url() returns the full URL of the current page. previous_url() returns the referring URL. Always use these helpers rather than hardcoding URLs, so the app works regardless of where it is deployed (subdirectory, different domain, etc.).
Helpers in CodeIgniter are collections of standalone PHP functions (not classes) that assist with specific tasks. They are organized by type and must be loaded before use. Load a helper: helper("url") or load multiple: helper(["url", "form", "text"]). Built-in helpers: url (URL-related functions like base_url(), redirect()), form (HTML form generation), text (text manipulation), html (link and img tags), date (date formatting), file (file-related functions), array (array utilities), security (xss_clean()), cookie. Load helpers globally in app/Config/Autoload.php under the $helpers array, or per-controller in the constructor. You can also create custom helpers in app/Helpers/.
Libraries in CodeIgniter (especially CI3) are classes that provide functionality beyond what helpers offer. In CI3, you load them with $this->load->library("email") then use $this->email->send(). Common built-in libraries: Email (send emails), Upload (file uploads), Session (session management), Encryption (encrypt/decrypt), Pagination, Form_validation, Image_lib (image manipulation), Cart (shopping cart), Cache. In CodeIgniter 4, the concept of libraries changed — functionality is now accessed through services: $email = \Config\Services::email() or service("email"). You can also create custom libraries in app/Libraries/.
CodeIgniter 4 uses PSR-4 autoloading via Composer for class loading. Define your app namespace in composer.json: "App\\": "app/" — then any class in app/ is automatically available as App\Controllers\UserController. CI4's own Config/Autoload.php configures: $psr4 (namespace to directory mapping), $classmap (explicit class path mapping), and $files (files to always include). Helpers and libraries specified in $helpers and $libraries are autoloaded globally. Run composer dump-autoload to regenerate the classmap. The CodeIgniter class locator also supports loading classes from multiple namespaces (allowing modules to override core classes). This modern autoloading approach replaced CI3's manual $this->load->library() pattern.
CodeIgniter 4 provides a robust Session library supporting multiple drivers: files (default), database, redis, and memcached. Configure in app/Config/Session.php. Get the session service: $session = service("session") or use the global session() function. Set data: $session->set("user_id", 1) or $session->set(["user_id" => 1, "role" => "admin"]). Get data: $session->get("user_id"). Check: $session->has("user_id"). Remove: $session->remove("user_id"). Flashdata (available only for the next request): $session->setFlashdata("msg", "Saved!"), $session->getFlashdata("msg"). Tempdata (flashdata with custom expiry in seconds): $session->setTempdata("item", "value", 300).
CodeIgniter 4 provides a flexible Validation class. Get the service: $validation = \Config\Services::validation(). Set rules: $validation->setRules(["username" => "required|min_length[5]|max_length[50]", "email" => "required|valid_email"]). Run validation: if (!$validation->withRequest($this->request)->run()) { return redirect()->back()->withInput()->with("errors", $validation->getErrors()); }. Built-in rules: required, min_length[n], max_length[n], exact_length[n], valid_email, valid_url, alpha, alpha_numeric, numeric, integer, is_unique[table.field], differs[field]. Custom error messages: pass as third parameter to setRules(). In CI4 models, define $validationRules property for automatic validation on insert/update.
CodeIgniter 4 provides a Query Builder (formerly Active Record) for fluent database interaction. Get a database connection: $db = \Config\Database::connect() or through a model. Build queries fluently: $db->table("users")->select("id, name")->where("active", 1)->orderBy("name")->get(). The get() method returns a Result object — fetch results with getResultArray(), getResult() (objects), getRow() (first row). Insert: $db->table("users")->insert($data). Update: $db->table("users")->where("id", $id)->update($data). Delete: $db->table("users")->where("id", $id)->delete(). Raw queries: $db->query("SELECT * FROM users WHERE id = ?", [$id]). Configure connections in app/Config/Database.php.
CodeIgniter 4 includes a built-in Pager class for pagination. In a model, use paginate() directly: $users = $this->userModel->paginate(15) — this returns the current page's data. Get the pager: $pager = $this->userModel->pager. In the view: <?= $pager->links() ?> renders navigation links. The current page is automatically read from the page query parameter in the URL. Customize templates in app/Views/Pager/. For non-model pagination (manual queries): $pager = service("pager"); $pager->makeLinks($currentPage, $perPage, $total). CI4's Pager generates clean Bootstrap-compatible pagination HTML and supports multiple pager groups per page for paginating multiple datasets simultaneously.
CodeIgniter 4's Email class sends emails via SMTP, PHP's mail(), or Sendmail. Get the service: $email = \Config\Services::email(). Configure in app/Config/Email.php or at runtime. Usage: $email->setFrom("from@example.com", "My App")->setTo("to@example.com")->setSubject("Hello")->setMessage("<p>Body</p>")->setMailType("html")->send(). Attachments: $email->attach("/path/to/file.pdf"). CC and BCC: $email->setCC("cc@example.com"). For SMTP, configure $protocol = "smtp", $SMTPHost, $SMTPPort, $SMTPUser, $SMTPPass, and $SMTPCrypto = "tls". Debug email issues: $email->printDebugger(["headers", "subject", "body"]). Consider using a third-party service (Mailgun, SendGrid) with Guzzle for production reliability.
CodeIgniter 4 provides built-in CSRF protection via the Security filter. Enable in app/Config/Security.php: $csrfProtection = "cookie" (or "session"). Apply the Security filter globally in app/Config/Filters.php or per route. In HTML forms, include the CSRF field: <?= csrf_field() ?> — generates a hidden input with the token. Or manually: <input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">. For AJAX requests, include the token in headers or request body. If validation fails, a 403 response is returned. Exclude specific routes from CSRF: add them to the $except array in the Security configuration (useful for webhook endpoints that receive external POST requests).
CodeIgniter 4 handles file uploads through the Files abstraction in the Request object. Get uploaded file: $file = $this->request->getFile("avatar"). Validate: if (!$file->isValid() || $file->hasMoved()) { return; }. Move to destination: $file->move(WRITEPATH . "uploads", $file->getRandomName()). File information: $file->getName(), $file->getSize(), $file->getMimeType(), $file->guessExtension(). Security: use guessExtension() instead of trusting the client-provided extension, validate MIME type, check file size, and use getRandomName() to generate safe filenames. For multiple file uploads: $files = $this->request->getFiles(). CI4's file handling is simpler and cleaner than CI3's Upload library.
Filters in CodeIgniter 4 are the equivalent of middleware in Laravel — they run before or after a controller executes. Define a filter by implementing CodeIgniter\Filters\FilterInterface with before() and after() methods. Register filters in app/Config/Filters.php: add to $aliases and assign to $globals (run on every request), $methods (run for specific HTTP methods), or $filters (run for specific URIs). Apply to routes: $routes->get("admin/dashboard", "Admin::index", ["filter" => "auth"]). Built-in filters: csrf (CSRF protection), honeypot (spam protection), invalidchars, forcehttps. The before() method can return a Response to short-circuit the request, and the after() method can modify the response before it is sent.
Services in CodeIgniter 4 provide a lightweight dependency injection mechanism and a central registry for framework components. The Config\Services class defines factory methods for all framework services. Access a service: $email = service("email") or \Config\Services::email(). Services return either a shared instance (singleton) with service("name", false) or a new instance. Key services: router, request, response, session, email, validation, logger, cache, encryption, pager. You can override any service by creating a Services class in app/Config/ that extends the system one — this is how you swap framework components with custom implementations without touching the core code.
Spark is CodeIgniter 4's command-line tool, similar to Laravel's Artisan. Run commands with php spark <command>. Built-in commands: php spark serve (start dev server on port 8080), php spark migrate (run migrations), php spark db:seed SeedName (run a seeder), php spark make:controller UserController (generate a controller), php spark make:model UserModel (generate a model), php spark make:filter AuthFilter (generate a filter), php spark make:seeder UserSeeder. List all commands: php spark list. Create custom Spark commands by extending CodeIgniter\CLI\BaseCommand and placing the file in app/Commands/. Custom commands must define $group, $name, $description, and a run() method.
CodeIgniter 4 is a complete rewrite of CI3 with modern PHP practices. Key differences: CI4 requires PHP 7.4+ (CI3 supported PHP 5.6+). CI4 uses namespaces and PSR-4 autoloading (CI3 used a custom autoloader without namespaces). CI4 has a proper HTTP layer with Request/Response objects (CI3 had a single Input class). CI4 has built-in migrations and seeders (CI3 had a simpler migrations library). CI4 has a Model class with validation and callbacks (CI3 models were bare). CI4 uses Filters instead of CI3's Hooks. CI4 has first-class CLI support via Spark. CI4 is Composer-first (CI3 was typically downloaded as a zip). CI4 supports dependency injection via Services. The application code structure changed significantly — CI3 apps cannot be directly migrated to CI4.
The Security class in CodeIgniter 4 handles various security features. Get it: $security = service("security"). CSRF: generates and validates CSRF tokens for form protection (csrf_token(), csrf_hash(), csrf_field() helpers). XSS filtering: $security->sanitizeFilename($filename) cleans filenames for safe use. Input sanitization: use $request->getVar("field", FILTER_SANITIZE_STRING) or PHP's filter functions. The esc($value, "html") function (or its alias h()) escapes output for HTML — use it in views instead of xss_clean(). Configure CSRF options in app/Config/Security.php: token name, cookie settings, regeneration policy, and which routes to exclude. Always escape output at display time rather than sanitizing at input time.
CodeIgniter 4 has a built-in Migration system for version-controlling your database schema. Create a migration: php spark make:migration CreateUsersTable. Migrations live in app/Database/Migrations/ with timestamps in the filename. Each migration has up() (apply changes) and down() (rollback) methods using the Forge class: $this->forge->addField(["id" => ["type" => "INT", "auto_increment" => true], "name" => ["type" => "VARCHAR", "constraint" => 100]])->addKey("id", true)->createTable("users"). Run: php spark migrate. Rollback: php spark migrate:rollback. Status: php spark migrate:status. Refresh (rollback all and re-run): php spark migrate:refresh. Migration state is stored in the migrations table.
Seeders in CodeIgniter 4 populate your database with test or initial data. Create: php spark make:seeder UserSeeder. Seeders live in app/Database/Seeds/. The run() method inserts data: $this->db->table("users")->insert(["name" => "Admin", "email" => "admin@example.com"]). Run a seeder: php spark db:seed UserSeeder. Call other seeders from within a seeder: $this->call("CategorySeeder"). For large amounts of test data, use Faker (install via Composer): $faker = \Faker\Factory::create(); for ($i = 0; $i < 50; $i++) { $this->db->table("users")->insert(["name" => $faker->name, "email" => $faker->email]); }. Seeders are used in development and testing — never run production seeders without careful review.
CodeIgniter 4's Validation service validates data from any source. Get the service: $validation = service("validation"). Method 1 — validate request directly: if (!$this->validate(["email" => "required|valid_email"])). Method 2 — validate any array: $validation->run(["email" => $email], "userRules") using rules defined in app/Config/Validation.php. Get errors: $this->validator->getErrors() or single error: $this->validator->getError("email"). In views: <?= validation_show_error("email") ?>. Custom rules: create a class with the rule as a method and register in Config/Validation.php under $ruleSets. Rule groups in config allow reusing the same rules in multiple controllers. CI4's validation integrates directly with models via $validationRules.
A Entity in CodeIgniter 4 is a class that represents a row of data, similar to an ORM model. Extend CodeIgniter\Entity\Entity. Entities provide automatic casting via the $casts property: protected $casts = ["is_active" => "boolean", "options" => "json", "created_at" => "datetime"] — values are automatically converted when set or retrieved. You can also define getter and setter methods using the getPropertyName() / setPropertyName() convention for mutators. Access properties: $user->name, $user->created_at->format("Y-m-d"). Link a model to an entity: protected $returnType = "App\Entities\User" in the model — all queries now return entity objects. Entities bring a cleaner domain model without the full weight of an ORM.
CodeIgniter 4 uses PHP class-based configuration — each config file in app/Config/ is a PHP class extending CodeIgniter\Config\BaseConfig with public properties. Example: class App extends BaseConfig { public string $baseURL = "http://localhost:8080"; }. Load config: $config = config("App") — returns the singleton instance. Access properties: config("App")->baseURL. Environment overrides: values can be overridden by environment variables following the pattern app.baseURL → APP_BASEURL in the .env file. This eliminates the need to change PHP config files per environment. The .env file supports all config keys in dot notation. Spark commands can also read config values. CI4's approach is more type-safe and IDE-friendly than CI3's array-based configuration.
The Response object in CodeIgniter 4 provides a clean API for building HTTP responses. Access via $this->response in controllers. Set body: $this->response->setBody($html). Set status code: $this->response->setStatusCode(404). Set content type: $this->response->setContentType("application/json"). Set headers: $this->response->setHeader("X-Custom", "value"). Return JSON: return $this->response->setJSON($data) — automatically sets the content type and encodes the array. Return HTML view (shorthand): return view("viewname", $data). Redirect: return redirect()->to("url") or redirect()->route("routename"). Set cookies: $this->response->setCookie("name", "value", 3600). Download: $this->response->download("filename.pdf", $data).
The Request object provides access to all incoming HTTP request data. Access in controllers via $this->request. GET parameters: $this->request->getGet("name"). POST parameters: $this->request->getPost("name"). Either (GET or POST): $this->request->getVar("name"). All POST data: $this->request->getPost(). JSON body: $this->request->getJSON() (for API endpoints receiving JSON). Request method: $this->request->getMethod(). Check method: $this->request->is("post"). IP address: $this->request->getIPAddress(). User agent: $this->request->getUserAgent(). Headers: $this->request->getHeader("Accept"). Files: $this->request->getFile("avatar"). Server data: $this->request->getServer("SERVER_NAME"). Apply filters to input: $this->request->getPost("name", FILTER_SANITIZE_STRING).
Both CI4 and Laravel define routes in a dedicated routes file, but they differ in several ways. CI4 routes are defined in app/Config/Routes.php using $routes->get(), $routes->post() etc. CI4 also supports optional auto-routing (disabled by default) where URLs automatically map to controllers/methods. CI4 route parameters use (:num), (:alpha) placeholders. Laravel routes are in routes/web.php and routes/api.php. Laravel has more built-in features: route model binding, resource routes, subdomain routing, rate limiting middleware, signed URLs, and more fluent API. Both support route grouping, naming, and middleware. Laravel's routing is generally considered more powerful and feature-rich, while CI4's routing is simpler and has lower overhead.
The Honeypot feature in CodeIgniter 4 is a simple spam protection mechanism that adds a hidden form field that regular users cannot see (hidden with CSS) but spam bots often fill in automatically. Configure in app/Config/Honeypot.php: set $hidden = true to enable. Add to forms: <?= \CodeIgniter\Honeypot\Honeypot::insertHoneypot() ?> or use the Honeypot filter. When the hidden field is submitted with a value (i.e., a bot filled it), the request is rejected with a 400 error. Enable as a global filter in app/Config/Filters.php. The Honeypot is not foolproof against all bots but significantly reduces automated spam submissions without adding any UX friction for real users. Combine with CAPTCHA for stronger bot protection.
CodeIgniter 4's Events system allows executing code at specific points during framework execution. Define events in app/Config/Events.php: Events::on("pre_controller", function() { /* runs before controller */ }). Built-in events: pre_system (very early bootstrap), pre_controller (after routing, before controller), post_controller_constructor (after controller instantiation), post_controller (after controller method), pre_models, post_models. Events allow you to hook into the framework lifecycle without modifying core files — useful for logging, analytics, performance monitoring, or global setup. CI4 events replaced CI3's Hooks system. Each event point can have multiple listeners. Unlike CI3 hooks, CI4 events are simpler and more Pythonic in their design.
CodeIgniter 4 has a PSR-3 compatible logging system. Configure in app/Config/Logger.php. Default log levels (in order of severity): emergency, alert, critical, error, warning, notice, info, debug. Set minimum threshold: $threshold = 4 (only log warnings and above). Log a message: log_message("error", "Something went wrong: {message}", ["message" => $e->getMessage()]). Or via the service: service("logger")->warning("Low disk space"). Handlers: FileHandler (logs to writable/logs/), ErrorlogHandler (PHP error log), or custom handlers implementing the PSR-3 LoggerInterface. Use the debug toolbar to view logs during development. In production, set a higher threshold to avoid writing excessive logs.
The Query Builder (formerly Active Record in CI3) is CI4's fluent interface for constructing database queries. Access from a model: $this->db or $db = \Config\Database::connect(). Methods: $db->table("users")->select("name, email")->where("active", 1)->orWhere("role", "admin")->whereIn("department", [1, 2])->like("name", "Al")->orderBy("name", "ASC")->limit(10, 20)->get(). Join: ->join("profiles", "profiles.user_id = users.id", "left"). Group by: ->groupBy("department")->having("count(*) >", 5). Insert: $db->table("users")->insert($data). Batch insert: ->insertBatch($rows). Update: ->where("id", $id)->update($data). The Query Builder automatically escapes values, preventing SQL injection. Unlike Eloquent, it does not return objects by default — use getResultArray() for arrays or getResult() for objects.
CI4 models can validate data automatically before insert/update. Define rules in the model: protected $validationRules = ["name" => "required|min_length[3]", "email" => "required|valid_email|is_unique[users.email]"]. Custom messages: protected $validationMessages = ["email" => ["is_unique" => "This email is already registered"]]. Skip validation for specific operations: $this->userModel->skipValidation(true)->save($data). Call validation manually: if (!$this->userModel->validate($data)) { $errors = $this->userModel->errors(); }. The save() method runs validation automatically — it returns false on failure. For update, the is_unique rule needs to ignore the current record: use placeholders: "is_unique[users.email,id,{id}]" where {id} is a placeholder replaced with the actual value at runtime. This keeps validation logic close to the model.
The Database Forge class provides a database-agnostic API for creating, modifying, and dropping database tables — used primarily in migrations. Get Forge: $forge = \Config\Database::forge(). Create a table: $forge->addField(["id" => ["type" => "INT", "constraint" => 11, "unsigned" => true, "auto_increment" => true], "username" => ["type" => "VARCHAR", "constraint" => 100], "created_at" => ["type" => "DATETIME", "null" => true]])->addKey("id", true)->createTable("users"). Add a column: $forge->addColumn("users", ["phone" => ["type" => "VARCHAR", "constraint" => 20, "null" => true]]). Modify column: $forge->modifyColumn(). Drop column: $forge->dropColumn(). Drop table: $forge->dropTable("tablename"). Rename table: $forge->renameTable("old", "new"). Forge generates the appropriate SQL for the active database driver (MySQL, PostgreSQL, SQLite).
CodeIgniter 4 provides a unified caching API supporting multiple drivers. Configure in app/Config/Cache.php: set $handler = "redis" (or file, memcached, wincache, dummy). Get the cache service: $cache = service("cache"). Save: $cache->save("key", $data, 3600) (TTL in seconds). Get: $cache->get("key") — returns null if not found or expired. Delete: $cache->delete("key"). Clean all: $cache->clean(). Check if cached: $cache->getCacheInfo("key"). Common pattern: if (!$data = $cache->get("key")) { $data = $this->model->getExpensiveData(); $cache->save("key", $data, 3600); }. Cache entire page output with CI4's Page Cache: $this->cachePage(300) in a controller caches the full HTML response for 300 seconds.
CodeIgniter 4 has built-in support for building RESTful APIs. The ResourceController provides RESTful routing: $routes->resource("users") maps HTTP verbs to controller methods: index() (GET /users), show($id) (GET /users/id), create() (GET /users/new), store() (POST /users), edit($id) (GET /users/id/edit), update($id) (PUT/PATCH /users/id), delete($id) (DELETE /users/id). For APIs (no HTML forms), use $routes->presenter("photos") which excludes create/edit. Return JSON: return $this->response->setJSON(["status" => "success", "data" => $users]). The Content Negotiation feature automatically formats responses based on the Accept header. Use Filters for authentication (JWT tokens, API keys) before resource controllers.
CodeIgniter 4 includes a built-in HTTP Client (based on cURL) for making outgoing HTTP requests to external APIs. Get it: $client = service("curlrequest"). Make a GET request: $response = $client->get("https://api.example.com/users"). POST with JSON: $client->post($url, ["json" => $data]). With headers: $client->get($url, ["headers" => ["Authorization" => "Bearer " . $token]]). Access response: $response->getStatusCode(), $response->getBody(), $response->getJSON(). Options: timeout, connect_timeout, allow_redirects, verify (SSL). CI4's HTTP client is simpler than Guzzle but sufficient for most API calls. For complex scenarios (connection pooling, async requests), use Guzzle via Composer.
CodeIgniter 4 supports a modular application structure where related controllers, models, views, and config are grouped together. Using CodeIgniter Modules (available via the codeigniter4/shield approach or third-party like myth/auth), each module is a self-contained namespace. Configure module locations in app/Config/Autoload.php under $psr4: "Modules\Blog" => ROOTPATH . "modules/Blog". Then create controllers at modules/Blog/Controllers/Blog.php with namespace Modules\Blog\Controllers. Modules improve code organization in large applications, allow code reuse across projects, and enable team members to work on independent parts. Routes for modules are typically defined in a Config/Routes.php inside the module directory. This approach is more manual than Symfony's bundle system but works effectively.
The Debug Toolbar in CI4 is a development tool that appears at the bottom of the page showing detailed performance information. It displays: timeline (how long each part of the request took), database queries (all queries executed, their bindings, and execution time), files loaded, session data, request data (GET, POST, COOKIES, headers), response info, log messages, and routes information. Enable in the .env file: CI_ENVIRONMENT = development. The toolbar automatically appears in development mode. It integrates with CI4's Events system. The Timeline tab is particularly useful for identifying slow parts of the request. The Database tab helps identify N+1 query problems. You can add custom timing markers to the timeline: $this->startTimer("name"); ... $this->stopTimer("name").
Database transactions in CodeIgniter 4 ensure data integrity for operations that must all succeed or all fail together. Manual control: $db = \Config\Database::connect(); $db->transStart(); $db->table("orders")->insert($orderData); $db->table("inventory")->where("id", $itemId)->decrement("stock", $qty); if ($db->transStatus() === false) { $db->transRollback(); } else { $db->transComplete(); }. Automatic transactions (throw exception on failure): $db->transException(true)->transStart(); ... $db->transComplete(). Strict mode (enabled by default): if one query fails, all subsequent queries in the transaction fail too. Disable strict: $db->transStrict(false). Nested transactions use a counter — the outer transComplete() is the actual commit. Transactions are essential for financial operations, inventory management, and any multi-table writes that must be atomic.
CodeIgniter 4 has a solid built-in testing infrastructure built on PHPUnit. The CIUnitTestCase base class provides CI-specific helpers. Feature tests simulate HTTP requests without starting a real server: $result = $this->call("get", "/users") — test the response: $result->assertStatus(200)->assertSee("Alice")->assertHeaderEmitted("Content-Type", "application/json"). Database tests: use the DatabaseTestTrait with $seed = "UserSeeder" to populate data. Check database state: $this->seeInDatabase("users", ["email" => "alice@example.com"]). Session tests: $result->assertSessionHas("user_id"). Mock services: Services::injectMock("email", $mockEmail). Run tests: vendor/bin/phpunit or php spark test. Test files go in tests/.
Content Negotiation in CI4 allows the application to respond to a request in the format the client prefers, based on the Accept header. Use the negotiate service: $negotiate = service("negotiator"); $format = $negotiate->media(["application/json", "text/html"]) — returns the preferred format the client accepts. Negotiate language: $negotiate->language(["en", "fr", "de"]). Negotiate encoding, charset, and language similarly. This is particularly useful for building APIs that serve both JSON (for API clients) and HTML (for browsers) from the same controller. CI4 also provides a CodeIgniter\API\ResponseTrait for API controllers that includes helpers like $this->respond($data), $this->failNotFound("User not found"), $this->failValidationErrors($errors), and $this->respondCreated($data).
CodeIgniter 4's Encryption service provides authenticated symmetric encryption using OpenSSL (AES-256-CTR with HMAC-SHA512). Configure the key in app/Config/Encryption.php or generate one: php spark key:generate (stored in .env as encryption.key). Get the service: $encrypter = service("encrypter"). Encrypt: $encrypted = $encrypter->encrypt("sensitive data") — returns a base64-encoded string. Decrypt: $original = $encrypter->decrypt($encrypted). The encryption includes authentication (HMAC) — if the data is tampered with, decryption will fail. Use cases: encrypting sensitive data before storing in a database, securing cookies, or protecting config values. Do not use CI4's Encryption for passwords — use password_hash() and password_verify() instead.
In CodeIgniter 4, redirect() is the recommended way to perform redirects. It returns a RedirectResponse object: return redirect()->to("https://example.com") or return redirect()->route("users.index"). Pass flash messages: return redirect()->to("/dashboard")->with("success", "Logged in!"). Redirect with old input: return redirect()->back()->withInput(). The redirect() function uses a 302 HTTP status code by default; change with: redirect()->to($url, 301). Using PHP's raw header("Location: $url"); exit bypasses CI4's response object and should be avoided — CI4's redirect properly flushes the response buffer, handles headers already sent, and integrates with the framework's response pipeline. Always return the redirect response from controller methods.
CodeIgniter's Query Builder offers several advantages over raw SQL queries. Security: all values passed to the builder are automatically escaped and bound as parameters, preventing SQL injection without manual prepare()/bind() calls. Database abstraction: the same PHP code works across MySQL, PostgreSQL, SQLite, and MSSQL — the builder generates the appropriate SQL syntax for each database. Readability: $db->table("users")->where("active", 1)->get() is clearer than raw SQL strings, especially for complex queries with joins. Method chaining: build queries incrementally across multiple lines or conditions. Query caching: cache repeated queries. The downside: very complex queries (window functions, CTEs, recursive queries) may still require raw SQL via $db->query() or $db->table()->select(DB::raw(...)).
The BaseController in CodeIgniter 4 (app/Controllers/BaseController.php) is an abstract controller that all other controllers extend. It extends CI4's Controller class and is the perfect place to put shared initialization code. The initController() method is called before any controller method — use it to load helpers, initialize properties, or set up shared services: helper(["url", "form"]), $this->session = service("session"). All controllers that extend BaseController inherit these shared resources. Add common middleware checks here (e.g., check if the user has accepted terms). This avoids duplicating initialization across every controller. Keep the BaseController lean — only put things that every controller needs. Create specialized base controllers for specific sections: AdminBaseController, ApiBaseController.
The ResponseTrait in CI4 (CodeIgniter\API\ResponseTrait) provides convenient methods for building consistent API responses when added to API controllers. Success responses: $this->respond($data, 200) (generic), $this->respondCreated($data) (201), $this->respondDeleted($data) (200), $this->respondNoContent() (204). Error responses: $this->fail("Error message", 400), $this->failNotFound("User not found") (404), $this->failUnauthorized() (401), $this->failForbidden() (403), $this->failValidationErrors($errors) (422), $this->failResourceExists() (409). All methods automatically detect the preferred format (JSON/XML) via Content Negotiation and set appropriate headers. This trait standardizes API responses and is far cleaner than manually building response arrays.
CodeIgniter 4 models support soft deletes — marking records as deleted without removing them from the database. Enable in your model: protected $useSoftDeletes = true and set protected $deletedField = "deleted_at". Add a nullable deleted_at DATETIME column to your migration. Calling $this->model->delete($id) sets deleted_at to the current timestamp. Normal queries (findAll(), where()) automatically exclude soft-deleted records. Include deleted records: $this->model->withDeleted()->findAll(). Show only deleted: $this->model->onlyDeleted()->findAll(). Restore a record: $this->model->delete($id, false) does a hard delete; to restore, manually update deleted_at to null. Soft deletes are useful for audit trails, trash functionality, and accidentally deleted record recovery.
CI4 sessions support four drivers configured in app/Config/Session.php. FileHandler (default): stores session files in writable/session/ — simple but not suitable for multi-server setups. DatabaseHandler: stores sessions in a database table — portable and auditable, configure with $driver = "CodeIgniter\Session\Handlers\DatabaseHandler" and create the sessions table with php spark session:migration. RedisHandler: fastest option for production, supports multiple servers and session expiration natively. MemcachedHandler: similar to Redis. Key settings: $sessionExpiration (seconds of inactivity before expiry), $sessionRegenerateDestroy (whether to delete old session on regeneration), $sessionMatchIP (invalidate session if IP changes — balance between security and usability for mobile users). Use Redis or database sessions in any multi-server production environment.
CodeIgniter 4 provides a centralized exception handling system. By default, all unhandled exceptions are caught by CI4's Exceptions handler and displayed as error pages. In production (CI_ENVIRONMENT = production), generic error pages are shown; in development, full stack traces are displayed. Custom error views are placed in app/Views/errors/html/ — create error_404.php, error_500.php, etc. Throw HTTP errors: throw new \CodeIgniter\Exceptions\PageNotFoundException() → 404, or use the shorthand show_404(). Custom exception classes: extend base exception classes and catch them in controllers or configure them in app/Config/Exceptions.php. The log_exception() parameter controls whether each exception type is logged. Override the handler with a custom class by updating app/Config/Exceptions.php.
CodeIgniter 4 supports Dependency Injection primarily through its Services system and constructor/method injection. Unlike Laravel's fully automatic DI container, CI4 is more explicit. Service injection: inject services through constructor: public function __construct(private UserModel $model, private SessionInterface $session) {} — CI4 will attempt to resolve type-hinted CI4 services automatically. For non-service classes, use the Factories class: Models::UserModel() returns the model singleton. The Services class itself is a simple DI container — register services and their factories there. For true automatic DI (autowiring), CI4 supports it to a limited extent — complex graphs require manual wiring in the Services class. Alternatively, use the CodeIgniter DI container available from the CI4 ecosystem or integrate PHP-DI.
The Factories class in CI4 is a lightweight factory pattern that creates and caches instances of framework components. It is used for Models, Libraries, and other components. Access models: $model = model("UserModel") or Factories::models("UserModel") — both return a shared (cached) instance. The Factories class searches across all registered namespaces for the class, enabling module override patterns where an app-level model replaces a module-level one. Factories::models("UserModel", ["getShared" => false]) creates a fresh instance. The factory supports: Factories::components(), Factories::config(). Use Factories::injectMock("models", "UserModel", $mockModel) in tests to inject mock objects. Compared to Laravel's container, Factories is simpler and less powerful but has lower overhead for CI4's design philosophy.
CodeIgniter 4 made significant strides toward PSR compliance compared to CI3. CI4 conforms to: PSR-1 and PSR-12 (coding standards), PSR-3 (Logger Interface — CI4's logger implements Psr\Log\LoggerInterface), PSR-4 (autoloading — full Composer PSR-4 support), PSR-7 (HTTP Messages — CI4's Request and Response classes are inspired by PSR-7 but are not fully compliant; they do not extend PSR-7 interfaces), PSR-11 (Container Interface — partially supported via the Services system). The PSR-7 non-compliance means CI4 does not natively work with PSR-15 middleware packages. However, CI4's Filters achieve the same goal internally. Third-party adapters exist to bridge CI4 with PSR-7 middleware ecosystems if needed. CI4 uses PSR-3 logging fully, so Monolog handlers can be integrated easily.
For large-scale applications, Laravel and CodeIgniter 4 have different strengths and trade-offs. Laravel advantages: richer ecosystem (Horizon, Telescope, Sanctum, Octane), more powerful DI container with automatic resolution, Eloquent ORM with more relationship types and query power, first-party packages for common needs, better broadcast/WebSocket support, more mature testing utilities, and the Forge/Vapor deployment platforms. CI4 advantages: lower framework overhead (faster cold starts, less memory), simpler learning curve, less magic (more explicit code), better suited for hosting environments without shell access, and smaller codebase to understand. For teams building complex enterprise applications with advanced features, Laravel is usually the better choice. CI4 excels for smaller teams, performance-critical APIs, and applications where simplicity and low overhead are priorities.
CodeIgniter 4 allows creating custom Spark commands for CLI tasks. Generate a command: php spark make:command SyncUsers. Commands live in app/Commands/. Define group, name, description, and arguments: protected $group = "custom"; protected $name = "users:sync"; protected $description = "Sync users from external API"; protected $arguments = ["source" => "The data source to sync from"]; protected $options = ["--dry-run" => "Preview without saving"]. The run(array $params) method contains logic. Access arguments: $params[0]. Check options: CLI::getOption("dry-run"). Output: CLI::write("Done!", "green"), CLI::error("Failed!"), CLI::table($headers, $data), CLI::progressBar(100). Get input: CLI::prompt("Enter name"), CLI::promptByKey("Choose", $options). Useful for data imports, scheduled cleanup tasks, and maintenance operations.
API versioning in CodeIgniter 4 is typically handled through route prefixing and namespacing. Define versioned routes: $routes->group("api/v1", ["namespace" => "App\Controllers\Api\V1"], function($routes) { $routes->resource("users"); }) and $routes->group("api/v2", ["namespace" => "App\Controllers\Api\V2"], function($routes) { $routes->resource("users"); }). Create controllers in corresponding namespace directories: app/Controllers/Api/V1/UserController.php. Each version can have its own controller logic, models, and response format. Alternatively, use headers for versioning (Accept: application/vnd.myapi.v2+json) and handle version selection in a filter or base controller. Share common logic through a base API controller that versioned controllers extend. Consider maintaining only the latest two API versions and deprecating older ones with sunset headers.
Multi-tenancy in CodeIgniter 4 can be implemented using several strategies. Shared database with tenant column: add a tenant_id to all tables. Create a base model that automatically adds WHERE tenant_id = {current_tenant} to all queries via a beforeFind callback. Set the current tenant in a Filter after authenticating the request. Separate schemas/databases per tenant: use dynamic database switching based on the tenant identifier. Configure the database connection in a Filter: $db = \Config\Database::connect(["database" => "tenant_" . $tenantId]). Subdomain routing: identify tenants by subdomain using CI4's subdomain routing and set a tenant context accordingly. The shared database approach is simpler; separate databases provide stronger isolation. In both cases, a Filter or BaseController method is the right place to resolve and set the current tenant context before any controller logic runs.
The Publisher class in CI4 is a utility for packages and modules to copy files from their own structure into the main project. It is primarily used by CI4 packages during installation. Extend CodeIgniter\Publisher\Publisher. Define source: protected $source = __DIR__ . "/../package". Define destination (default is ROOTPATH). Methods: addFile(), addDirectory(), merge(). The Publisher also provides a conflict resolution strategy — overwrite, keep, or ask. It is most commonly used in package development to "publish" views, config files, and migrations into the application. Run: php spark publish. CI4's Publisher is analogous to Laravel's php artisan vendor:publish. For end-user applications, it is rarely used directly — it is a package development tool.
CI4's Entity casting system automatically converts data types when getting or setting Entity properties. Built-in cast types: integer, float, string, boolean, json (encodes/decodes JSON automatically), array, csv (comma-separated values), datetime (returns a Time object), timestamp, uri. Define casts: protected $casts = ["is_admin" => "boolean", "metadata" => "json", "tags" => "csv", "created_at" => "datetime"]. Custom cast types: implement CastInterface and register in the model. Nullable casts: "age" => "?integer". Casting in entities greatly reduces data transformation boilerplate — setting $entity->tags = ["php", "ci4"] automatically serializes to CSV, and getting $entity->tags returns an array. This brings CI4 models closer to Eloquent's casting functionality.
CI4 Model callbacks are methods that run automatically before or after database operations — similar to Eloquent model events. Define callback methods in your model and register them: protected $beforeInsert = ["hashPassword", "setCreatedBy"], protected $afterFind = ["formatData"]. Available hooks: beforeInsert, afterInsert, beforeUpdate, afterUpdate, beforeFind, afterFind, beforeDelete, afterDelete. Each callback method receives a $data array containing the operation data and must return the (possibly modified) $data. Example: protected function hashPassword(array $data): array { if (!empty($data["data"]["password"])) { $data["data"]["password"] = password_hash($data["data"]["password"], PASSWORD_BCRYPT); } return $data; }. Callbacks are a clean way to handle cross-cutting concerns like hashing, timestamps, and audit logging.
Both CI4 Services and Laravel Facades provide convenient access to framework functionality, but they work differently. Laravel Facades use PHP's static method calls proxied to underlying objects resolved from the container: Cache::get("key") resolves the cache manager from the DI container and calls get(). They feel like static calls but are actually instance methods on container-resolved objects, maintaining testability. CI4 Services are explicit factory methods returning shared instances: service("cache")->get("key") or \Config\Services::cache()->get("key"). Services are more transparent — you can see exactly which class is being returned. Facades are more concise. Both support mocking in tests: Laravel uses Cache::shouldReceive(), CI4 uses Services::injectMock(). CI4 services have lower "magic" overhead and are more predictable, while Laravel facades are more ergonomic for everyday use.
CodeIgniter 4 provides a simple but effective View Layouts system for template inheritance without a full templating engine. Create a layout file in app/Views/Layouts/main.php: <?= $this->renderSection("content") ?> marks where child content goes. In a child view, extend the layout: <?= $this->extend("Layouts/main") ?> and define the content section: <?= $this->section("content") ?>My page content<?= $this->endSection() ?>. Include another view: <?= $this->include("partials/header") ?>. You can have multiple sections in one layout (e.g., content and scripts). Layouts provide the template inheritance functionality that was traditionally handled by third-party libraries in CI3. For more powerful templating with variables and filters, integrate Twig via Composer.
View Cells in CI4 allow you to call a class method from within a view and insert its HTML output. They are reusable components that encapsulate both logic and a view. Define a cell class with a method that returns a view: namespace App\Cells; class RecentPosts { public function index(array $params = []): string { $posts = model("PostModel")->latest()->limit($params["count"] ?? 5)->findAll(); return view("cells/recent_posts", ["posts" => $posts]); } }. Call from a view: <?= view_cell("App\Cells\RecentPosts::index", ["count" => 3]) ?>. Cells can be cached: view_cell("App\Cells\RecentPosts::index", $params, 300) — cached for 300 seconds. View Cells are analogous to Laravel's View Components or Blade's @livewire — they encapsulate reusable view logic like sidebars, comment counts, and dynamic widgets.
The Form helper in CI4 provides functions for generating HTML form elements. Load it: helper("form"). form_open("users/save", ["method" => "post"]) opens a form tag with the URL and method. form_close() closes it. form_input(["name" => "username", "value" => $username, "placeholder" => "Enter username"]) creates an input. form_password("password"), form_textarea("bio", $bio, ["rows" => 4]), form_dropdown("role", $options, $selected), form_radio("gender", "male", true), form_checkbox("agree", "yes", $agreed). form_submit("submit", "Save") creates a submit button. form_hidden("id", $id) creates hidden fields. The helper also includes set_value("field", "default") for repopulating form data after validation failure, and form_error("field") to display validation errors inline.
CodeIgniter 4's Language system provides internationalization (i18n) support. Create language files in app/Language/en/Messages.php: return ["welcome" => "Welcome, {0}!", "not_found" => "Page not found"]. Load language strings: $lang = service("language")->setLocale("fr"). Get a string: lang("Messages.welcome", ["Alice"]) — placeholders {0}, {1} are replaced with arguments. In views: <?= lang("Messages.welcome", [$username]) ?>. Auto-detect locale from browser: $request->negotiate->language(["en", "fr", "de"]). Store locale in session for user preference persistence. Language files can be nested: lang("Messages.validation.required"). CI4's language system is simpler than Laravel's but sufficient for most multi-language applications. For complex pluralization and number formatting, consider adding the intl PHP extension.
CI4 provides an Image class for common image manipulation tasks. Load an image: $image = Services::image()->withFile("/path/to/image.jpg"). Resize: $image->resize(200, 200, true)->save("/path/to/thumb.jpg") (third parameter: maintain aspect ratio). Crop: $image->crop(100, 100, 10, 10) (width, height, x, y). Rotate: $image->rotate(90). Flip: $image->flip("horizontal"). Convert: $image->convert(IMAGETYPE_WEBP). Watermark text: $image->text("© My App", ["color" => "#fff", "opacity" => 50, "withShadow" => true]). Fit (crop to fit): $image->fit(200, 200, "center"). CI4 supports GD2, ImageMagick, and Netpbm drivers — configure in app/Config/Images.php. Chain operations: $image->resize(800, 600, true)->watermarkText("©")->save().
CodeIgniter 4 does not have a separate route caching command like Laravel, but it has several performance features. The route optimization is handled through CI4's startup sequence. Enable Config caching to avoid re-parsing configuration on every request. The most significant performance feature is Auto-routing Improved (disabled by default) — when disabled, only explicitly defined routes are processed, which is faster and more secure. Use $routes->setPrioritize(true) for performance in large route files. Page caching: $this->cachePage(300) in a controller method caches the entire response as a static file for 300 seconds — extremely fast for pages that do not change frequently. Enable OPcache at the PHP level for significant performance gains by caching compiled PHP bytecode. For API-heavy applications, use Redis for response caching via CI4's Cache service.
CodeIgniter 4 supports connecting to multiple databases simultaneously. Configure multiple connections in app/Config/Database.php: define $default (primary), $replica (read replica), and $legacy (old system). Connect to a specific database: $db1 = \Config\Database::connect("default"); $db2 = \Config\Database::connect("legacy"). Use a specific DB in a model: protected $DBGroup = "legacy". Switch databases at runtime: $this->db = \Config\Database::connect("replica"). You can run transactions across connections by managing them manually. CI4 also supports read/write splitting — separate the read and write connections to distribute load. This is useful for migrating from legacy systems, connecting to read replicas for reporting, or multi-tenant applications where each tenant has their own database.
CodeIgniter Shield is the official first-party authentication and authorization package for CI4. Install: composer require codeigniter4/shield. Setup: php spark shield:setup — creates authentication tables, config files, and optionally adds routes. Features: user registration and login (email/password, username/password), session-based auth for web, token-based auth for APIs (similar to Sanctum), magic link login (passwordless email links), two-factor authentication, remember me, email verification, and authorization (groups and permissions). Protect routes: $routes->group("", ["filter" => "session"], function($routes) { ... }). Get current user: auth()->user(). Check permission: auth()->user()->can("read posts"). Shield is the recommended alternative to rolling your own auth in CI4 apps.
CodeIgniter 4 supports named routes for generating URLs by name instead of hardcoding paths. Assign names in app/Config/Routes.php: $routes->get("users/(:num)", "UserController::show/$1", ["as" => "user.show"]). Generate URLs by name: route_to("user.show", 42) returns /users/42. In views: <a href="<?= route_to("user.show", $user->id) ?>">View</a>. Reverse routing also works with controller::method notation: route_to("UserController::show", $id). Redirect to named route: return redirect()->route("user.show", [$id]). Named routes decouple views and controllers from URL structures — change the URL in one place and all generated links automatically update. This is especially valuable in large applications and when implementing URL slugs or localized URLs that change per-language.
CI4 RESTful API authentication is typically implemented using Filters. Common approaches: API Key authentication: client passes a key in the X-API-Key header; a filter validates it against the database. JWT (JSON Web Tokens): use a Composer package like lcobucci/jwt or firebase/php-jwt. The filter decodes and validates the JWT from the Authorization: Bearer {token} header. CodeIgniter Shield tokens: Shield's token auth handler validates tokens from the Authorization header. HTTP Basic Auth: CI4's IncomingRequest provides getServer("PHP_AUTH_USER") and getServer("PHP_AUTH_PW"). In the Filter's before() method, reject the request: return Services::response()->setStatusCode(401)->setJSON(["error" => "Unauthorized"]). Always use HTTPS for API auth — never send credentials over plain HTTP.
Performance comparison between CI4 and Laravel depends on the use case. CI4 has lower framework overhead: fewer service providers, simpler DI, and a lighter bootstrap process result in faster cold start times and lower memory consumption per request. Benchmarks show CI4 handling ~20-30% more requests per second than Laravel in simple scenarios. However, absolute performance differences are usually dwarfed by I/O (database queries, network calls) in real applications. Laravel compensates with Octane (eliminates bootstrap cost entirely by keeping the app in memory) and OPcache. For raw throughput, both frameworks perform well enough for typical web applications. Choose based on features, ecosystem, and team expertise rather than benchmarks. For genuinely high-performance scenarios (100k+ RPS), both may need to offload work to queues, add caching layers, and optimize database access — the framework choice matters less than the architecture.
CodeIgniter 4 supports routes that are only accessible via the command-line interface, not via HTTP requests. Define CLI-only routes: $routes->cli("migrate", "MigrateController::index"). These routes can only be called with php index.php migrate from the command line. Detect CLI environment in controllers: if (is_cli()) { /* CLI-specific logic */ }. CI4 also uses the CLI class for output: CLI::write("Done!", "green"), CLI::error("Failed!"), CLI::input("Enter name: "). CLI routes and controllers are useful for maintenance scripts, imports, and administrative tasks that should not be exposed via HTTP. For more structured CLI work, use Spark Commands instead — they provide argument parsing, option handling, and help text generation automatically. CLI routes are a simpler alternative for basic scripts.
CI4 supports environment-specific configuration through the .env file and environment-based overrides. Set the environment: CI_ENVIRONMENT = production in .env. This changes: debug toolbar visibility, error display verbosity, and can trigger environment-specific code paths: if (ENVIRONMENT === "production") { ... }. Config overrides via .env: any config class property can be overridden using the dot notation in .env: app.baseURL = https://mysite.com overrides Config\App::$baseURL. Database-specific: database.default.hostname = prod-db.example.com. This approach keeps environment-specific values out of PHP files and version control. CI4 also supports multiple .env files in theory, though it processes only one. Use separate .env files per server (.env.production, .env.staging) and symlink the appropriate one, or use server environment variables directly.
CI4 does not include built-in rate limiting like Laravel, but it can be implemented via a Filter. Using CI4's Cache service: in the filter's before() method, track request counts per IP: $key = "rate_limit_" . $this->request->getIPAddress(); $count = cache($key); if ($count === null) { cache()->save($key, 1, 60); } elseif ($count >= 60) { return Services::response()->setStatusCode(429)->setJSON(["error" => "Too Many Requests"]); } else { cache()->save($key, $count + 1, 60); }. For per-user rate limiting, use the user ID as the cache key. Using Redis: leverage Redis INCR and EXPIRE for atomic operations. Register the filter as a route filter for API routes. Third-party packages like cirlabs/ci4-throttle provide ready-made rate limiting with configurable limits per route. For production APIs, also consider implementing rate limiting at the infrastructure level (Nginx, API gateway) for better performance.
CI4's testing infrastructure allows mocking services to isolate units under test. The key method is Services::injectMock("email", $mockEmail) — after this call, any code that calls service("email") gets the mock instead. Use PHPUnit mocks: $mockEmail = $this->createMock(\CodeIgniter\Email\Email::class); $mockEmail->method("send")->willReturn(true); Services::injectMock("email", $mockEmail). Reset mocks after test: Services::reset(). For models: Factories::injectMock("models", "UserModel", $mockModel). For the database: use an in-memory SQLite database configured in app/Config/Database.php under the tests group. CI4's DatabaseTestTrait wraps each test in a transaction and rolls it back — faster than recreating the schema. Mock the Request with custom data: $this->request->setGlobal("post", ["name" => "Alice"]).
The Benchmark class in CI4 measures elapsed time between two points in your code, helping identify performance bottlenecks. The timer is automatically started when CI4 begins processing and stopped at the end, giving you the total execution time. Mark custom timing points: $benchmark = service("timer"). Start a timer: $benchmark->start("my_process"). Stop it: $benchmark->stop("my_process"). Get elapsed time: $benchmark->getElapsedTime("my_process"). In development mode, the Debug Toolbar automatically displays all timer data in the Timeline tab. Check memory usage: $benchmark->getMemoryUsage(). The Benchmark class is automatically shared via the Services class — the same instance is used throughout the request. Use it to profile specific database queries, external API calls, or complex computations to identify where time is being spent in your application.
The URI class in CI4 provides methods for working with the current URL. Access via the request object: $uri = service("request")->getUri() or current_url(true). Get URI segments: $uri->getSegment(1) (first segment after the base URL). Get all segments: $uri->getSegments(). Get the path: $uri->getPath(). Get query string: $uri->getQuery(). Get a specific query param: $uri->getQueryParams()["page"]. Build URLs: $uri = new \CodeIgniter\HTTP\URI("https://example.com"); $uri->addQuery("page", 2). The URI class supports the PSR-7 URI interface methods. Helper functions: uri_string() returns the path portion of the current URL, current_url() returns the full current URL, base_url() returns the base URL, segment($n) returns the nth URL segment.
CodeIgniter 4's View Parser is a simple template system that replaces pseudo-variables with data without requiring full PHP execution. Load: $parser = service("parser"). Render: $parser->render("template", $data). Simple variables: {username} is replaced with the username value from the data array. Variable pairs (loops): {users}{name}{/users} iterates over the users array. Conditionals: {if $active}Active{/if} and {if $age > 18}Adult{else}Minor{/if}. Escaped output: pseudo-variables are automatically HTML-escaped. Comments: {# This is a comment #}. The View Parser is simpler than Twig or Blade — it is suitable for non-PHP developers maintaining templates, CMS content, or email templates. For powerful templating with filters, inheritance, and macros, integrate Twig via Composer. The parser has lower overhead than PHP-based templates.
The UserAgent class in CI4 parses the browser's User-Agent string to identify the browser, platform, mobile device, and robot. Get it: $agent = service("request")->getUserAgent(). Detect browser: $agent->isBrowser() (is it a browser?), $agent->getBrowser() (returns browser name like "Chrome"), $agent->getVersion(). Platform: $agent->isPlatform("Windows"), $agent->getPlatform(). Mobile: $agent->isMobile(), $agent->getMobile(). Robot/Bot: $agent->isRobot(), $agent->getRobot(). Referral: $agent->isReferral(), $agent->getReferrer(). Use cases: serving mobile-optimized content, blocking bots from certain pages, logging user device statistics, and adjusting UI based on browser capabilities. Note that User-Agent strings can be spoofed — do not rely on them for security decisions.
CodeIgniter 4 provides a dedicated Cookie class for working with cookies in a secure, object-oriented way. Create a cookie: $cookie = new \CodeIgniter\Cookie\Cookie("theme", "dark", ["expires" => YEAR, "path" => "/", "secure" => true, "httponly" => true, "samesite" => "Lax"]). Set via response: $this->response->setCookie($cookie) or $this->response->setCookie("theme", "dark", YEAR). Read cookies: $this->request->getCookie("theme") or get_cookie("theme"). Delete a cookie: $this->response->deleteCookie("theme"). The Cookie class encourages security best practices: secure (HTTPS only), httponly (no JavaScript access), and samesite (CSRF protection). Configure default options in app/Config/Cookie.php. Cookie values are not encrypted by default — use CI4's Encryption service to encrypt sensitive cookie values before setting them.
Auto Routing Improved (CI4.2+) is a safer version of auto-routing that maps URLs to controller methods without requiring explicit route definitions. Enable: $routes->setAutoRoute(true) in app/Config/Routes.php. URL: /controller/method/param maps to Controller::method($param). Key security improvements over the old auto-routing: only methods explicitly prefixed with the HTTP verb are accessible (getIndex() for GET, postStore() for POST), parent class methods are not accessible, the method must accept the correct number of parameters. This prevents accidental exposure of internal methods. The route list: php spark routes shows all accessible auto-routes. For new projects, explicit routes are still recommended for clarity, but Auto Routing Improved is a much safer option than the original CI3/CI4 auto-routing for rapid development.
CodeIgniter 4 allows creating custom validation rules for business-specific checks. Method 1 — Ruleset class: create a class with rule methods and register in app/Config/Validation.php under $ruleSets: protected $ruleSets = ["App\Validation\CustomRules"]. The rule method signature: public function validPhoneNumber(string $value, string $params, array $data, string &$error): bool — return false and set $error to show a message. Use: "phone" => "required|valid_phone_number". Method 2 — Closure rules (CI4.3+): $validation->setRule("code", "Code", [function($value, $data, &$error) { if (!preg_match("/^[A-Z]{3}-[0-9]{4}$/", $value)) { $error = "Invalid code format"; return false; } return true; }]). Rule parameters: "min_value[10]" — in the method, $params receives "10". Custom rules integrate seamlessly with CI4's validation pipeline.
CodeIgniter 4 models support event callbacks that function like the observer pattern. Unlike Eloquent's dedicated Observer class, CI4 uses model callbacks defined directly in the model. While CI4 does not have a formal Observer class, you can implement the pattern by: creating a listener class and calling it from model callbacks. In the model's callback method: protected function afterInsert(array $data): array { (new UserObserver())->created($data["id"]); return $data; }. For a true observer pattern, create a custom Observer base class that registers callbacks: class UserModel extends Model { public function __construct() { parent::__construct(); $this->afterInsert[] = [UserObserver::class, "created"]; } }. Alternatively, use CI4's Events system: fire custom events from model callbacks and register listeners in app/Config/Events.php — this provides true decoupling between models and their observers.
Cache invalidation is one of the hardest problems in software — CI4 provides several strategies. Time-based expiry: set a TTL (time-to-live) and let cache expire naturally — simple but stale data is possible. Event-based invalidation: delete specific cache keys when data changes — use model callbacks (afterSave, afterDelete) to call cache()->delete("posts_list"). Cache tags (Redis/Memcached): group related items under a tag and invalidate the whole group: not natively in CI4 but achievable with custom logic. Cache versioning: include a version number in cache keys ("posts_v{$version}") and increment the version to invalidate. Page cache invalidation: cache()->delete(md5($url)) removes a specific page cache. Strategies: aggressive short TTL (simpler but more DB load), event invalidation (complex but data-accurate), or read-through caching with webhooks for external data.
Building a modular CI4 application involves organizing code into self-contained feature modules. Register module namespaces in app/Config/Autoload.php: $psr4 = ["App" => APPPATH, "Modules\Blog" => ROOTPATH . "modules/Blog", "Modules\Auth" => ROOTPATH . "modules/Auth"]. Each module has its own directory: modules/Blog/Controllers/, models/, Views/, Config/, Database/Migrations/. Routes: include module routes in the main routes file: require ROOTPATH . "modules/Blog/Config/Routes.php". Config: module config files are auto-discovered if they extend BaseConfig. Module migrations: php spark migrate --namespace=Modules\Blog. This architecture improves code organization and allows modules to be enabled/disabled by adding/removing the namespace registration. The modular pattern is especially valuable for large teams where different teams own different feature modules.
CI4 provides several tools for database query profiling. The Debug Toolbar's Database tab shows all queries executed during a request with their execution times, affected rows, and the line of code that triggered them — the primary tool for identifying N+1 problems. DB debugging: in app/Config/Database.php, set $DBDebug = true to throw exceptions on database errors instead of failing silently. Query logging: $db->query("...") then $db->getLastQuery() returns the last executed query. Explain queries: run EXPLAIN on slow queries to check index usage: $db->query("EXPLAIN " . $originalQuery). Connection info: $db->getConnectDuration() for connection time. For production profiling, enable MySQL's slow query log with long_query_time = 1 to catch queries over 1 second. Use the CI4 Benchmark class to time specific operations.
CodeIgniter 4 Filters support arguments allowing one filter class to behave differently based on passed parameters. Apply a filter with arguments: $routes->get("admin/dashboard", "Admin::index", ["filter" => "roles:admin,superuser"]). In the filter's before() method, access arguments: public function before(RequestInterface $request, $arguments = null) { if (!in_array($currentUser->role, $arguments)) { return redirect()->to("/")->with("error", "Access denied"); } }. Register filters with aliases in app/Config/Filters.php: $aliases = ["roles" => App\Filters\RoleFilter::class]. Multiple arguments: ["filter" => "throttle:60,1"] receives ["60", "1"]. You can apply filters per HTTP method: $methods = ["post" => ["csrf"]]. Filter arguments make a single filter class flexible enough to handle multiple permission levels, throttle rates, or feature flags without creating separate filter classes for each variation.
CodeIgniter 4 supports multiple database drivers through its Query Builder abstraction. MySQL/MariaDB: the most common choice, supported via MySQLi (default) or PDO. PostgreSQL: full support via the Postgre driver — good choice for complex queries, JSON operations, and full-text search. SQLite3: perfect for development, testing, and small applications with no separate database server. MSSQL (SQL Server): supported via PDO SQLSRV for Microsoft SQL Server databases. OCI8: for Oracle databases in enterprise environments. CUBRID: an open-source relational database. Configure in app/Config/Database.php: set $DBDriver to "MySQLi", "Postgre", "SQLite3", or "SQLSRV". The Query Builder generates database-appropriate SQL automatically — the same PHP code works across all supported drivers without modification.
CI4 models provide a fluent interface for querying with conditions. Chain Query Builder methods before terminal methods: $this->userModel->where("active", 1)->where("role", "editor")->orderBy("name")->findAll(10) — the integer argument to findAll() is the limit. Offset: findAll(10, 20) — 10 results starting from position 20. Find by primary key: find($id) (one) or find([1, 2, 3]) (array). First match: $this->userModel->where("email", $email)->first(). Convenience methods: findColumn("name") returns an array of values for one column. whereIn("id", $ids)->findAll() for IN queries. Check existence: $this->userModel->where("email", $email)->countAllResults(). The model returns the $returnType class (array or Entity) for each result. Use asArray() or asObject() temporarily: $this->userModel->asArray()->find(1).
CI4 supports two levels of caching. Data caching stores computed values (query results, API responses, processed data) in a cache store (file, Redis, Memcached) using the Cache service: if (!$data = cache("key")) { $data = $this->model->expensive(); cache()->save("key", $data, 600); }. This caches data between requests while the controller still runs. Page/Output caching stores the complete rendered HTML response and serves it as a static file, bypassing the controller entirely for cached requests. Enable in a controller method: $this->cachePage(300). The first request renders the page and saves it to writable/cache/. Subsequent requests within 300 seconds serve the file directly. Clear specific page cache: cache()->deleteMatching("url_hash*"). Use output caching for truly static pages (landing pages, blog posts that rarely change); use data caching for pages that need some dynamic elements but have expensive data operations.
Both are redirect methods in CI4 but serve different purposes. redirect()->to($uri) redirects to a specific URL or URI. If a relative URI is given, base_url() is prepended. Status code: 302 by default; pass a second argument for other codes: redirect()->to("/home", 301). redirect()->back() redirects to the previous page using the HTTP Referer header. If no Referer is available, it redirects to the base URL. It is typically used after form submission failures. Both support fluent methods: ->with("key", "value") (flash to session), ->withInput() (flash current POST data for form repopulation), ->withCookies() (send response cookies with redirect). Named routes: redirect()->route("users.index"). The redirect response must be returned: return redirect()->to("/dashboard"). Do not use PHP's header("Location: ...") — it bypasses CI4's response pipeline.
CodeIgniter 4 supports automatic dependency injection in controller methods through type-hinting. When CI4 resolves a controller method, it attempts to resolve type-hinted parameters from the Services class. Inject models: public function index(UserModel $model) — CI4 automatically creates and injects a UserModel instance. Inject services: public function create(ValidationInterface $validation). This reduces the need to call service() or model() manually inside methods. However, CI4's injection is less sophisticated than Laravel's container — it primarily works for CI4 framework classes, not arbitrary user classes. For complex dependency graphs, use the Services class to explicitly configure how classes are built. Constructor injection is more predictable: declare dependencies in __construct() and CI4 resolves them when the controller is instantiated. Method injection is convenient for optional dependencies used in only specific actions.
CodeIgniter 4 provides built-in support for sending file downloads via HTTP. Download a file from disk: return $this->response->download("app/data/report.pdf") — CI4 sets the correct Content-Disposition: attachment header and streams the file. The first argument can be the file path, second can be the filename to display: $this->response->download("/path/to/file", "my_report.pdf"). Download from string/binary data: $data = $pdfGenerator->generate(); return $this->response->download("report.pdf", $data, true) — the third argument (true) indicates in-memory content. For inline display (open in browser instead of download): $this->response->setContentType("application/pdf")->noCache()->setBody($data). Large file downloads: use PHP's readfile() or CI4's download with chunked output to avoid memory exhaustion.
CI4 is well-suited for building CLI applications beyond just Spark commands. Detect CLI context: is_cli(). CI4 CLI apps can: process queues, run scheduled jobs, import/export data, send batch emails, and perform database maintenance. The CLI class provides rich output: CLI::write("text", "color") (colors: green, red, yellow, blue), CLI::table($headers, $rows), CLI::progressBar($total), CLI::printProgress($current, $total). Capture input: CLI::input("Press Enter to continue"), CLI::confirm("Are you sure?"), CLI::promptByKey("Choose", $options). Parse CLI arguments from $_SERVER["argv"]. Long-running scripts: increase ini_set("max_execution_time", 0) and memory_limit. For daemon processes (run forever), use a while loop with a sleep: while (true) { $this->process(); sleep(5); }. Use Supervisor to manage long-running CI4 CLI processes in production.
Creating a custom BaseModel in CI4 that all your models extend is a powerful pattern for sharing common behaviour. Create app/Models/BaseModel.php extending CodeIgniter\Model. Add common functionality: soft deletes enabled by default (protected $useSoftDeletes = true), timestamps enabled (protected $useTimestamps = true), common validation patterns (reusable rule sets), shared callbacks (e.g., always trim string fields before insert), common scopes (active(), orderByLatest()), and audit fields (automatically set created_by from the logged-in user ID in a beforeInsert callback). Add helper methods: findOrFail($id) throws 404 instead of returning null, paginator($perPage) returns paginated results with consistent defaults. All application models extend BaseModel instead of CI4's Model. This reduces repetition across models and ensures consistent behaviour like soft deletes and audit trails without having to remember to configure them in each model.
CI4's feature testing simulates real HTTP requests against your application. Best practices: Use separate test database configured in app/Config/Database.php under the tests group and activated with protected $DBGroup = "tests" in the test class. Use DatabaseTestTrait with $refresh = true to reset the database between tests — this rolls back all transactions or drops/recreates tables. Seed test data: specify $seed = "TestSeeder" or call $this->seed("UserSeeder"). Test authentication: $result = $this->withSession(["user_id" => 1])->get("/dashboard"). Mock services: Services::injectMock("email", $this->createMock(Email::class)). Test assertions: assertStatus(), assertRedirectTo(), assertSee(), assertDontSee(), assertSessionHas(), assertCookieSet(). Structure tests in the same namespace hierarchy as your controllers for clarity.