CodeIgniter 4 filters explained: order, arguments and attributes
Where to apply CodeIgniter 4 filters, the order they run in since 4.5, filter arguments and the new 4.7 controller attributes, with a working API key filter.
Updated 6 min read

In this article
CodeIgniter 4 filters are small classes that run before or after a controller, and they are where cross-cutting rules belong: authentication, API keys, CSRF, rate limits, security headers and maintenance pages. You can attach them in four places: globally or by URI pattern in app/Config/Filters.php, per route or route group in app/Config/Routes.php, and, since CodeIgniter 4.7, with PHP attributes on the controller itself.
Choosing the right place, and knowing the order filters run in, is the difference between a protected admin area and one that is open through a URL you did not think of. This article explains the model, shows a working filter with arguments, and gives a rule of thumb for where each kind of filter should live.
How a filter works
A filter implements CodeIgniter\Filters\FilterInterface, which has two methods. before() runs before the controller and can change or replace the request, or stop the request by returning a response. after() runs after the controller and can change the response, but cannot stop execution. Both methods must exist, even if one is empty.
Here is a filter that protects machine-to-machine endpoints with a shared API key read from .env:
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class ApiKey implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
$expected = (string) env('api.key');
$given = $request->getHeaderLine('X-Api-Key');
if ($expected === '' || ! hash_equals($expected, $given)) {
return service('response')
->setStatusCode(401)
->setJSON(['error' => 'Invalid or missing API key']);
}
return null;
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
return null;
}
}Returning a Response from before() sends it to the client and ends the request; the controller never runs. Returning the request object replaces the current request. Returning nothing lets the next filter and then the controller run. hash_equals() compares the key in constant time, and the empty-string check means a missing setting fails closed instead of letting everyone in. You can generate the skeleton with php spark make:filter ApiKey.

