Skip to content
Edit this page

Version 4.8.0

Release Date: Unreleased

4.8.0 release of CodeIgniter4

Highlights

  • TBD

BREAKING

Behavior Changes

  • The static Boot::initializeConsole() method no longer handles the display of the console header. This is now handled within Console::run(). If you have overridden Boot::initializeConsole(), you should remove any code related to displaying the console header, as this is now the responsibility of the Console class.
  • CLI: Uncaught exceptions in spark, and errors shown with BaseCommand::showError() or AbstractCommand::renderThrowable() (as migrate and db:seed do), are now printed to STDERR instead of STDOUT. The exit code is unchanged. See CLI Error Output.
  • CLI: CLI::beep() now writes through the InputOutput transport instead of echo, so it is discarded by callSilently(), captured by MockInputOutput, and no longer seen by PHP output buffering on the command line.
  • CLI: CLI::print() of text ending in a line feed, and a progress bar drawn with CLI::showProgress(), no longer cause a blank line before the next CLI::write(), since the cursor already sits at the start of a line. CLI::init() now also resets that cursor state, the progress bar, and the cached terminal dimensions.
  • CLI: CLI::showProgress() now draws a full bar at 100% for a step beyond the total, instead of throwing a ValueError.
  • CLI: CLI::error() no longer leaves color support disabled for STDOUT when it is given an invalid color name, and CLI::write(), CLI::print(), and CLI::color() now accept an empty foreground with a background instead of throwing.
  • CLI: An empty NO_COLOR no longer turns color off, following https://no-color.org/. Color detection now turns color off when TERM is dumb, and on Windows when the stream is not a terminal (outside MSYS2's MINGW32 and MINGW64). On Windows without VT100 support, any TERM starting with xterm, screen, vt100, vt220, putty, rxvt, ansi, cygwin or linux, or ending in -256color, now enables color, where previously only xterm did. Coloring text that already contains a colored segment now keeps that segment whatever its color, where previously only the dim colors (\033[0;...m) were kept and a bright or background-only segment lost the color of the text after it.
  • CLI: The output state moved out of the static CLI properties. A subclass of CLI that overrides fwrite() or color(), or reads $lastWrite, $isColored, $io, $height, $width, or the color tables, no longer affects or observes the output. See CLI Output Classes.
  • CLI: The prompt methods moved to Prompt. A subclass of CLI that overrides validate() no longer affects the prompts, and an override of prompt() no longer affects promptByKey() or promptByMultipleKeys(). Passing prompt() options or rules, or a promptByKey() text, of another type than string, array, or null now throws a TypeError instead of an InvalidArgumentException. See CLI Prompt.
  • Commands: The -h option of the routes command, which was previously mapped to --sort-by-handler, is now removed. Use --sort-by-handler instead to sort the routes by handler when running the routes command.
  • Commands: The filter:check command now requires the HTTP method argument to be uppercase (e.g., spark filter:check GET / instead of spark filter:check get /).
  • Commands: Several built-in commands have been migrated from BaseCommand to the modern AbstractCommand style. Applications that extend a built-in command to override behavior may need to re-implement against the modern API (configure() + execute() and the #[Command] attribute) once the class it extends is migrated, or, preferably, compose instead of extending. Invocations on the command line are unaffected.
  • Commands: Generator commands migrated to AbstractGeneratorCommand now return EXIT_ERROR (previously EXIT_SUCCESS) when the target file exists without --force, when the namespace is not defined, or when the name does not form a valid class name. The CodeIgniter namespace confirmation is no longer prompted on non-interactive runs, and make:cell and make:model --return entity now ask it only once.
  • Commands: The make:command command now generates modern commands: a basic command extends AbstractCommand with the #[Command] attribute, and a generator command extends AbstractGeneratorCommand with the #[GeneratorCommand] attribute.
  • Commands: The make:migration command now rejects --force as an unknown option. It was previously accepted but ignored, since the timestamped file name only collides with an existing file when the same name is generated twice within one second.
  • Commands: The success and error messages from debugbar:clear, cache:clear, and cache:info now include the affected path or cache driver/handler so the user can see which resource was acted on (or rejected). Scripts asserting on the prior literal text will need to be updated.
  • Commands: Declining the key:generate overwrite prompt interactively now returns EXIT_SUCCESS instead of EXIT_ERROR. Output messages were also reworded; CI/automation that branches on the exit code or greps the previous wording will need updating.
  • Commands: The migrate:rollback command no longer accepts the undocumented -g (database group) option. It never had any effect, since MigrationRunner::regress() ignores the group, and the modern command pipeline now rejects unknown options. Remove -g from any migrate:rollback invocation.
  • Commands: The logs:clear command now returns EXIT_SUCCESS (previously EXIT_ERROR) when the user declines the interactive confirmation prompt, since user-initiated cancellation is not a failure. Output messages have also been reworded to distinguish cancellation (interactive n) from abort (non-interactive without --force), and the resolved log directory path is now included in the prompt, success, and failure messages.
  • Commands: The db:table command now returns EXIT_SUCCESS from db:table --show (previously EXIT_ERROR), and reports an error instead of prompting when run non-interactively (--no-interaction or piped input) without a valid table. CI/automation that branches on the --show exit code will need updating.
  • Commands: The db:table output was reworded: the data and metadata column headers are now capitalized (e.g. Id, Created_at), the connection summary headers read Hostname, Database, Username, DB Driver, DB Prefix, and Port, and the section titles changed (e.g. Data of "users" table:). Scripts that grep the previous output will need updating.
  • Commands: The worker:uninstall command now returns EXIT_SUCCESS (previously EXIT_ERROR) when the user declines the interactive confirmation prompt, since user-initiated cancellation is not a failure. In non-interactive mode without --force it now aborts with EXIT_ERROR instead of prompting, and the redundant Uninstalling FrankenPHP Worker Mode header was dropped. Scripts that branch on the previous exit code or wording will need updating.
  • Commands: The phpini:check command's invalid-argument error now prints only You must specify a correct argument.. The usage example and the argument listing moved to phpini:check --help, where the argument is now named section. Scripts that grep the previous output will need updating.
  • Commands: The prompts that pick from fixed options (db:table, db:create, key:generate, key:rotate, make:command, make:controller, and make:model) now use Prompt::select(), so where the arrow-key menu is unavailable, as on Windows, the answer is the option's number instead of its value. Option values such as --prefix still take the value.
  • Config: CodeIgniter\Config\BaseService is now abstract. It is intended to be extended (as Config\Services does) and accessed statically; code that instantiated it directly with new BaseService() will now throw an Error.
  • Config: BaseConfig now scans the environment once instead of checking every property against it, which makes building config classes several times faster without config caching and in Worker Mode. As a result, a bare putenv() call (adding, changing, or removing a variable) made after the first config class was built may not be seen until BaseConfig::reset() runs, including in later requests in Worker Mode. See Config Environment Lookups.
  • Database: BaseBuilder::getCompiledSelect(), getCompiledInsert(), getCompiledUpdate(), getCompiledDelete(), delete() and countAllResults() now clear bindings and binding counters after compilation or execution when their reset parameter is enabled (the default). This prevents accumulation when a builder is reused, but getBinds() now returns an empty array after these calls. This also aligns getCompiledSelect() with its documented reset behavior, which should match get(). Pass false for the reset parameter to retain the query and its bindings. Test mode retains bindings needed by its returned raw SQL. See Query Builder Bindings.
  • Database: The Postgre driver's $db->error()['code'] previously always returned ''. It now returns the 5-character SQLSTATE string for query and transaction failures (e.g., '42P01'), or '08006' for connection-level failures. Code that relied on $db->error()['code'] === '' will need updating.
  • Debug: CodeIgniter\Debug\Toolbar\Collectors\BaseCollector is now abstract. Custom toolbar collectors must extend it; code that instantiated it directly with new BaseCollector() will now throw an Error.
  • Filters: HTTP method matching for method-based filters is now case-sensitive. The keys in Config\Filters::$methods must exactly match the request method (e.g., GET, POST). Lowercase method names (e.g., post) will no longer match.
  • HTTP: Routes defined with $routes->add() now also match HTTP QUERY requests. If the route has CSRF protection, remember that CSRF verification does not protect safe methods such as GET and QUERY.
  • HTTP: CodeIgniter\HTTP\Exceptions\RedirectException no longer redeclares the inherited $code property. The status applied to a Response passed to the constructor whose status is outside the 301-308 range now comes from the new protected int $defaultStatusCode = 302 property. Subclasses that overrode $code to change that status must override $defaultStatusCode instead.
  • Router: RouteCollection no longer applies esc(strip_tags()) to namespace, controller, or method values, including controller options passed to resource() and presenter(). Applications relying on this implicit transformation must preprocess these values before passing them to the router.
  • Testing: Tests using the FeatureTestTrait must now use uppercase HTTP method names when they call the call() method directly (e.g., $this->call('GET', '/path') instead of $this->call('get', '/path')). Additionally, method-based routes set with withRoutes() must also use uppercase method names (e.g., $this->withRoutes([['GET', 'home', 'Home::index']])).

Interface Changes

NOTE: If you've implemented your own classes that implement these interfaces from scratch, you will need to update your implementations to include the new methods or method changes to ensure compatibility.

  • Cache: CodeIgniter\Cache\CacheInterface::remember() now accepts a TTL callable. Custom implementations of CacheInterface must update the $ttl parameter type from int to callable|int.
  • Database: CodeIgniter\Database\ConnectionInterface now requires the afterCommit(), afterRollback(), inTransaction(), and transaction() methods.
  • HTTP: CodeIgniter\HTTP\ResponseInterface now requires the stream() and eventStream() methods, which create streaming and SSE responses. See Streaming Responses.
  • HTTP: CodeIgniter\HTTP\Files\UploadedFileInterface::move() now returns static instead of bool. The previous bool return was incompatible with CodeIgniter\Files\File::move(), which UploadedFile extends, so no implementation could satisfy both. See Upgrading from 4.7.x to 4.8.0 for the migration.
  • Logging: CodeIgniter\Log\Handlers\HandlerInterface::handle() now requires a third parameter array $context = []. Any custom log handler that overrides handle() - whether implementing HandlerInterface directly or extending a built-in handler class - must add the parameter to its handle() method signature.
  • Security: The SecurityInterface's verify() method now has a native return type of static.
  • Validation: CodeIgniter\Validation\ValidationInterface now requires the getValidatedInput() method, which returns a CodeIgniter\Input\ValidatedInput instance.

Method Signature Changes

  • Database: BaseBuilder::join() and BaseBuilder::compileJoinTable() now accept RawSql|string for the table argument. Custom builders overriding either method must accept both types. See Upgrading from 4.7.x to 4.8.0.

  • CLI: The Console::run() method now accepts an optional array $tokens parameter. This allows you to pass an array of command tokens directly to the console runner, which is useful for testing or programmatically running commands. If not provided, it will default to using the global $argv.

  • CodeIgniter: The deprecated parameters in methods have been removed:

    • CodeIgniter\CodeIgniter::handleRequest() no longer accepts the deprecated $cacheConfig and $returnResponse parameters.

      • $cacheConfig is no longer used and is now hard deprecated. A deprecation notice will be triggered if this is passed to the method.
      • $returnResponse has been removed, since it was already deprecated and unused.
      • The updated method signature is now handleRequest(?RouteCollectionInterface $routes, ?Cache $cacheConfig = null).
    • CodeIgniter\CodeIgniter::gatherOutput() no longer accepts the deprecated $cacheConfig parameter. As this is the first parameter, custom uses of this method will need to be updated to remove the parameter.

  • Config: CodeIgniter\Config\Services::request() no longer accepts any parameter.

  • Database: The following methods have had their signatures updated to remove deprecated parameters:

    • CodeIgniter\Database\Forge::_createTable() no longer accepts the deprecated $ifNotExists parameter. The method signature is now _createTable(string $table, array $attributes).
  • HTTP: CodeIgniter\HTTP\OutgoingRequest::__construct() now requires the $uri parameter, which was previously ?URI $uri = null. Passing null only worked when a Host header was supplied in the same call, since the constructor otherwise dereferences the URI to set that header. Consequently OutgoingRequest::getUri() now returns URI instead of URI|null, matching OutgoingRequestInterface. See Upgrading from 4.7.x to 4.8.0 for the migration.

  • Model: CodeIgniter\BaseModel now requires the chunkRows(), chunkById(), and chunkRowsById() methods. Custom classes extending BaseModel directly must implement them.

  • Session: The $max_lifetime parameter of the following gc() methods now has the native int type, matching SessionHandlerInterface: ArrayHandler::gc(), DatabaseHandler::gc(), FileHandler::gc(), MemcachedHandler::gc(), PostgreHandler::gc(), RedisHandler::gc().

Property Scope Changes

  • HTTP: The following properties have changed their scope (visibility):
    • CodeIgniter\HTTP\RequestTrait::$ipAddress is now private (previously protected).

Method Scope Changes

  • CLI: CodeIgniter\CLI\Commands::getCommandAlternatives() is now public (previously protected).

  • HTTP: The following methods have changed their scope (visibility):

    • CodeIgniter\HTTP\URI::setUri() is now private (previously public).
    • CodeIgniter\HTTP\URI::refreshPath() is now protected (previously public).
    • CodeIgniter\HTTP\SiteURI::refreshPath() is now protected (previously public).

Removed Deprecated Items

  • Autoloader: Removed the following deprecated methods:

    • CodeIgniter\Autoloader\Autoloader::sanitizeFileName()
    • CodeIgniter\Autoloader\Autoloader::discoverComposerNamespaces()
  • Bootstrap: The deprecated system/bootstrap.php file has been removed.

  • Cache: Removed the following deprecated methods and constant:

    • CodeIgniter\Cache\Handlers\BaseHandler::RESERVED_CHARACTERS (deprecated since v4.1.5)
    • CodeIgniter\Cache\Handlers\FileHandler::writeFile() (deprecated since v4.6.0)
    • CodeIgniter\Cache\Handlers\FileHandler::deleteFile() (deprecated since v4.6.0)
    • CodeIgniter\Cache\Handlers\FileHandler::getDirFileInfo() (deprecated since v4.6.0)
    • CodeIgniter\Cache\Handlers\FileHandler::getFileInfo() (deprecated since v4.6.0)
  • CLI: Removed the following deprecated properties and methods:

    • CodeIgniter\CLI\BaseCommand::getPad()
    • CodeIgniter\CLI\CLI::$readline_support
    • CodeIgniter\CLI\CLI::$wait_msg
    • CodeIgniter\CLI\CLI::isWindows()
    • CodeIgniter\CLI\GeneratorTrait::execute()
  • CodeIgniter: Removed the following deprecated properties and methods:

    • CodeIgniter\CodeIgniter::$cacheTTL
    • CodeIgniter\CodeIgniter::$returnResponse
    • CodeIgniter\CodeIgniter::initializeKint()
    • CodeIgniter\CodeIgniter::detectEnvironment()
    • CodeIgniter\CodeIgniter::bootstrapEnvironment()
    • CodeIgniter\CodeIgniter::forceSecureAccess()
    • CodeIgniter\CodeIgniter::displayCache()
    • CodeIgniter\CodeIgniter::cache()
    • CodeIgniter\CodeIgniter::cachePage()
    • CodeIgniter\CodeIgniter::generateCacheName()
    • CodeIgniter\CodeIgniter::displayPerformanceMetrics()
    • CodeIgniter\CodeIgniter::determinePath()
    • CodeIgniter\CodeIgniter::callExit()
    • CodeIgniter\Test\MockCodeIgniter::callExit()
  • Config: Removed the following deprecated property:

    • CodeIgniter\Config\BaseService::$services (deprecated since v4.5.0)
  • Database: Removed the following deprecated properties and methods:

    • CodeIgniter\Database\BaseBuilder::setInsertBatch()
    • CodeIgniter\Database\BaseBuilder::setUpdateBatch()
    • CodeIgniter\Database\BaseBuilder::cleanClone()
    • CodeIgniter\Database\BaseConnection::$strictOn
    • CodeIgniter\Database\Forge::$createTableIfStr
    • CodeIgniter\Database\Seeder::$faker
    • CodeIgniter\Database\Seeder::faker()
    • CodeIgniter\Database\OCI8\Forge::$createTableIfStr
    • CodeIgniter\Database\OCI8\Forge::getError()
    • CodeIgniter\Database\SQLSRV\Forge::$createTableIfStr
  • Debug: Removed the following deprecated properties and methods:

    • CodeIgniter\Debug\Toolbar\Collectors\BaseCollector::cleanPath() (deprecated since v4.2.0)
    • CodeIgniter\Debug\Exceptions::$ob_level (deprecated since v4.4.0)
    • CodeIgniter\Debug\Exceptions::$viewPath (deprecated since v4.4.0)
    • CodeIgniter\Debug\Exceptions::determineView() (deprecated since v4.4.0)
    • CodeIgniter\Debug\Exceptions::render() (deprecated since v4.4.0)
    • CodeIgniter\Debug\Exceptions::collectVars() (deprecated since v4.4.0)
    • CodeIgniter\Debug\Exceptions::maskSensitiveData() (deprecated since v4.4.0)
    • CodeIgniter\Debug\Exceptions::maskData() (deprecated since v4.4.0)
    • CodeIgniter\Debug\Exceptions::cleanPath() (deprecated since v4.2.0)
    • CodeIgniter\Debug\Exceptions::describeMemory() (deprecated since v4.4.0)
    • CodeIgniter\Debug\Exceptions::highlightFile() (deprecated since v4.4.0)
  • Exceptions: Removed the following static constructors of FrameworkException and its child classes:

    • CodeIgniter\Exceptions\DownloadException::forCannotSetCache()
    • CodeIgniter\Exceptions\FrameworkException::forMissingExtension()
    • CodeIgniter\Honeypot\Exceptions\HoneypotException::forNoHiddenValue()
    • CodeIgniter\HTTP\Exceptions\HTTPException::forInvalidSameSiteSetting()
    • CodeIgniter\Security\Exceptions\SecurityException::forInvalidSameSite()
    • CodeIgniter\Session\Exceptions\SessionException::forInvalidSameSiteSetting()
  • Filters: Removed the following deprecated properties and methods:

    • CodeIgniter\Filters\Filters::$arguments (deprecated since v4.6.0)
    • CodeIgniter\Filters\Filters::$argumentsClass (deprecated since v4.6.0)
    • CodeIgniter\Filters\Filters::getArguments() (deprecated since v4.6.0)
  • HTTP: Removed the following deprecated properties and methods:

    • CodeIgniter\HTTP\Message::getHeaders() (deprecated since v4.0.5)
    • CodeIgniter\HTTP\Message::getHeader() (deprecated since v4.0.5)
    • CodeIgniter\HTTP\RequestTrait::getEnv() (deprecated since v4.4.4)
    • CodeIgniter\HTTP\URI::$uriString (deprecated since v4.4.0)
    • CodeIgniter\HTTP\URI::$baseURL (deprecated since v4.4.0)
    • CodeIgniter\HTTP\URI::setBaseURL() (deprecated since v4.4.0)
    • CodeIgniter\HTTP\URI::getBaseURL() (deprecated since v4.4.0)
    • CodeIgniter\HTTP\URI::setScheme() (deprecated since v4.4.0)
    • CodeIgniter\HTTP\SiteURI::$segments (deprecated since v4.4.0)
    • CodeIgniter\HTTP\SiteURI::setBaseURL() (deprecated since v4.4.0)
    • CodeIgniter\HTTP\SiteURI::setURI() (deprecated since v4.4.0)
  • Security: Removed the following deprecated properties and methods:

    • CodeIgniter\Security\SecurityInterface::sanitizeFilename() (deprecated since v4.6.2)
    • CodeIgniter\Security\Security::sanitizeFilename() (deprecated since v4.6.2)
    • CodeIgniter\Security\Security::$csrfProtection (deprecated since v4.4.0)
    • CodeIgniter\Security\Security::$tokenRandomize (deprecated since v4.4.0)
    • CodeIgniter\Security\Security::$tokenName (deprecated since v4.4.0)
    • CodeIgniter\Security\Security::$headerName (deprecated since v4.4.0)
    • CodeIgniter\Security\Security::$expires (deprecated since v4.4.0)
    • CodeIgniter\Security\Security::$regenerate (deprecated since v4.4.0)
    • CodeIgniter\Security\Security::$redirect (deprecated since v4.4.0)
    • CodeIgniter\Security\Security::$sameSite (deprecated since v4.4.0)

Enhancements

API

  • Added per-type sparse fieldsets to API transformers. A transformer may declare a $type so clients can request ?fields[<type>]=... to filter the fields of that resource type independently, including for nested/related resources, while the flat ?fields=... continues to apply to the root resource only. See API Resources.

Commands

  • Added a new attribute-based command style built on AbstractCommand and the #[Command] attribute, with configure() / isAvailable() / initialize() / interact() / execute() hooks and typed Argument / Option definitions. The legacy BaseCommand style continues to work. See Modern Spark Commands.
  • Modern commands can now declare command aliases through an aliases list on the #[Command] attribute. Aliases resolve to the command at dispatch (php spark <alias> and help <alias>), are listed as their own rows in spark list, and appear in an Aliases: section of help <command>. An alias that collides with an existing command name or another alias is rejected at discovery. See Modern Spark Commands.
  • Modern commands can now be hidden with hidden: true on the #[Command] attribute. A hidden command and its aliases are left out of spark list and of the suggestions for a mistyped command name, but still run by exact name or alias and still have a help page. AbstractCommand::isHidden() and Commands::isHiddenCommand() report the flag. See Hidden Commands.
  • Modern commands can now opt out of the spark header with headerless: true on the #[Command] attribute, with the same effect as always passing --no-header. help <command> and <command> --help still print the header. AbstractCommand::isHeaderless() and Commands::isHeaderlessCommand() report the flag. See Commands Without the Header.
  • Every modern command now ships with a --no-interaction / -N flag that skips the interact() hook, plus public isInteractive() / setInteractive() methods on AbstractCommand. isInteractive() also auto-detects piped or CI environments by probing STDIN for a TTY, and the state cascades to sub-commands invoked via $this->call(...). See Non-Interactive Mode.
  • spark help <command> now shows the default value of each option that accepts a value (e.g., [default: "file"]), the same way argument defaults are already displayed. Boolean flags and negatable toggles, which have no meaningful default to show, are unaffected.
  • You can now retrieve the last executed command in the console using the new Console::getCommand() method. This is useful for logging, debugging, or any situation where you need to know which command was run.
  • CLI now supports the -- separator to mean that what follows are arguments, not options. This allows you to have arguments that start with - without them being treated as options. For example: spark my:command -- --myarg will pass --myarg as an argument instead of an option.
  • CLI now supports options with values specified using an equals sign (e.g., --option=value) in addition to the existing space-separated syntax (e.g., --option value). This provides more flexibility in how you can pass options to commands.
  • CLI now supports parsing array options written multiple times (e.g., --option=value1 --option=value2) into an array of values. This allows you to easily pass multiple values for the same option without needing to use a comma-separated string. When used with CLI::getOption(), an array option will return its last value (for example, in this case, value2). To retrieve all values for an array option, use CLI::getRawOption().
  • Likewise, the command() function now also supports the above enhancements for command-line option parsing when using the function to run commands from code.
  • Added make:request generator command to scaffold Form Request classes.
  • Added key:rotate command to demote the current encryption.key to encryption.previousKeys in .env and generate a new key. See Rotating with the key:rotate Command.
  • Added AbstractCommand::callSilently() to invoke another command with its output discarded, restoring the prior output afterwards. See Calling Silently.
  • The output side of the CLI class now lives in instance classes, with CLI delegating to them: Output (print(), write(), error(), newLine(), deferNewLine(), beep(), clearScreen(), color(), wrap()), Terminal (dimensions and color support), ProgressBar, Table, and Color (ANSI codes and Color::width()). CLI::getOutput() and CLI::getTerminal() return the shared instances. See The Classes Behind the Facade.
  • Modern commands can write through $this->getOutput(), the same Output used by the static CLI methods. AbstractCommand::run() and Commands::runCommand() accept an Output to run against, which Console passes down and sub-commands inherit. See Writing Output.
  • The prompt methods of CLI now delegate to Prompt (ask(), askByKey(), askByMultipleKeys()), which reads through an Output and accepts the validation that checks the answers. CLI::getPrompt() and AbstractCommand::getPrompt() return one on the shared output and on the output of the run. See The Classes Behind the Facade.
  • Added Prompt::select() and Prompt::multiselect(), which pick options with the arrow keys and the space bar, and ask for numbers where keys cannot be read. See select() and multiselect().
  • Added --silent, --quiet / -q, and --verbose / -v, -vv, -vvv to every command, with SHELL_VERBOSITY as the fallback. Modern commands can write at a Verbosity through $this->getOutput(). lang:find uses the global --verbose in place of its own. See Verbosity and Writing at a Verbosity.
  • Color detection now honors FORCE_COLOR (see https://force-color.org/), which turns color on even when the output is piped, and COLORTERM. NO_COLOR still wins over both. The color variables can also be set in the .env file.
  • Added AbstractGeneratorCommand and the #[GeneratorCommand] attribute, the modern counterpart of GeneratorTrait for commands that generate files from templates. Generator commands built on it accept the -n, -s, and -f shortcuts for --namespace, --suffix, and --force. See Modern Generator Commands.
  • Legacy BaseCommand::call() can now invoke modern commands: integer-keyed params are passed as arguments and string-keyed params as options, which the target command validates like any other input.
  • The make:controller command now accepts -b and -r as shortcuts for --bare and --restful, and rejects an invalid --restful value on non-interactive runs instead of prompting.
  • The make:model command now accepts -t, -g, and -r as shortcuts for --table, --dbgroup, and --return, and rejects an invalid --return value on non-interactive runs instead of prompting.
  • The make:command command now accepts -c, -t, and -g as shortcuts for --command, --type, and --group, and rejects an invalid --type value on non-interactive runs instead of prompting.
  • The make:migration command now accepts -t and -g as shortcuts for --table and --dbgroup.
  • The make:scaffold command now accepts -b, -t, and -g as shortcuts for --bare, --table, and --dbgroup, and asks for a missing class name once instead of once per generated file.
  • spark now offers to run the closest matching command when a command name is mistyped on an interactive run. See Correcting a Mistyped Command.
  • Modern commands can now opt in to prompting for missing required arguments on interactive runs by implementing the new PromptsForMissingInputInterface marker interface. Prompt labels can be customized via getArgumentPromptLabels(), and an afterPrompting() hook runs when prompting occurred. Non-interactive runs keep failing fast with the missing-arguments error. See Prompting for Missing Arguments.
  • The migrate, migrate:rollback, migrate:refresh, and migrate:status commands now accept long option names (--namespace, --group, --batch, --force) alongside their existing short forms (-n, -g, -b, -f).
  • The namespaces command now accepts long option names (--config-only, --raw, --max-length) alongside its existing short forms (-c, -r, -m).
  • The worker:install and worker:uninstall commands now accept the -f short option alongside --force.
  • Added NullInputOutput, an InputOutput sink that discards all writes and returns an empty string from input().
  • The spark routes command now resolves the Before/After Filters columns for routes that use custom placeholders registered via $routes->addPlaceholder(). Sample URIs for custom placeholders are derived from the placeholder regex when possible, and can be overridden through the new $placeholderSamples property in Config\Routing. See Sample Values for spark routes.

Testing

  • Added assertSameSql() to CIUnitTestCase to compare generated SQL while ignoring newlines in the actual SQL.
  • Added FeatureTestTrait::withFiles() and MockUploadedFile for testing routes that validate and move uploaded files.

Database

  • Added afterCommit() and afterRollback() transaction callbacks to database connections. These callbacks run after the outermost transaction commits or rolls back. See Running Code after Commit or Rollback.
  • Added ConstraintViolationException with UniqueConstraintViolationException, ForeignKeyConstraintViolationException, NotNullConstraintViolationException, and CheckConstraintViolationException subclasses for typed database integrity errors. See Constraint Violation Exceptions.
  • Added inTransaction() to database connections to check whether the connection is inside an active CodeIgniter-managed transaction. See Checking Transaction State.
  • Added support for PHP BackedEnum values in database escaping, query bindings, and Query Builder bound values.
  • Prepared query execution failures now throw or store typed database exceptions such as ConstraintViolationException and RetryableTransactionException when applicable, matching normal query failures.
  • Added RetryableTransactionException for driver-specific retryable transaction failures such as deadlocks and serialization failures. See Classifying Retryable Transaction Failures.
  • Added the transaction() method to database connections to run a callback inside a transaction, with optional retry attempts for retryable transaction failures and scoped transException and resetTransStatus options. See Running Transactions with a Closure.
  • Added trustServerCertificate option to SQLSRV database connections in Config\Database. Set it to true to trust the server certificate without CA validation when using encrypted connections.

Query Builder

  • BaseBuilder::join() now accepts RawSql as the table argument, allowing raw JOIN targets such as subqueries. See RawSql.
  • Added exists() and doesntExist() to Query Builder to check whether the current Query Builder query would return at least one row. See $builder->exists().
  • Added explain() to Query Builder to run execution-plan queries for the current SELECT query. See $builder->explain().
  • Added havingBetween(), orHavingBetween(), havingNotBetween(), and orHavingNotBetween() to Query Builder. See $builder->havingBetween().
  • Added likeAny() and orLikeAny() to Query Builder to search one value across multiple fields with grouped OR LIKE conditions. See $builder->likeAny().
  • Added nowait() and skipLocked() lock wait modifiers for pessimistic SELECT locks on supported drivers. See No Wait and Skip Locked.
  • Added sharedLock() to add pessimistic read locks to SELECT queries on supported drivers. See Shared Lock.
  • Added whereBetween(), orWhereBetween(), whereNotBetween(), and orWhereNotBetween() to Query Builder. See $builder->whereBetween().
  • Added whereColumn() and orWhereColumn() to compare one column to another column while protecting identifiers by default. See $builder->whereColumn().
  • Added whereExists(), orWhereExists(), whereNotExists(), and orWhereNotExists() to add EXISTS and NOT EXISTS subquery conditions. See $builder->whereExists().
  • Added new incrementMany() and decrementMany() methods to CodeIgniter\Database\BaseBuilder for performing bulk increment/decrement operations.
  • Added lockForUpdate() to add pessimistic write locks to SELECT queries on supported drivers. See Lock for Update.

Forge

Others

  • Added new timezone option to connection array in Config\Database config. This ensures consistent timestamps between model operations and database functions like NOW(). Supported drivers: MySQLi, Postgre, and OCI8. See timezone for details.
  • Added $db->getLastException() which returns the typed exception even when DBDebug is false. See $db->getLastException().
  • Added DatabaseException::getDatabaseCode() returning the native driver error code as int|string; getCode() is constrained to int by PHP's Throwable interface and cannot carry string SQLSTATE codes.

Debug

  • Added a Copy Details button to detailed HTML exception pages.

Model

  • Added chunkRows(), chunkById(), and chunkRowsById() methods to CodeIgniter\Model for processing large datasets in chunks.
  • Added encrypted, encrypted-json and encrypted-json-array casting for Entity properties and Model fields using the Encryption service. See encrypted.
  • Added firstOrInsert() method to CodeIgniter\Model that finds the first row matching the given attributes or inserts a new one. See firstOrInsert().
  • Added $insertOnlyFields and setInsertOnlyFields() to CodeIgniter\Model to remove configured fields from Model update operations while allowing them during inserts. See $insertOnlyFields.
  • Added $throwOnDisallowedFields and throwOnDisallowedFields() to CodeIgniter\Model to throw a DataException when write data contains fields that would otherwise be discarded by $allowedFields. See $throwOnDisallowedFields.
  • Added withLockedRow() to CodeIgniter\Model to run a callback with one row reloaded inside a transaction using lockForUpdate(). See withLockedRow().

Libraries

  • Cache: Added support for TTL callables in the remember() method of cache handlers. This allows you to specify a callable that returns a TTL value, which can be useful for dynamic TTL.
  • Context: This new feature allows you to easily set and retrieve normal or hidden contextual data for the current request. See Context for details.
  • Images: Added support for the AVIF file format.
  • Locks: Added Atomic Locks for owner-aware, cross-process mutual exclusion backed by supported cache handlers: File, Redis, Predis, and Memcached. Memcached support has driver-specific release limitations because Memcached has no atomic compare-and-delete command.
  • Logging: Log handlers now receive the full context array as a third argument to handle(). When $logGlobalContext is enabled, the CI global context is available under the HandlerInterface::GLOBAL_CONTEXT_KEY key. Built-in handlers append it to the log output; custom handlers can use it for structured logging.
  • Logging: Added per-call context logging with three new Config\Logger options ($logContext, $logContextTrace, $logContextUsedKeys). Per PSR-3, a Throwable in the exception context key is automatically normalized to a meaningful array. All options default to false.
  • Logging: Added the stopChainOnFailure handler option for FileHandler and ErrorlogHandler. Set it to false to let the remaining handlers run when the handler fails to write the log. It defaults to true, which keeps the existing behavior. See Stopping the Handler Chain on Failure.
  • Security: Added Fetch Metadata based CSRF protection with token fallback.

Helpers and Functions

  • Array Helper gained five new dot-path functions: dot_array_has(), dot_array_set(), dot_array_unset(), dot_array_only(), and dot_array_except().
  • Array Helper dot-path read operations now support object rows (including Entity) in dot_array_search(), dot_array_has(), dot_array_only(), dot_array_except(), and array_group_by(). dot_array_only() and dot_array_except() still return arrays. If the source itself is an object, it is read as an array-like value. Object values inside the source are kept unchanged when selected as a whole or left untouched. Partial object paths are returned as arrays.

HTTP

  • Added support for the HTTP QUERY method in routing, feature tests, CURLRequest, and FormRequest input source selection.
  • Added the retry option to CURLRequest for retrying failed responses with configurable delays, retryable status codes, optional transient cURL error retries, and Retry-After support. See retry.
  • Added Form Requests - a new FormRequest base class that encapsulates validation rules, custom error messages, and authorization logic for a single HTTP request.
  • Added IncomingRequest::input() to read GET, POST, JSON, and raw request data through InputData.
  • Added support for streaming responses. The StreamResponse class streams a body that is generated while it is being sent, such as large dataset exports or AI/LLM token streams, and the SSEResponse class extends it to stream Server-Sent Events (SSE) over HTTP. Both can be created via the new $this->response->stream() and $this->response->eventStream() factory methods. See Streaming Responses and Server-Sent Events (SSE).
  • Response and its child classes no longer require Config\App to be passed to their constructors. Consequently, CURLRequest's $config parameter is unused and will be removed in a future release.
  • Response now lazily loads the ContentSecurityPolicy, Cookie, and CookieStore classes only when used, instead of instantiating them on every request in the constructor. Requests that do not touch CSP or cookies no longer incur the cost of loading these classes.
  • CLIRequest now supports the -- separator to mean that what follows are arguments, not options. This allows you to have arguments that start with - without them being treated as options. For example: php index.php command -- --myarg will pass --myarg as an argument instead of an option.
  • CLIRequest now supports options with values specified using an equals sign (e.g., --option=value) in addition to the existing space-separated syntax (e.g., --option value). This provides more flexibility in how you can pass options to CLI requests.
  • Added enable() and disable() methods to ContentSecurityPolicy to enable or disable the CSP header for the current request. See Enable or Disable CSP at Runtime.
  • Added $enableStyleNonce and $enableScriptNonce options to Config\App to control whether nonces are automatically added to the style-* and script-* directives in the Content Security Policy (CSP) header when CSP is enabled. See Control Nonce Generation for details.
  • Added URI::withQuery(), URI::withQueryArray(), URI::withoutQueryVars(), and URI::withOnlyQueryVars() to return cloned URIs with replaced or filtered query variables.
  • Added URI::withQueryVar() and URI::withQueryVars() to return a cloned URI with query variables added or replaced.
  • URI now accepts an optional boolean second parameter in the constructor, defaulting to false, to control how the query string is parsed on instantiation. This is the behavior of ->useRawQueryString() brought into the constructor for convenience. Previously, you had to call $uri->useRawQueryString(true)->setURI($uri) to get this behavior. Now you can simply do new URI($uri, true).
  • CLIRequest now supports parsing array options written multiple times (e.g., --option=value1 --option=value2) into an array of values. This allows you to easily pass multiple values for the same option without needing to use a comma-separated string. When used with CLIRequest::getOption(), an array option will return its last value (for example, in this case, value2). To retrieve all values for an array option, use CLIRequest::getRawOption().

Validation

  • Custom rule methods that set an error via the &$error reference parameter now support the {field}, {param}, and {value} placeholders, consistent with language-file and setRule()/setRules() error messages.
  • Added Validation::getValidatedInput() to access validated data through a ValidatedInput object.

Others

  • Config: Added optional CodeIgniter\Config\Merge directives for Registrars to control replacing, deep merging, and ordered list additions. Existing registrar merge behavior is unchanged. See Controlling how values are merged.
  • Float and Double Casting: Added support for precision and rounding mode when casting to float or double in entities.
  • Added CodeIgniter\Input\InputData, ValidatedInput, and InputDataFactory for reusable typed input data objects.
  • Float and Double casting now throws CastException::forInvalidFloatRoundingMode() if a rounding mode other than up, down, even, or odd is provided.
  • Filters: Added RequestId filter for request tracing and correlation logging. The filter stores the request ID in the request context and automatically adds the X-Request-ID response header. Incoming X-Request-ID headers are used when valid. See Request ID for details.
  • Environment: Added CodeIgniter\EnvironmentDetector class and corresponding environment service as a mockable wrapper around the ENVIRONMENT constant. Framework internals that previously compared ENVIRONMENT directly now go through this service, making environment-specific branches reachable in tests via Services::injectMock(). See The environment service.
  • Time: Added Time::between(), Time::min(), and Time::max() comparison helpers. See between, min, and max.

Message Changes

  • Added new language keys:

    • Cache.unsupportedLockStore (CacheException::forUnsupportedLockStore())
    • CLI.commandAlias and CLI.helpAliases (command alias rendering in list and help)
    • CLI.altCommandNone, CLI.altCommandRun, and CLI.altCommandSelect (correcting a mistyped command)
    • CLI.generator.invalidCommandType (make:command rejecting an invalid --type)
    • CLI.generator.invalidReturnType (make:model rejecting an invalid --return)
    • Commands.invalidCommandAlias, Commands.commandAliasSameAsName, Commands.duplicateCommandAlias, Commands.aliasClashesWithCommandName, and Commands.aliasClashesWithAlias (command alias validation)
  • Removed deprecated language keys tied to removed exception constructors:

    • Core.missingExtension (FrameworkException::forMissingExtension())
    • HTTP.cannotSetCache (DownloadException::forCannotSetCache())
    • HTTP.disallowedAction
    • HTTP.invalidSameSiteSetting (HTTPException::forInvalidSameSiteSetting())
    • Security.invalidSameSite (SecurityException::forInvalidSameSite())
    • Session.invalidSameSiteSetting (SessionException::forInvalidSameSiteSetting())

Changes

  • Config: Added the md key for Config\Mimes::$mimes for Markdown files.
  • Router: Reduced route registration overhead in RouteCollection::create() by skipping placeholder replacement for route keys without : and calling helper('array') only once per collection.

Deprecations

  • CLI: The CLI::parseCommandLine() method is now deprecated and will be removed in a future release. The CLI class now uses the new CommandLineParser class to handle command-line argument parsing.
  • CLI: Returning a non-integer exit code from a command is now deprecated and will trigger a deprecation notice. Command methods should return an integer exit code (e.g., 0 for success, non-zero for errors) to ensure proper behavior across all platforms.
  • CLI: Commands::run() is now deprecated in favor of Commands::runLegacy() for legacy BaseCommand commands, and Commands::runCommand() for modern AbstractCommand commands.
  • CLI: The $commands parameter of Commands::verifyCommand() and the $collection parameter of Commands::getCommandAlternatives() are no longer used. Passing a non-empty array for either will trigger a deprecation notice.
  • CLI: BaseCommand is now deprecated in favor of AbstractCommand. Running a command that extends it triggers a deprecation notice. See Modern Spark Commands.
  • CLI: GeneratorTrait is now deprecated in favor of AbstractGeneratorCommand. See Modern Generator Commands.
  • CLI: The CLI.generator.viewName.cell language string is deprecated. It was never displayed, since make:cell only prompts for the class name.
  • CLI: The CLI::$foreground_colors, CLI::$background_colors, CLI::$lastWrite, CLI::$height, CLI::$width, CLI::$isColored, CLI::$io, and CLI::$stdoutToStderr properties are deprecated and no longer used. Their state lives in Color, Output, and Terminal. See CLI Output Classes.
  • CLI: CLI::validate() is deprecated and no longer used by the prompts. Calling it triggers a deprecation notice. Override Prompt::validate() instead. See CLI Prompt.
  • HTTP: The CLIRequest::parseCommand() method is now deprecated and will be removed in a future release. The CLIRequest class now uses the new CommandLineParser class to handle command-line argument parsing.
  • HTTP: URI::setSilent() is now hard deprecated. This method was previously only marked as deprecated. It will now trigger a deprecation notice when used.

Bugs Fixed

See the repo's CHANGELOG.md for a complete list of bugs fixed.