codeigniter4/framework
CodeIgniter 4 is a lightweight, fast PHP framework for building dynamic web apps. It offers MVC structure, minimal configuration, strong security tools, and a small footprint, making development simple and efficient.
codeigniter4/framework is not a library you drop into an existing app — it is the application skeleton's engine (system/), and you almost never reference it directly in business code. Day-to-day you work in a project generated from codeigniter4/appstarter, which pulls this package in via Composer.
composer create-project codeigniter4/appstarter my-app
cd my-app
cp env .env # set CI_ENVIRONMENT = development, app.baseURL
php spark serve # http://localhost:8080, PHP built-in server
First place to look:
app/Config/Routes.php — all routing; nothing is reachable without a route (auto-routing is off by default).app/Config/App.php — $baseURL must be right or site_url(), redirects, and assets break.app/Controllers/BaseController.php — shared helpers, initController(), auth guards.app/Config/Filters.php — the middleware equivalent ($globals, $aliases, $required).vendor/codeigniter4/framework/app/Config/ — the canonical copies of every config file you own in app/Config/.First use case: a thin CRUD endpoint. php spark make:controller Users --restful, php spark make:model UserModel, php spark make:migration CreateUsers, write the up() forge call, php spark migrate, wire $routes->resource('users'), inject the model in initController(), return $this->response->setJSON($rows). That loop — generate, migrate, route, respond — is the whole framework in miniature.
For a Laravel developer the mental mapping is: Artisan → Spark, Eloquent → Models + Entities (Active Record/Query Builder), Blade → plain PHP views, Middleware → Filters, Service Providers → Events + Config\Services, Facades → service()/helpers, model factories → Seeders/Fakers. Queueing, auth, and settings live in separate packages (codeigniter4/shield, codeigniter4/queue, codeigniter4/settings) — the core deliberately stays small.
Routing and grouping. Closures are fine for prototypes; production goes through controllers. Group by filter and prefix:
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api'], static function ($routes) {
$routes->get('users/(:num)', 'Users::show/$1');
$routes->resource('posts', ['except' => ['new', 'edit']]);
});
php spark routes prints the resolved table — reach for it the moment a 404 is unexplained (trailing slash, verb mismatch, filter rejection all surface here).
Models as the unit of data access. The model is where $allowedFields, $validationRules, $useTimestamps, $returnType, and $casts live. Prefer entities ($returnType = User::class) over the default array so $user->created_at is a Time object and $user->toArray() gives you the API shape. Use model('UserModel') or DI in the constructor; keep Query Builder chains in the model, not the controller.
Filters for cross-cutting concerns. Register aliases and apply them per-route rather than sprinkling logic in BaseController:
public array $aliases = ['auth' => \App\Filters\AuthFilter::class, 'throttle' => ...];
A filter returns $request to continue or a Response to short-circuit — the same shape as Laravel middleware, but there is no $next($request) closure.
Responses and validation. ResponseTrait ($this->respond(), $this->failValidationErrors()) standardizes API payloads; $this->validate([...]) runs inline rules and populates $this->validator->getErrors(). For forms, put rule sets in Config\Validation::$user and call $this->validate('user') so the same rules serve HTML and JSON.
Config over code. Every knob is a class property in app/Config/*. .env values override them at runtime using classname.property keys (database.default.hostname, app.baseURL). This means one deployable image + per-environment .env, no config caching step.
Testing. Use CIUnitTestCase + FeatureTestTrait for HTTP-level assertions and DatabaseTestTrait with $refresh = true, $namespace = 'App', $migrate = true for a real schema per test. Services::reset() between tests clears the shared service instances — skipping it is the source of most flaky suites.
Commands. Anything you would write as an Artisan command is an App\Commands\* class extending BaseCommand; auto-discovered from app/Commands, invoked via php spark your:command.
app/Config is a fork, not a symlink. composer update upgrades system/ but never touches your config copies. After every upgrade, diff app/Config/ against vendor/codeigniter4/framework/app/Config/ and merge new properties by hand — missing properties cause silent behavior changes rather than errors (php spark config:check catches some of this).system/ or anything in vendor/. Override through Config\Services (register a replacement for a framework service), Events (Events::on('pre_system', ...)), or by extending the class in app/Libraries. The repo's own guidance — "dependencies should be injectable", "prefer the minimum useful abstraction" — is the same discipline you apply in your app.$allowedFields is mandatory. An empty array silently blocks all inserts/updates while find() still works, so reads pass and writes no-op. Pair it with $protectFields = true and never call skipValidation() casually.setAutoRoute(true) in production — explicit routes plus spark routes are your safety net.is_cli().validate() returns bool and stashes the validator on $this->validator; call getErrors() after the failed check, not on a fresh instance.writable/session with the file handler by default. In containers, point Config\Session::$savePath at a mounted volume or switch to a shared handler, or logins evaporate across replicas.Config\Toolbar / Config\Kint. They add measurable overhead and leak query data — confirm CI_ENVIRONMENT before profiling anything in staging.public/frankenphp-worker.php is generated from a template; if you've customized it, framework upgrades will not propagate fixes — republish with php spark worker:install --force and re-apply your edits. Static state and open connections are per-process, so any request-scoped cache you add must be reset explicitly.find() returns null, not an exception. Build a findOrFail-style helper in your base model if you're used to Laravel's behavior.composer.json pins a minor line and the project follows semver-with-caveats: develop accepts no BC breaks, 4.* allows narrow, documented ones. Read the upgrading guide on every minor bump; deprecated APIs survive at least two minors, so pin deliberately.codeigniter4/shield and friends on day one rather than retrofitting auth later.How can I help you explore Laravel packages today?