Register an alias
Filters are referenced by alias in app/Config/Filters.php. The documentation is clear that aliases are mandatory in this file: using a full class name in $globals or $filters throws an error. One alias can also stand for several classes.
public array $aliases = [
// ... framework aliases such as csrf, toolbar, honeypot ...
'apikey' => \App\Filters\ApiKey::class,
'role' => \App\Filters\Role::class,
];Four places to apply a filter
1. $required and $globals: every request
$required, added in 4.5, holds framework filters that must always run: by default forcehttps and pagecache before the controller, and pagecache, performance and toolbar after it. Required before-filters run even when no route matches. $globals is for your own cross-cutting filters such as csrf, invalidchars and secureheaders, with an except list for paths that should be skipped:
public array $globals = [
'before' => [
'csrf' => ['except' => ['api/*']],
'invalidchars',
],
'after' => [
'secureheaders',
],
];2. $methods and $filters: by HTTP method or URI pattern
$methods applies before-filters to every request of a given verb, for example 'POST' => ['invalidchars', 'csrf']. $filters maps an alias to URI patterns, and since 4.4 it accepts arguments after a colon:
public array $filters = [
'apikey' => ['before' => ['api/*']],
'role:admin' => ['before' => ['admin/*']],
];When admin/* matches, the role filter receives ['admin'] in $arguments. Since 4.6 the same filter can run more than once with different arguments.
3. Routes and route groups
Route options accept an alias, an alias with arguments, a class name (since 4.1.5) or an array of them:
$routes->group('api', ['filter' => 'apikey'], static function ($routes) {
$routes->resource('books');
});
$routes->get('admin/reports', 'Admin\Reports::index', ['filter' => ['role:admin,editor']]);Here the role filter's before() receives ['admin', 'editor']. Group filters are merged into the filters of the routes inside the group; a bug that prevented this was fixed in 4.5.4.
4. Controller attributes (CodeIgniter 4.7)
Version 4.7 added PHP attributes for controllers. The Filter attribute applies a filter to a whole class or a single method, with arguments passed through having:
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use CodeIgniter\Router\Attributes\Filter;
#[Filter(by: 'role', having: ['admin'])]
class Users extends BaseController
{
public function index()
{
return view('admin/users/index');
}
#[Filter(by: 'role', having: ['superadmin'])]
public function delete(int $id)
{
// Both role filters apply to this method.
}
}Attributes work with every routing method, including auto-routing, which keeps the rule next to the code it protects. The same release added Restrict (limit a controller to environments, hostnames or subdomains) and Cache attributes. Two cautions from the user guide: if a filter is applied both by attribute and in configuration, both run, and every argument is converted to a string. If you do not use attributes, turn the feature off with Config\Routing::$useControllerAttributes = false.

Execution order
CodeIgniter 4.5 changed the order. It is now:
- Before: required → globals → methods → filters → route
- After: route → filters → globals → required
After-filters run in reverse, like layers of an onion: the filter closest to the controller on the way in is the first one on the way out. The debug toolbar always runs last so it can see everything. If an upgraded application depends on the pre-4.5 order, Config\Feature::$oldFilterOrder = true restores it, but treat that as a temporary switch.
Where should each filter live?
| Filter type | Best place | Why |
|---|---|---|
| CSRF, invalid characters, security headers | $globals | They protect everything; exceptions are explicit |
| HTTPS, page cache, toolbar | $required (defaults) | Framework features that must always run |
| API keys, login checks for a section | Route group | Visible next to the routes they protect |
| Role or permission for one screen | Route option or controller attribute | Precise and easy to review |
| Rules that must hold whatever the URL | $filters with a trailing *, or an attribute | Covers alternative URLs to the same controller |
Gotchas
- Legacy auto-routing bypasses route filters. With Auto Routing (Legacy) on, a controller may be reachable through a URL that has no route, so filters set in
Routes.phpdo not run, and any HTTP method can reach a method, which defeats$methods. The user guide's advice is to disable auto-routing and set filters on routes, or use Auto Routing (Improved). - End URI patterns with
*.Blog::indexcan answer atblog,blog/indexandblog/index/1;blog*catches them all. - After-filters cannot block. Authorisation always belongs in
before(). - Filters are not authorisation by themselves. A role filter guards the door, but the controller or service should still check that the user may act on that specific record.
Check what actually runs
Do not guess. php spark filter:check get /admin/reports prints the before and after filters for a route, their arguments and, since 4.6, the resolved class names. php spark routes lists routes with filters too, though it may be inaccurate for routes defined with regular expressions. Add a feature test that requests a protected URL without credentials and expects a 401 or redirect, and you will know the next refactor did not open a hole.
What to do next
List your sensitive URLs, run filter:check on each and fix any that show no authentication filter. Then decide on one convention for your team, for example route groups for sections and attributes for single-method exceptions, and write it in the project README so new code follows the same pattern.
Frequently asked questions
In what order do CodeIgniter 4 filters run?
Since 4.5, before-filters run required, globals, methods, filters, then route filters. After-filters run in reverse: route, filters, globals, required.
Config\Feature::$oldFilterOrderrestores the old order.How do I pass arguments to a filter?
Add them after a colon, separated by commas, for example
role:admin,editorin a route option or in$filters. The filter receives['admin', 'editor']in$arguments. With attributes, use#[Filter(by: 'role', having: ['admin'])].Can an after filter stop the request?
No. An after-filter can only modify or replace the response. To block a request, return a response from
before().Which CodeIgniter version added controller attributes?
CodeIgniter 4.7.0 added controller attributes, including
Filter,RestrictandCache, plus an interface for custom attributes.How can I see which filters apply to a route?
Run
php spark filter:check get /your/path. It lists the before and after filters with their arguments and the resolved filter classes.Why is my route filter not running?
The most common cause is Auto Routing (Legacy), which can reach the controller through a URL that has no defined route. Disable it or switch to Auto Routing (Improved), and check the path with
filter:check.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- Controller Filters — CodeIgniter Foundation
- Controller Attributes — CodeIgniter Foundation
- URI Routing: Applying Filters — CodeIgniter Foundation
- Version 4.7.0 change log — CodeIgniter Foundation
- Auto Routing (Improved) — CodeIgniter Foundation
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
More from Cloudsoft Zone

Industry updateCodeIgniter
CodeIgniter 4.7.5 security release: what to patch and how
CodeIgniter 4.7.5 fixes six advisories, including a critical upload bypass and Parser code execution. See who is exposed and how to upgrade safely.
By Cloudsoft Zone

Troubleshooting guideCodeIgniter
CodeIgniter 4 validation not working? 9 causes and fixes
Fix CodeIgniter 4 validation problems fast: wrong input source, lost errors after redirect, permit_empty, strict JSON rules, is_unique placeholders and more.
By Cloudsoft Zone

Industry updateCakePHP
What's new in CakePHP 5.4: ORM changes, DTO mapping and a built-in container
CakePHP 5.4 changes default eager loading, enforces entity-table ownership and adds request-to-DTO mapping, an optional built-in DI container, typed unhydrated queries and JSON streaming. Here is what to check before upgrading.
By Cloudsoft Zone
