Allow the `TableLocator` to search across multiple configurable namespaces when resolving a table class.

Currently the locator always uses `'Model/Table'` as the only namespace. Accept an optional array of namespaces via the constructor (default `['Model/Table']`). When resolving an alias to a class name, if an explicit `className` option is not provided:

1. If a class with the generated name already exists, return it immediately.
2. Otherwise, iterate over the configured namespaces in order and use `App::className` to locate the table. Return the first matching class.
3. If no class is found in any namespace, fall back to the base `Cake\ORM\Table` (preserving the current behavior).

Plugin tables should be resolved within each custom namespace as well, respecting the plugin prefix (e.g., `TestPlugin.Infrastructure/Table` should map to `TestPlugin\Infrastructure\Table\*Table`).


As a developer building an HTTP middleware pipeline, I need the `CsrfProtectionMiddleware` to conform to the PSR‑15 `MiddlewareInterface` so that it can be used with any PSR‑15 compatible request handler, consistent with other middlewares in the project (e.g. `BodyParserMiddleware`).

**Acceptance Criteria**

1. The middleware must implement `Psr\Http\Server\MiddlewareInterface`.
2. The middleware must be invoked through the standard `process` method, which accepts a `Psr\Http\Message\ServerRequestInterface` and a `Psr\Http\Server\RequestHandlerInterface`, and returns a `Psr\Http\Message\ResponseInterface`.
3. All existing CSRF protection behavior must remain unchanged:
   - On `GET` requests where no CSRF cookie exists, the middleware must still create a token, attach it to the request (via the `_csrfToken` parameter), set a corresponding secure/HTTP‑only cookie with configurable name/path/expiry, and then delegate to the handler.
   - On state‑changing requests (`POST`, `PATCH`, `PUT`, `DELETE`), the middleware must still validate the token from the request data or `X-CSRF-Token` header against the cookie value, remove the token field from parsed data, and delegate to the handler on success.
   - On token mismatch or absence in state‑changing requests, the middleware must still raise an `InvalidCsrfTokenException`.
4. Delegation to the next processing step must use `$handler->handle($request)` instead of the previous callable signature; the middleware must not directly receive or return a framework‑specific `Cake\Http\Response` via its public API.

No changes are required to the token generation, cookie configuration, or validation algorithms themselves — only the middleware’s interface and delegation mechanism need to be updated.

Update CsrfProtectionMiddleware to PSR 15 standard.


We need the SQLite schema dialect to understand geospatial column types. Although SQLite does not natively support them, we store them as plain text and preserve the type name in the schema layer so that application code and the ORM can treat them as geospatial types.

**Acceptance Criteria**

1. **Introspection of existing tables**
   When reading the schema of a SQLite table whose column is declared as `GEOMETRY`, `POINT`, `LINESTRING`, or `POLYGON`, the column’s type returned by `_convertColumn()` must be `'geometry'`, `'point'`, `'linestring'`, or `'polygon'` respectively.
   - It must not default to `'text'` or `'integer'`.
   - The column type `'point'` must not be caught by the existing `'int'` substring check and mistaken for an integer type.

2. **Generation of CREATE TABLE column definitions**
   When `columnSql()` generates a column definition for a field whose type is one of the geospatial types (`TYPE_GEOMETRY`, `TYPE_POINT`, `TYPE_LINESTRING`, `TYPE_POLYGON`), the output must include the uppercase type name (e.g., `GEOMETRY`, `POINT`, etc.) rather than `TEXT`.
   - NOT NULL constraints (if the column is non-nullable) must be appended normally.
   - The `srid` column option is accepted but **must not** appear in the SQL output, because SQLite has no SRID support and cannot store it.

3. **Backward Compatibility**
   The existing integer-type heuristics (treating any column name containing `'int'` as an integer) must continue to work for all other type names except `'point'`. The addition of geospatial types must not break detection of `tinyint`, `bigint`, etc.

4. **No SRID persistence**
   Since SQLite does not preserve extra metadata like SRID in its column description pragma, the dialect should not attempt to read or store an SRID. This is a known limitation and can be documented as a future improvement.


**Bug Report: `plugin load` shell always writes to `config/bootstrap.php`, ignoring the Application class**

**Environment**
CakePHP 3.x application that uses an `Application` class (common since CakePHP 3.3+).

**Observed Behaviour**
Running `bin/cake plugin load SomePlugin` only modifies `config/bootstrap.php` by appending a `Plugin::load()` call. However, plugins are typically loaded from an `Application` class (`src/Application.php`) using `$this->addPlugin()` inside its `bootstrap()` method. Because the shell does not update the Application class, the plugin is not actually loaded when the application boots.

**Expected Behaviour**
When an `Application.php` file exists in the app’s `src` directory, the command should add a `$this->addPlugin('SomePlugin', [...])` call inside the `bootstrap()` method of that class. If the Application class is missing or the `--no-app` flag is passed, it should fall back to the current behaviour of modifying `config/bootstrap.php`.

The command’s existing options (`--bootstrap`, `--routes`, `--autoload`) should produce the corresponding array entries in the `addPlugin` call, exactly as they do for the `Plugin::load()` syntax. If the Application class exists but has no `bootstrap()` method, the command should abort with a clear error message instructing the user to add one.

**Steps to Reproduce**
1. Have a CakePHP 3.x app with `src/Application.php` containing a `bootstrap()` method.
2. Run `bin/cake plugin load ContactManager`.
3. Open `config/bootstrap.php` – it now contains `Plugin::load('ContactManager');`.
4. The Application’s `bootstrap()` method is unchanged, so the plugin is not bootstrapped.

**Impact**
The `plugin load` command is practically broken for any project using the Application class pattern, forcing developers to manually edit `Application.php` after running the shell. This undermines the convenience of the task and leads to confusion when plugins don’t appear to load.

Add plugins via application class methods.


**Feature Request: Separate Form schema getter/setter for fluent configuration and unambiguous access**

The `Form::schema(?Schema $schema = null): Schema` method mixes retrieval and mutation in a single call. This makes fluent configuration impossible (setting the schema returns the schema, not the form) and introduces ambiguity about what `null` means. Users should be able to set the schema via a dedicated method that returns `$this` for chaining, and query it via a dedicated getter that always returns the current schema instance.

**Acceptance Criteria**

1. **Fluent setter**
   `Cake\Form\Form` exposes `setSchema(\Cake\Form\Schema $schema): self`. Calling it replaces any existing schema and returns the form instance, enabling fluent calls like `$form->setSchema($s)->validate($data)`.

2. **Unambiguous getter**
   `Cake\Form\Form` exposes `getSchema(): \Cake\Form\Schema`.
   - On first invocation the getter lazily builds the schema using the existing `_buildSchema` hook.
   - Subsequent calls return the same instance (unless `setSchema` is called again).
   - The getter must **never** accept arguments.

3. **Deprecation of the combined method**
   The existing `schema(?Schema $schema = null): Schema` method remains functional but is deprecated. It must delegate to `setSchema()` when a non-null argument is given and to `getSchema()` when called with no argument, preserving backward-compatible behavior.

4. **Lazy-initialization invariant**
   The `_buildSchema` hook is only invoked when the schema is first accessed (via `getSchema` or via constructing the form with no prior `setSchema`). Explicitly calling `setSchema` must not trigger the build hook, and it overrides any lazily-built schema.

5. **Debug representation**
   The `__debugInfo` output for a form must use `getSchema()` rather than the deprecated `schema()` method to retrieve schema internals.

**Non-obvious constraints**

- The new setter must be typed to accept only `Schema` instances (no nullable parameter).
- The getter must guarantee the returned object is the active schema; if the schema was replaced after lazy initialization, the getter returns the replacement, not a stale reference.
- Calls to the deprecated method with `null` must still return the current schema, aligning with the old getter behavior.

Split Form::schema() into getter and setter


**Title:** Binary UUID primary key generates binary string instead of UUID string on save

**Observed behavior**
When using a table that defines its primary key as `binaryuuid`, saving a new entity without an explicit `id` results in an entity whose `id` property contains a raw binary string instead of a human-readable UUID (e.g., `550e8400-e29b-41d4-a716-446655440000`). This makes the ID unusable for display, routing, or API responses without manual conversion.

**Expected behavior**
Saving a new record should automatically set the entity’s `id` to a valid, 36-character dashed hex UUID string (matching `/^[a-f0-9-]{36}$/`), exactly as the existing string-based `uuid` primary key type does. The binary conversion for storage should be an internal database concern, not exposed to the entity.

**Steps to reproduce**
1. Create a table with `'id' => ['type' => 'binaryuuid']` in its field definition.
2. Create a new entity without setting `id` and save it via `TableRegistry::get(…)‑>save()`.
3. Inspect the returned entity’s `id` value – it will be a binary string, not a UUID string.

**Impact**
Applications that rely on binary UUID columns cannot safely read the primary key after insertion without additional workarounds. This breaks parity with the plain `uuid` type, where `id` is always a proper UUID string. Any integration tests that assert on the format of a generated ID will fail for `binaryuuid` tables.

**Suspected cause**
`Cake\Database\Type\BinaryUuidType::newId()` currently returns the result of `convertStringToBinaryUuid(Text::uuid())`, making the generated identifier a binary value from the start. Instead, it should return a plain string UUID (like `Text::uuid()`) and let the `toDatabase()` method handle the binary conversion when persisting.

**Request**
1. Fix `BinaryUuidType::newId()` to return a human-readable UUID string, matching the behavior of the string `uuid` type.
2. Extend the existing UUID table integration tests (`testSaveNew`, `testSaveUpdate`, `testDelete`, `testEmptyUuid`) to also run against a `binaryuuid`‑keyed table, so that all primary key workflows are verified to work identically for both storage formats.


We need a way to hide certain console commands from the help listing and shell completion without preventing them from being executed. Currently, all registered commands appear in `bin/cake`’s list of available commands and in the autocomplete suggestions generated by the built-in completion command. This is problematic for internal or infrastructure commands (e.g., maintenance tasks, one-off data migrations, or developer-only debugging commands) that we want to keep accessible but shouldn’t be exposed to all users.

**Observed behavior:**
- Running `bin/cake` without arguments or with `help` shows every command, including those we’d prefer to keep hidden.
- The shell completion (via `completion`) also returns these commands as suggestions, which clutters the autocomplete output and can confuse users.

**Expected behavior:**
- It should be possible to mark a command so that it is omitted from both the human-readable help output (text mode) and the XML representation used by external tools.
- The same command should also be excluded from the completion suggestions generated by the `CompletionCommand`.
- The hidden command must remain fully executable when invoked directly (e.g., `bin/cake internal:task`), with its normal options and behavior intact.

**Impact:**
- Users are exposed to internal commands that may not be relevant or safe for them to know about, cluttering the help and autocomplete experience.
- There is no existing mechanism (e.g., an interface or configuration flag) to control visibility of a command from the default output channels.

A solution should be lightweight and easy for command authors to adopt—ideally a simple marker that the existing `HelpCommand` and `CompletionCommand` can check when building their lists, without requiring method overrides or complex configuration. It must not affect the execution path of the command itself, and it should be backward-compatible (commands that don’t opt in continue to behave exactly as they do today).

5.next Allow to hide commands from help output (#19101)


**Problem**
When implementing a controller action, middleware, or component logic that needs to trigger a redirect (e.g. authentication failures, authorization checks, maintenance-mode, or forced sign-in), the only official redirect exception available is `Cake\Routing\Exception\RedirectException`. This class is designed for internal use by the routing layer and is not caught by the global `ErrorHandlerMiddleware`. Throwing it outside of routing code results in an unhandled exception and a generic error page, not a redirect response.

Developers are forced to either manually construct and return a redirect response object at the call site, which breaks exception-based flow control, or to couple their code tightly to the routing layer’s handling.

**Expected behavior**
1. A new, general-purpose redirect exception (`Cake\Http\Exception\RedirectException`) should be available that can be thrown from any application code (controllers, middleware, components, services, etc.).
2. The `ErrorHandlerMiddleware` should recognise this exception and automatically convert it into a proper HTTP redirect response, before it reaches the generic exception handler.
3. The response should:
   - Use the URL given in the exception as the `Location` header.
   - Use the status code provided (default `302`).
   - Include any additional headers specified when throwing the exception.
   - Have no response body.
4. Custom headers should be addable and removable via the exception object, allowing callers to attach, for example, flash message cookies, session markers, or CORS headers.

**Impact**
Without this, there is no clean way to trigger a redirect via exception outside of routing. Workarounds require every potential redirect point to manually build and return a response, which cannot be raised up through nested middleware or layers that rely on exception bubbling. This leads to inconsistent error/redirect handling and bypasses the middleware pipeline’s error envelope.

**Existing reference**
- `src/Routing/Exception/RedirectException` exists but is explicitly for routing-layer use and is not processed by `ErrorHandlerMiddleware`.
- `ErrorHandlerMiddleware::process()` currently only catches `Throwable` generically; it should intercept the new `RedirectException` first and handle it separately.


The `EncryptedCookieMiddleware` needs tests verifying its core behavior:
- Incoming request cookies for configured names are decrypted.
- Outgoing response cookies set via `withCookie()` are encrypted for those names.
- Outgoing response cookies set via raw `Set-Cookie` headers are also encrypted.

Additionally, a bug must be fixed: `encodeSetCookieHeader()` currently returns `$request->withHeader(...)` instead of the response, so encrypted `Set-Cookie` headers are never actually applied to the response.

Clean up the internal `_encrypt` calls: the key is no longer needed as an argument because the `CookieCryptTrait` obtains it through `_getCookieEncryptionKey()`.

Cover edge cases where plain cookies (not in the configured list) must never be modified.

Add outline of tests for EncryptedCookieMiddleware.


When using a data URI as an image source (e.g. `$this->Html->image('data:image/png;base64,...')`), the generated `src` attribute incorrectly includes the application’s base path or webroot, breaking the URI. The same issue affects other asset URL methods such as `UrlHelper::assetUrl`. Data URIs should be recognized and passed through unmodified (suitably HTML-escaped) instead of being treated as filesystem paths.

**Observed behavior**
In a CakePHP application with a base path (e.g. `/subdir/`), calling `$this->Html->image('data:image/png;base64,iVBORw0KGgo...')` produces:
```html
<img src="/subdir/data:image/png;base64,iVBORw0KGgo..." alt="" />
```
The URI is no longer valid, and the asset fails to load.

**Expected behavior**
Data URIs matching the pattern `data:[<mediatype>][;base64],<data>` (per RFC 2397) should be detected during URL generation. For such URIs the helper must:
- Not prepend any base directory, webroot, or theme path.
- Not apply asset timestamping.
- HTML-escape the URI to prevent XSS, using the existing `h()` function.

The resulting markup should be:
```html
<img src="data:image/png;base64,iVBORw0KGgo..." alt="" />
```

**Affected methods**
- `HtmlHelper::image()`
- `UrlHelper::assetUrl()`
- Any other helpers that internally use `assetUrl()` for asset paths (e.g., CSS, script helpers).

**Safety constraint**
Even though data URIs should not be transformed into a different URL structure, they must still be HTML-escaped (e.g., `h($path)`) to avoid injection attacks. The escaping alone is sufficient; no other URL processing should occur.

**Additional edge cases**
- Data URIs with different MIME types (`data:image/svg+xml;base64,...`, `data:text/html,...`) should work.
- Data URIs without base64 encoding (`data:image/png,<data>`) should also be handled.
- The detection should be robust enough to avoid false positives with ordinary paths that happen to contain “data:” elsewhere. A check such as `preg_match('/^data:[a-z]+\/[a-z]+;/', $path)` would be appropriate.

Enable data URIs to be used in asset URLS.


The console runner should invoke plugin bootstrap and command hooks when the application supports them, enabling plugins to load their own console services and register commands. After the application's `bootstrap()` call, the runner should call `pluginBootstrap()` if the app implements `PluginApplicationInterface`. Likewise, after the application's `console()` returns the command collection, the runner should call `pluginConsole($commands)` to let plugins add commands. These calls must be skipped when the app does not implement the interface, preserving backward compatibility.

Integrate new plugin hooks into the console runner


**Title:** Parent entity becomes dirty when containing an association that uses `formatResults`

**Description:**

When I fetch a record with a contain that uses an association whose custom finder applies a `formatResults` callback (to modify the associated entity after hydration), the parent entity is incorrectly marked as dirty. The same problem occurs when the Translate behavior is loaded on the target table of a `BelongsTo` or `HasOne` association, because the behavior internally modifies translated entities during result formatting.

This means that after a simple `find()->contain()`, `$entity->isDirty()` returns `true` even though no user code has changed any field. Any subsequent `save()` on the parent entity will trigger an unnecessary (and potentially harmful) UPDATE query.

**Steps to reproduce:**

1. Define a `BelongsTo` association (e.g., `Articles` belongs to `Authors`).
2. Configure that association to use a custom finder that modifies the associated entity in a `formatResults` callback, for example adding a virtual `formatted` property.
3. Execute `$article = $articles->find()->where(['Articles.id' => 1])->contain('Authors')->firstOrFail();`
4. Check `$article->isDirty()` and `$article->author->isDirty()`.

**Observed behavior:**
- `$article->isDirty()` returns `true` (the parent entity is dirty).
- The associated `author` entity may or may not be dirty depending on the modifications made by the formatter.
- The database record was only read; no changes should be flagged.

**Expected behavior:**
- `$article->isDirty()` should return `false` because the entity was just fetched from the database without any intentional changes by the application.
- The associated `author` entity should be clean if its internal state represents a persisted record (i.e., no user-provided data was set).

**Additional context:**
- The same issue occurs with `HasOne` associations and when the Translate behavior is attached to the target table (e.g., `Authors` with `TranslateBehavior` on `name`, or `Articles` with `TranslateBehavior` on `title`). After `$articles->get(1, ['contain' => 'Authors'])`, the parent entity is dirty.
- This happens because the `formatResults` callback is executed after the association’s result set is injected into the parent entity. Modifications to the associated entity at that point mark the parent as dirty.
- The problem is a regression from the normal behavior where associations loaded from the database leave the parent clean.

**Impact:**
- Unintended UPDATE queries are executed on the parent table whenever a fetched entity is passed to `Table::save()` or `Entity::save()`.
- Application logic that relies on `isDirty()` to detect user changes may trigger false positives.
- Data integrity can be compromised if callbacks or save strategies depend on the dirty state.

**Possible hints:**
- The `_formatAssociationResults()` method in `Association` attaches results and then applies the formatter. If the formatter modifies entities (e.g., setting a virtual property), the parent’s dirty flag is set.
- A solution would ensure that after the formatter runs, the associated entities are explicitly cleaned (via `clean()`) so that the parent remains clean. The performance-critical aspect should be considered, but correctness is the priority.

**Request:**
Please ensure that when an association with `formatResults` is loaded via `contain` or `loadInto`, the parent entity remains clean (i.e., `isDirty()` returns `false`) as long as no user code has explicitly modified it.

Fix dirty associations that use formatResults


**Feature Request: `plugin list` command should display plugin flags from config**

As a developer managing multiple plugins, I want the `bin/cake plugin list` command to show not just plugin names, but also whether each plugin is restricted to debug mode, CLI-only, or marked as optional — based on the project’s `config/plugins.php` configuration file.

**Acceptance Criteria**

1. **Table output**
   When at least one plugin is loaded, the command outputs a table with the following columns: `Plugin`, `onlyDebug`, `onlyCli`, `optional`.

2. **Flag markers**
   For each loaded plugin:
   - If the plugin appears in `config/plugins.php` and has the `onlyDebug` setting set to `true`, an `X` is shown in the `onlyDebug` column.
   - Likewise for `onlyCli` and `optional` settings.
   - If the plugin is not listed in the config file, or its entry does not contain a truthy value for a given setting, the corresponding cell remains blank.

3. **Configuration file format**
   The command reads the file at `CONFIG . 'plugins.php'` (the standard app configuration file). This file is expected to return an array. Entries can be:
   - A plain string (just the plugin name), or
   - An associative array with optional keys `onlyDebug`, `onlyCli`, `optional` (each boolean).
   The command handles both formats transparently.

4. **No plugins case**
   If no plugins are loaded, the existing warning message (“No plugins have been found.”) is displayed and no table is rendered.

5. **Existing `plugins` config compatibility**
   The list of loaded plugins is still sourced from `Configure::read('plugins')`. The new flag information is an overlay from `config/plugins.php`; the underlying plugin list behavior is otherwise unchanged.

6. **Console table rendering**
   The table is produced using the framework’s built-in table helper, ensuring consistent formatting in the terminal.


**Title:** `routes` CLI command should warn about route template collisions

**Background**
The `bin/cake routes` command currently outputs a table listing all connected routes. If multiple routes share the same URI template (path), only the first matching route will ever be used at runtime. This can silently hide misconfigurations, making debugging difficult.

**Desired behavior**
Enhance the `routes` command to detect and report **duplicate route templates** (collisions). After printing the main route table, the command should:

1. Identify any URI template that appears more than once across all connected routes (regardless of plugin, prefix, HTTP method constraints, or route name).
2. If duplicates are found, output a clear warning message (e.g., “The following route collisions were detected”) followed by a second table listing only the colliding routes, using the same columns as the main route table (Route name, URI template, Plugin, Prefix, Controller, Action, Method(s)).
3. If no duplicates exist, the command output should remain exactly as it is today – no extra warning or table.

**Constraints & edge cases**
- A route is considered colliding purely based on `template` equality; route names, methods, or other options are not considered distinct.
- The warning table should include all routes that share a duplicated template, not just one representative.
- The exit code must stay `CODE_SUCCESS` (0) when collisions are found – this is a warning, not an error.
- The main route listing (unsorted/sorted) must continue to behave as before; the new detection logic should not alter the ordering or content of that table.
- If the command is invoked with the `--sort` option, the main table ordering applies, but the collision warning table can list routes in the order they appear in the route collection (no sorting requirement).

**Why**
Many developers accidentally connect two routes with the same path (e.g., a catch-all after a specific route) and don’t realise that only the first one is reachable. An explicit warning in the `routes` output will reduce debugging time and encourage correct routing setup.


When using the new-style plugin system—adding plugins via `application->addPlugin('Vendor/Plugin')` or by passing `PluginInterface` instances directly—the path mappings defined in `vendor/cakephp-plugins.php` are never loaded. This file is generated by the `cakephp/plugin-installer` and is the mechanism that resolves custom plugin locations when applications use `extra.plugin-paths` in `composer.json`. As a result, plugins placed outside the standard `Plugin` directories are not found, even when a valid entry exists in `cakephp-plugins.php`.

Steps to reproduce:
- Define a custom plugin path via `extra.plugin-paths` in the composer.json of a CakePHP application.
- Load the plugin using `$app->addPlugin('SomePlugin')` without providing a `'path'` config key, relying on resolution from the configuration file.
- The plugin fails to load because the configuration file is never read.

Expected behavior:
- The plugin path configuration from `vendor/cakephp-plugins.php` should always be loaded, regardless of whether the old `Plugin::load()` or the new `addPlugin()`/`PluginCollection` mechanism is used.
- `PluginCollection` (or the underlying application setup) should load the file early enough so that path resolution works for all plugin loading styles.
- When a plugin is added without an explicit path, `PluginCollection` should centralize the path lookup logic: first checking Configure data (`Configure::read('plugins.$name')`), then falling back to scanning `App::path('Plugin')`, and should throw a `MissingPluginException` if no location is found.
- The `BaseApplication::makePlugin` method (which creates plugin instances from name + config) should be able to leverage this centralized path resolution.

Impact:
- Applications depending on custom plugin paths (e.g., to keep certain plugins outside of `vendor/`) break silently when switching to the new plugin API.
- Logic for path resolution is duplicated between `Plugin::load()` and other parts of the framework, leading to inconsistencies and maintenance burden.
- Making the configuration loading and path resolution a responsibility of `PluginCollection` (and adjusting visibility of `makePlugin`) simplifies the code and removes the dependency on the old `Plugin` class for future deprecation.

Move plugin configuration file loading to PluginCollection


Implement `addPlugin()` in `BaseApplication` so it accepts either a plugin instance (implementing `Cake\Core\PluginInterface`) or a fully‑qualified class name string.
When given a string:
- throw `InvalidArgumentException` if the class does not exist,
- throw `InvalidArgumentException` if the class does not implement `PluginInterface`,
- otherwise instantiate the plugin (passing any configuration array).

Add the resulting plugin to the application’s `PluginCollection`. Expose the plugin collection through a `getPlugins()` method so that added plugins can be inspected.


Allow `WindowExpression::partition()` and `WindowExpression::order()` (via `WindowInterface`) to receive a `Closure` as their argument.
The closure must be called immediately, and its return value must be:

- for `partition()`: an `ExpressionInterface` (e.g. an aggregate or identifier expression)
- for `order()`: an `OrderByExpression`

If the closure returns anything else, throw a `RuntimeException` with a descriptive message.
This makes both methods consistent with the existing `Query::window()` which already supports a closure for lazy window construction.

Added closure support to partition() and order()


Add a `routes(RouteBuilder)` hook method to `BaseApplication` so applications can define their routes directly, rather than relying on the global `config/routes.php` auto-loading inside `Router`. The default implementation should load `config/routes.php` from the application’s configured config directory to preserve backward compatibility. Update `RoutingMiddleware` to accept an optional `BaseApplication` instance and call the `routes` hook if available; when the hook executes, the old automatic route-loading path in `Router` must be disabled to avoid double-loading. Deprecate the `Router::_loadRoutes()` automatic loading mechanism, as it will be removed in 4.0.0.


Repair `Request` retrieval methods to return correct values instead of `null` in the following scenarios:

- When calling `header()` with a `null` filter (either explicitly or via the default parameter), the method must return the raw header value. Passing `null` as the filter should be treated as `FILTER_DEFAULT`, not cause `null` to be returned.
- When calling `server()` (or any internal use of `fetchGlobal` with `INPUT_SERVER` or `INPUT_ENV`), the value must be retrieved directly from `$_SERVER`/`$_ENV` and processed with `filter_var`, because `filter_input` with those types is unreliable on some FastCGI server configurations.
- Null filters passed to `fetchGlobal` (for any input type) must be replaced with `FILTER_DEFAULT` so that the method always returns the unfiltered value when no filter is specified.
- The behavior of other input sources (GET, POST, etc.) must remain unchanged; only server and environment retrieval should be adapted to avoid the `filter_input` issues.

Patching up some Request object issues and more tests on it.


**Feature: Time utilities for testability and flexible instantiation**

As a developer writing tests or working with external date-time sources, I need the `Time` class to support freezing the current time during tests and to accept timestamps, custom formats, and raw `DateTime` objects.

**Acceptance criteria**

1. **Freeze `now()` for testing**
   - Calling `Time::setTestNow($time)` with a `Time` instance, a string parseable by `Time`, or a `DateTime` (converted to `Time`) causes all subsequent calls to `Time::now()` (and to `new Time()` with no time argument) to return that frozen moment instead of the real current time.
   - `Time::setTestNow(null)` restores real-time behaviour.
   - `Time::hasTestNow()` returns `true` when a frozen time is active, `false` otherwise.
   - When a frozen time is active, the timezone of the frozen instant is used by default for new `Time` instances that omit the timezone argument.

2. **Factory method from a custom format**
   - `Time::createFromFormat($format, $datetime, $timeZone = null)` should return a `Time` instance matching the parsed value, while applying the given timezone (instead of inheriting the input string’s ambiguous zone as raw `DateTime::createFromFormat` would). If no timezone is given, it uses the default timezone.

3. **Factory method from a Unix timestamp**
   - `Time::createFromTimestamp(int $timestamp, $timeZone = null, string $locale = null)` creates a `Time` at the given Unix time. Omitted timezone and locale fall back to defaults.

4. **Factory method from any `DateTime` instance**
   - `Time::instance(\DateTime $dateTime, string $locale = null)` returns a `Time` with the same date-time and timezone as the provided `DateTime`. If a locale is supplied, it overrides the default locale.

**Invariants and edge cases**
- All new factory methods must respect immutable `Time` behavior (since `Time` extends `DateTime`, which can be mutable, but these methods should return fresh instances without side effects).
- When a frozen time is set, `new Time(null)` must produce the frozen instant with its timezone, even if a different timezone is the system default.
- Existing public API (e.g., `parse`, `toDateTimeString`, locale handling) must not be affected.
- Relative keywords in the `$time` string of `setTestNow` should be evaluated at the moment `setTestNow` is called, not continuously deferred. (Implied by storing a concrete `Time` after parsing.)

More constructors and testing utilities on Time.


**Feature request: Language override priority and bundle integrity**

We need to ensure that the language system reliably prioritizes user‑supplied message files over the framework defaults, and that all built‑in language bundles are well‑formed.

### Acceptance criteria

1. **`getLocale()` returns the active locale**
   `$language->getLocale()` must return the locale string that was set during construction (e.g., `'en'`).

2. **Prioritised locator overrides system messages**
   When a language file exists both in the core `system/Language/` path and in a higher‑priority locator path (e.g. an application language directory), the higher‑priority file’s values must be used.
   - Example: if the higher‑priority `Number.php` defines `'trillion' => ' lots'`, then `lang('Number.trillion')` returns `' lots'` instead of the system default `' trillion'`.
   - Any keys that appear **only** in the higher‑priority file (e.g. `'bazillion'`) must also be accessible.

3. **All core message bundles are valid and loadable**
   Every framework‑shipped message bundle (CLI, Cache, Cast, Core, Database, Email, Files, Filters, Format, HTTP, Images, Language, Log, Migrations, Number, Pager, Router, Session, Time, Validation, View) must load without errors or warnings when registered, and each must return an array whose keys are unique – no duplicate keys that could silently overwrite values.


The Entity’s `__isset` and `__unset` do not respect the `$datamap` direction. When a property is mapped to a database column (e.g., `'bar' => 'foo'`), both the property name and the column name respond to `isset()` and `unset()`. This makes the database column name visible as a public property, which violates the intended mapping. Only the property name should be usable with `isset()` and `unset()`; the column name should behave as if it is not a defined property.

Fix `__isset` to return `false` for keys that are database column names present as values in `$datamap`. Fix `__unset` to be a no‑op for such keys. Ensure the existing behavior for the actual property name remains unchanged.


We need a way to display relative time strings on a `Time` object, like “3 weeks ago”, “in 2 days”, or “just now”, so that UIs can show human-friendly timestamps to users.

## Desired Behavior

Add a public method `humanize()` to the `CodeIgniter\I18n\Time` class that returns a localized string representing the difference between the instance and the current time (in the instance’s timezone):

- If the difference is less than a minute, return the translation for “just now”.
- For differences ≥ 1 minute, choose the largest whole unit (years, months, weeks, days, hours, minutes) that fits the difference.
- Format the string depending on direction:
  - **Past:** `"{quantity} {unit} ago"` (e.g., “3 years ago”, “1 hour ago”)
  - **Future:** `"in {quantity} {unit}"` (e.g., “in 2 weeks”, “in 1 day”)
- Units must honor grammatical plurality (e.g., “1 day” vs “2 days”).
- Weeks are used for differences of 7 or more days (rounded up) when smaller than a month; otherwise days are used for 1–6 day differences.
- The method must always use the `Time` instance’s timezone for comparison, not UTC.

## Constraints & Edge Cases

- The comparison must be calendar-aware to correctly handle varying month lengths, leap years, and DST transitions.
- The translations and plural forms must come from the existing language file (`system/Language/en/Time.php`). That file should be extended with:
  - Pluralizable translation keys for each time unit (`years`, `months`, `weeks`, `days`, `hours`, `minutes`) using ICU MessageFormat syntax.
  - A key for the past suffix (`ago`).
  - A key for the future prefix (`inFuture`) that will appear before the quantity.
  - A key for the “just now” case (`now`).
- The method must work correctly when the current time is changed via `Time::setTestNow()` (i.e., respect the test-clock).


## Debug Toolbar renders even when `toolbar` filter is disabled

When the `toolbar` filter is disabled (e.g., by removing `'toolbar'` from `Config\Filters::$globals['after']` or using an `except` rule), **the toolbar HTML and its surrounding comments still appear** in the final page output.

`View::render()` only checks `$this->debug` and the `debug` render option; it does not verify whether the `DebugToolbar` filter is actually active for the current request.
Therefore filtering configuration cannot suppress the toolbar.

### Expected behavior
- The toolbar must be injected **only when `CodeIgniter\Filters\DebugToolbar` is present in the executed “after” filter list**.
- The View must not rely on `$debug` alone; it must consult the resolved filter class list.

### Implementation requirements
1. **Filters service must expose the list of resolved filter class names** for a position (e.g., `getFiltersClass()['after']`).
   - This list should already account for aliases, multiple classes per alias, and any filtering (`except`/`only`).
2. **`View::render()` must look up that list** and only append the toolbar collectors if `\CodeIgniter\Filters\DebugToolbar` is present (exact string comparison).
   - All existing `debug` flag and option checks remain in place; they should be combined with this new filter‑presence check via `&&`.

fix. toolbar disable,view comment not remove (#3010)


The Query Builder's `like()` method (and related `orLike`, `notLike`, `orNotLike`) always generates a case-sensitive `LIKE` clause. This is a problem when the database or table uses a case-sensitive collation (e.g., PostgreSQL with `en_US.UTF-8` collation or MySQL `utf8_bin`). There is currently no built-in way to force a case-insensitive search: users must either use raw SQL or manually wrap everything in `LOWER()` calls, which is error-prone and reduces readability.

**Expected behavior**
A new optional parameter should be added to all four LIKE methods that, when set to `true`, causes the generated SQL to use `LOWER(column) LIKE :bind` (or `LOWER(column) NOT LIKE :bind` for the NOT variants), and the bound match value should be lowercased before binding. This should work regardless of the `side` parameter (both, before, after, none) and should still support the `ESCAPE` clause when `$escape` is true.

**Example**
```php
$builder->like('name', 'Developer', 'both', null, true);
// Expected SQL: SELECT * FROM "job" WHERE LOWER(name) LIKE :name ESCAPE '!'
// :name bound to '%developer%'
```

**Edge cases / constraints**
- When `$insensitiveSearch` is true, only the column should be wrapped in `LOWER()` in the SQL string; the bound value is already lowered by the builder.
- The `ESCAPE` clause (when `$escape === true` and a platform-specific escape string is defined) must still be appended after the LIKE expression.
- The change should be backward-compatible: the new parameter defaults to `false`, preserving existing behavior.

**Impact**
This addition would allow developers to easily write case-insensitive LIKE queries across all supported databases without resorting to raw SQL, improving the usability and portability of the Query Builder.


The `Filters::run()` method silently executes filter classes that don’t implement `FilterInterface`, leading to fatal errors or undefined behavior at runtime. Additionally, the method provides no way to retrieve the request or response after filters have been applied, making it impossible to inspect or propagate modifications made by filters. Invalid filter aliases (not present in the configuration) also cause cryptic failures rather than clear exceptions.

**Expected behavior:**
1. If a filter class specified in `$config->aliases` does not implement `CodeIgniter\Filters\FilterInterface`, `run()` should throw a `RuntimeException` with a descriptive message.
2. `run()` must return the request object when executing `before` filters, and the response object when executing `after` filters, so callers can access any changes made by the filter.
3. An invalid alias (not defined in the aliases array) should cause `run()` to throw an `InvalidArgumentException`.

**Current behavior:**
- No interface check is performed; calling methods on an object that lacks `before()` or `after()` results in a fatal error.
- `run()` returns `void`, discarding any request/response object modifications.
- Missing aliases trigger generic array key errors or unexpected nulls rather than a clear, catchable exception.

These issues make filter configuration error-prone and limit the usefulness of the filtering system for request/response manipulation.


Add an overwrite flag to `UploadedFile::move()` that controls destination uniqueness. When `overwrite` is `true`, the method must **not** overwrite an existing file; instead compute a unique path via a new `getDestination()` method that appends `_n` before the extension, incrementing any existing numeric suffix after the delimiter. When `overwrite` is `false`, the file is moved to the exact given path (may overwrite). Expose `getDestination()` in `UploadedFileInterface`.


Fix the global `redirect()` helper to resolve named routes instead of treating the argument as a raw URI path.
Currently, `redirect('route_name')` calls `$response->to($uri)`, which does not look up named routes.
Change it to use `$response->route($uri)` so that `redirect('login')` redirects to the route named `login` when one exists.

Also ensure that cookies set on the returned response (e.g., `redirect()->route('login')->setCookie(...)`) are actually sent when the response is output.

More unit tests, and fix Common::redirect()


The `isWriteType()` method does not recognise write queries that start with a Common Table Expression (WITH clause). For example, `WITH … INSERT …` or `WITH … UPDATE …` are incorrectly treated as non‑write operations.

Additionally, on PostgreSQL, the method already treats `INSERT … RETURNING …` and `UPDATE … RETURNING …` as read operations, but `DELETE … RETURNING …` is missing from that list and is therefore wrongly classified as a write query.

Acceptance criteria:
1. Calling `BaseConnection::isWriteType($sql)` (and its subclass overrides) on a query string that has optional leading whitespace followed by a `WITH` clause, then one of the recognised write keywords (`SET`, `INSERT`, `UPDATE`, `DELETE`, etc.) must return `true`.
2. For the PostgreSQL driver only, `Postgre\Connection::isWriteType($sql)` must return `false` when the query string, after optional leading whitespace and an optional `WITH` clause, starts with `INSERT`, `UPDATE`, or `DELETE` and is immediately followed by a `RETURNING` clause (i.e. the query is meant to return data).
3. Leading whitespace anywhere in the query should not affect the classification result.

fix: isWriteType() to recognize CTE; [Postgre] allow beginning whitespace at query start, RETURNING with DELETE


Add a `$helpers` property to `app/Config/Autoload.php` (an array of helper names, e.g., `['form', 'html']`). After the autoloader is initialized, those helpers must be loaded automatically using the framework’s `helper()` function, mirroring the way `$files` loads arbitrary files. This allows helpers to be available globally without manual `helper()` calls in every controller.


**Title:** `negotiateMedia` should enforce media type/subtype matching, not only parameter matching

**Affected component:** `system/HTTP/Message.php` – `negotiateMedia` (and its internal matching chain: `getBestMatch`, `match`)

**Problem:**
Currently `negotiateMedia` can return a supported media type that does not actually match the type/subtype portion of the client’s `Accept` header. The matching logic in `match` compares only the parameters (e.g. `q`, `charset`) but ignores the primary type and subtype, so a request with `Accept: image/*` could currently match `text/plain` if parameter conditions happen to pass.

**Desired behaviour:**
When negotiating media types, the algorithm must additionally verify that the type and subtype from the `Accept` entry and the supported type are compatible:
- Exact types must match (e.g. `image` must equal `image`).
- A wildcard subtype (`*`) in the client’s accept entry matches any subtype of that type (e.g. `text/*` should match `text/plain` but *not* `image/png`).
- A full wildcard `*/*` should match any type and subtype.
- If type or subtype do not match, that accept entry is skipped for that supported type.

**Non-goals / safety constraints:**
- Do **not** alter the matching behaviour for `negotiateCharset`, `negotiateEncoding`, or `negotiateLanguage` – those are type/subtype‑less and must continue using only parameter matching.
- The existing precedence rules (highest `q` value, more specific before wildcard) must remain unchanged; only the type/subtype filter is added as an initial gate before parameter comparison.

**Acceptance criteria (observable):**
- `negotiateMedia(['text/plain'])` with `Accept: text/*` returns `text/plain`.
- `negotiateMedia(['text/plain'])` with `Accept: image/*` *does not* return `text/plain`; it falls back to the first supported element only if no other match exists (preserving the fallback contract).
- The method still correctly handles complex headers like `Accept: text/*, text/plain;format=flowed, */*` and selects the first supported type that satisfies both type/subtype and parameter constraints according to quality/precedence.

NegotiateMedia command can recognize types and subtypes now.


Fix `BasePreparedQuery::prepare()` placeholder conversion so it doesn’t break PostgreSQL cast syntax or time literals.

**Problem**
When SQL contains named placeholders (`:name`, `:name:`) alongside PostgreSQL-style casts (`::type`) or string literals like `'12:34'`, the current regex `:[^\s,)]+` replaces the double‑colon of the cast as well, generating invalid SQL.

**Desired behaviour**
Only convert named placeholders that match `:[a-zA-Z_][a-zA-Z0-9_]*:?` (an identifier optionally followed by a trailing colon). A colon immediately preceded by another colon (`::`) must not start a match. Other colon characters (e.g., inside quoted strings) must be left untouched.

**Non‑obvious constraints**
- Placeholder syntax can include an optional trailing colon, so `:value:` must still become `?`.
- The conversion must happen before the driver‑specific `_prepare` method, as currently.
- The change must not affect existing calls with simple `:name` placeholders.

fix: preserve Postgre casts when converting named placeholders in prepared queries (#9960)


The template parser in the view layer has two distinct problems that break both security and developer experience.

### Problem 1: Auto‑escaping never activates

When a view variable is rendered without an explicit `noescape` filter, the parser is supposed to apply an implicit HTML‑escaping layer (the `esc` filter). In `shouldAddEscaping`, a regex checks whether an `esc` filter already exists; if not, the component expects to add one. The regex used is `/^|\s+esc/`. The `^` alternative matches the start of *any* string, so `preg_match` always returns true. As a result, the “no esc filter present” path is never taken, and variables that should be auto‑escaped are output raw.

**Steps to reproduce:**
1. Set data like `['foo' => 'http://foo.com']`.
2. Render a template string `{ foo }` (no filter).
3. The output is the unescaped string instead of the HTML‑escaped equivalent.

**Expected behaviour:**
Variables without a `noescape` filter and without an explicit `esc` filter should be automatically HTML‑escaped.

**Impact:**
This is a silent XSS vector. Any user‑supplied value rendered in templates without an explicit `noescape` or `esc` filter bypasses the built‑in escaping, leaving applications open to injection.

---

### Problem 2: Malformed conditional syntax causes an unhandled `\ParseError`

Conditional blocks (`{if}`/`{elseif}`/`{else}`) are evaluated via `eval()`. If the template contains a PHP syntax error (e.g., mismatched parentheses or a missing closing tag), `eval` throws a `\ParseError` that is not caught. The framework does not convert this into a `ViewException`, so developers see a raw PHP fatal error instead of a descriptive error that points to the template.

**Steps to reproduce:**
1. Provide a template with a broken conditional, such as `{if doit}Howdy{elseif doit}Welcome{ endif )}`.
2. Render the template.
3. A `\ParseError` escapes and either results in a white screen or a plain PHP fatal‑error message.

**Expected behaviour:**
The parser should catch `\ParseError` and re‑throw it as a `ViewException` (e.g., using the existing `ViewException::forTagSyntaxError`) so the error is handled gracefully and the offending template content is surfaced.

**Impact:**
Debugging invalid parser syntax becomes extremely difficult. The lack of a proper exception leaves developers without a clear indication that the problem lies in the view template, and without the template content in the error message.

Flesh out Parser tests; handle ParseError


Implement database transaction support in the abstract `BaseConnection` class, mirroring the established CI3 transaction API. Add the following public methods and behaviours:

- `transStart(bool $test_mode = false)` / `transComplete()` / `transStatus(): bool`
- `transStrict(bool $mode = true)` setter for strict mode
- Configuration flags `trans_enabled` (default `true`), `trans_strict` (default `true`), and internal depth/status/failure tracking properties.

Nested transactions must be supported: only the outermost call actually begins/commits/rolls back the underlying connection; inner calls just increment/decrement a depth counter.

When a query fails inside `BaseConnection::query()` and a transaction is active (`_trans_depth !== 0`), set the transaction status to failed. Additionally, if `DBDebug` is enabled, automatically roll back any open transaction and return `false` from the query method, ensuring that lingering transactions are not left open.

Implement strict mode: when strict is `true` and a transaction group fails, any subsequent `transStart`/`transComplete` calls will automatically rollback. When strict is `false`, each group is independent and a rollback in one does not affect the next.

Add three abstract methods that driver-specific connection classes must implement:

- `protected function _transBegin(): bool`
- `protected function _transCommit(): bool`
- `protected function _transRollback(): bool`

Provide concrete implementations for the existing drivers:

- **MySQLi**: use `mysqli::begin_transaction()`, `commit()`, `rollback()`, and manage `autocommit` accordingly (turn off at begin, restore after commit or rollback).
- **Postgre**: execute the SQL statements `BEGIN`, `COMMIT`, `ROLLBACK` via `pg_query`.
- **Mock driver**: return `true` for all three methods to allow functional testing.

The `transStart` and `transComplete` methods should be aliases that delegate to `transBegin`/`transCommit` (with a `test_mode` parameter that can force a rollback even on success).

Porting over database transaction code from CI3. Fixes #268


### getRowObject() and getCustomRowObject() use wrong property for current-row comparison, breaking cursor consistency and missing out-of-bounds safety

**Observed behavior**
When using `BaseResult::getRowObject(int $n)` or `getCustomRowObject(int $n, string $className)`, the current row pointer is not advanced correctly, and accessing a row beyond the result set can produce an “Undefined index” error instead of returning `null`.

Mixing calls to `getRowObject()` and `getRowArray()` does not maintain a shared row position: after `getRowObject(1)`, a subsequent `getRowArray()` may return a different row than expected.

**Root cause**
In `getRowObject()` (and the private wrapper inside `getRow()`), the check that decides whether to update `$this->currentRow` compares the integer `$n` against `$this->customResultObject` (an array) rather than `$this->currentRow`. Because the comparison is never false, the internal pointer is forced to `$n` whenever the requested index exists, even if `$n` is out of bounds, leaving the object in an inconsistent state.

The other row-fetching methods (`getRowArray`, `getCustomRowObject`, `getPreviousRow`) return the row directly from the result array using `$this->currentRow` without a fallback when the index is invalid, causing an undefined index notice/error.

**Expected behavior**
1. `getRowObject($n)` and `getCustomRowObject($n)` should behave like `getRowArray($n)`: they only change `$this->currentRow` when `$n` differs from the current position and the index is valid. Otherwise the cursor should stay where it is.
2. All row accessor methods should return `null` when the requested row does not exist (including when the internal cursor points to an invalid index after a prior operation).
3. Switching between `getRowObject` and `getRowArray` must share the same cursor so that they operate on the same logical row.

**Impact**
This makes it impossible to reliably switch between array and object row representations within the same result set, and causes fatal errors when edge cases (e.g., iterating past the last row, or using an invalid explicit index) are hit. Any code that relies on the cursor staying consistent across method calls is broken.

fix(database): use currentRow instead of customResultObject in getRowObject and add null fallback (#10257)


We need a way to add multiple headers with the same name (e.g., `Set-Cookie`) as distinct header objects, not just a single header with multiple comma-separated values. Currently the `Message` class (via `MessageInterface` / `MessageTrait`) only supports:

- `setHeader()` – replaces any existing header(s) with a single `Header`.
- `appendHeader()` / `prependHeader()` – appends/prepends a value to a single `Header`’s value, treating it as a multi-value field.

Neither allows you to have two separate `Set-Cookie` headers with different attributes.

**Request**: Add a new method `addHeader(string $name, string $value): self` that:

1. If no header with `$name` exists, behaves like `setHeader()` and creates a single `Header`.
2. If exactly one `Header` with `$name` already exists (the normal case), converts that single `Header` into a list containing that original header plus a new second `Header` with the given `$value`.
3. If the header is already a list of headers (multiple headers with the same name, i.e., a “multiple headers” state), appends a new `Header` object to the list.

After using `addHeader()` multiple times, calling `header('Set-Cookie')` should return an indexed array of `Header` objects. The `headers()` method’s return type should reflect that a header can hold either a single `Header` or a list of `Header` objects.

**Safety constraint**: Once a header is in the “multiple headers” state, it becomes ambiguous to modify via `setHeader()`, `appendHeader()`, or `prependHeader()`. Those methods should throw an `InvalidArgumentException` if called on a header that already has multiple headers, with a message indicating the user must remove the header first. This avoids silent loss of the list or unintended merging.

**Edge case**: The existing behavior of `setHeader()` with a header whose value is an array (i.e., a single `Header` with an array value) should remain unaffected – that is still a single header object, not a list of headers. Only `addHeader()` can create the multiple-headers state.

Make sure the new method, the dual return type of `header()`, and the guard in `setHeader()`/`appendHeader()`/`prependHeader()` are all implemented in `MessageTrait` and declared in `MessageInterface`.


# Need better support for controller testing and session handling in test environment

**Observed problems**

1. **Controller testing is too manual and brittle** – To exercise a controller method I must manually construct a full `IncomingRequest`, a `Response`, a `URI`, and often an `App` config object, wire them together, and then call the method. There is no built‑in shortcut to say “run this controller method with this request and capture what it outputs”. After execution I want a simple wrapper that holds the final response and body so I can check whether the request was successful (status code in the 2xx/3xx range and a non‑empty body).

2. **Session failures during testing** – In CLI testing (PHPUnit) the `session_start()` call inside `Session::startSession()` can produce “session already started” warnings or “headers already sent” errors, breaking test execution. In a `testing` environment I expect the session to be bypassed entirely: just use a plain `$_SESSION` array so the session service can be used but no actual session is opened.

3. **Response `statusCode` defaults to `null`** – When a controller method runs but does not explicitly call `setStatusCode()`, the property remains `null`. Later checks against the status code (e.g. `$response->getStatusCode() < 400`) throw type errors or behave unpredictably. A fresh response should have a sane default status code of `200`.

4. **`hasInDatabase()` loses the insert result** – The `CIDatabaseTestCase::hasInDatabase()` method inserts data but does not return anything from the underlying `$db->table()->insert()` call. This makes it impossible to inspect the insert result (for example, to get the last insert ID or verify success).

**Expected behavior**

- Provide a fluent helper (likely a trait for test classes) that allows:
  - optionally setting a custom request, response, URI, and body,
  - if omitted, it creates sensible defaults,
  - instantiation of a controller by class name,
  - execution of a given method with arbitrary parameters,
  - and finally an object that exposes the request, the response, the output body, and a simple `isOK()` method (status code in 2xx/3xx and non‑empty body).
- In a `testing` environment, `Session::startSession()` should skip actual session startup and only set `$_SESSION = []`, so session data can still be manipulated without triggering PHP session errors.
- A new `Response` object should have `$statusCode = 200` by default (the constructor must initialize it).
- `CIDatabaseTestCase::hasInDatabase()` must return the result of the insert operation, as the Query Builder’s `insert()` already returns the result.

**Impact**

Without these changes, controller tests are hard to write reliably, sessions cause noisy test output or outright failures, and response status defaults lead to confusing errors.

Starting on some additional tools for testing controllers.


**Bug Report: Incorrect age for future dates, unreliable DST detection for non‑observing timezones, and misleading property‑set magic**

### 1. `Time::getAge()` returns negative numbers for future dates

#### Observed behaviour
- Calling `getAge()` (or accessing the `age` property via `__get`) on any `Time` instance that represents a future date gives the negative year difference instead of `0`.
- For example, `Time::parse('August 12, 2116 4:15:23pm')->age` returns a negative integer (e.g., `-99`), even though the date is clearly in the future.
- The same method returns `0` for the current time only by accident (the condition `! $now >= $time` is always false).

#### Expected behaviour
- `getAge()` should return `0` for all future dates.
- `getAge()` should also return `0` for the exact current instant (“now”).
- It should continue to return the correct positive year difference for past dates.

#### Impact
Age‑based humanisation and any code relying on age to decide whether an event is past/future will be broken.

---

### 2. `Time::getDst()` fails or produces notices for timezones that do not observe DST

#### Observed behaviour
- When a `Time` instance uses a timezone that has no DST transitions (e.g., `Asia/Shanghai`) the `getDst()` method may:
  - throw an “Undefined index: isdst” notice because the transition array might not contain the key,
  - or return an unexpected boolean because it relies on the first transition **after** the timestamp (which is the *future* state, not the current one).
- The method does not properly fall back to `false` when no transitions are found.

#### Expected behaviour
- `getDst()` (and the `dst` property) must always return `false` when the timezone does not observe daylight saving time.
- It must never emit notices for missing array keys.

#### Impact
Downstream code that implements DST‑aware logic (date calculations, event scheduling, etc.) may crash or behave incorrectly.

---

### 3. Property‑set magic (`__set`) silently ignores assignments

#### Observed behaviour
The `Time` class implements `__set` so developers can write:
```php
$time = new Time();
$time->day = 10;
```
However, the underlying `setDay()` method is **immutable** and returns a new `Time` instance. `__set` discards that return value, so the original object stays unchanged and the assignment has **no effect**. There is no error or warning.

#### Expected behaviour
Either the class should support in‑place mutation (contradicting the immutability of all other `set*` methods) or `__set` should be **removed** to prevent this silent logical error.

#### Impact
This can lead to subtle, hard‑to‑diagnose bugs where the developer believes they have moved the time forward/backward but the original value persists.

---

### 4. Minor: `toLocalizedString()` does not accept `null` in strict mode

- The `$format` parameter is optional but declared as `string $format = null`. In PHP 7.1+ strict mode, passing `null` explicitly results in a `TypeError`.
- Expected: nullable type hint `?string $format = null`.

---

**Desired outcome**: Fix the age calculation, make DST detection safe for all timezones, remove the broken `__set` method, and allow `null` explicitly in `toLocalizedString`.


We need the `cache:clear` command to actually clear the system cache instead of just dumping the raw parameters.

**Expected behavior:**
- Running `spark cache:clear` without arguments should clear the cache using the default handler configured in `config('Cache')->handler`.
- Running `spark cache:clear <handler>` should clear the cache using the specified handler, provided that handler name exists in `config('Cache')->validHandlers`.
- If an invalid handler name is supplied, the command must output an error message and exit without attempting to clear anything (to avoid a crash or silent failure).
- When clearing succeeds, the command must output a visible success message (green “Done”).
- When the cache handler’s `clean()` method returns `false`, the command must output an error message.

**Safety / edge cases:**
- The command must validate the handler argument before constructing the cache instance; attempting to use an unrecognized handler should not cause an unhandled exception.
- The default handler from the config must be used when no argument is given.
- The output must be direct and not include raw data dumps (the `dd()` call must be removed).


## Ticket: View/Parser data scoping with `saveData` is broken

**Affected components:** `CodeIgniter\View\View`, `CodeIgniter\View\Parser`

**Symptom:**
When rendering a view with `saveData = false` (the default), data that was set *during that render* (e.g., via `setData()` called inside a layout or included view) is not available to all parts of the same render cycle. Conversely, data from a previous render can bleed into a later render even when `saveData` was false for the earlier call. The parser has the same issues.

**Expected behaviour:**

- **During a render (or renderString) call:** All data that the View object currently holds (from prior `setData` / `setVar` calls or from a previous render with `saveData=true`) must be available to the view, its layouts, includes, and sections. Additionally, any data set *during* the render (e.g., inside a layout script) must be visible for the remainder of that render call.
- **After the render, when `saveData` is false:** The View object must not retain any data that was introduced or modified during that render. A subsequent render call should see only the data that was present *before* the previous render started (or that was explicitly set afterwards). In particular, calling `render()` with no extra options immediately after a `saveData=false` render must not accidentally see data from that previous render.
- **After the render, when `saveData` is true:** The View object’s data should be updated to include any changes made during the render (e.g., if the layout added variables via `setData`).
- The `getData()` method must always return the data that would currently be used for rendering (i.e., the temporary set while a render is in progress, otherwise the persistent set).
- The Parser class (`CodeIgniter\View\Parser`) must follow the same rules when rendering view files and when rendering strings.

**Edge cases:**
- Nested renders (e.g., calling `view()` or `$this->render()` inside a view) should not disturb the outer render’s data set.
- Layouts and sections that use `setData` during rendering must correctly propagate the data within that render only.
- The parser’s PHP evaluation (for `<?= ... ?>` tag support) must also extract from the correct scoped data.


## Bug: Filter wildcard matching and global ‘except’ rules are broken

### Summary
When filter rules use wildcard patterns (e.g., `'admin*'` or `'admin/*'`), the framework fails to correctly apply or exclude filters. This makes it impossible to use wildcards to define filters that should apply to a URI prefix or pattern, and global `'except'` rules that rely on wildcards do not work as documented.

### Steps to Reproduce
1. Define a filter with a wildcard pattern, for example:
   ```php
   // app/Config/Filters.php
   public $filters = [
       'auth' => ['before' => ['admin*']]
   ];
   ```
2. Access URIs like `/admin`, `/admin/login`, `/admin123`.
3. **Observed behavior**: The `auth` filter is **not** applied to any of these URIs, even though the wildcard should match.

4. Define a global filter with an `except` pattern using wildcards:
   ```php
   public $globals = [
       'before' => [
           'csrf' => ['except' => 'admin*']
       ]
   ];
   ```
5. Access URIs like `/admin`, `/admin/login`, `/admin123`.
6. **Observed behavior**: The `csrf` filter is **still applied** to these URIs, even though the `except` pattern should have excluded them.

### Expected behavior
- A path pattern containing `*` should be interpreted as a wildcard that matches zero or more characters.
  - `'admin*'` should match `admin`, `admin/login`, `admin123`, etc.
  - `'admin/*'` should match `admin/login` but not `admin` or `admin123`.
- Global filter rules with `'except'` should check whether the current URI matches any of the exclusion patterns. If a match is found, that filter should **not** be added for the current request (neither `before` nor `after`).
- All matching should be case-insensitive.
- An empty path (or missing) should match all URIs (meaning no exclusion or always included, depending on context).

### Impact
- Any application relying on wildcard patterns for filter routing (e.g., applying admin authentication to a group of controllers) is completely broken.
- Global protection like CSRF or throttling cannot be selectively disabled for admin pages using simple wildcard patterns, forcing workarounds with explicit per-route configuration.
- Developers cannot use the documented feature of `'except'` with wildcards in global filters, leading to unexpected security or usability issues.

### Relevant configuration keys
- `Config\Filters::$filters` – per-filter rules with `before`/`after` containing path patterns.
- `Config\Filters::$globals` – global filters, where each alias can have an `'except'` array of path patterns.
- The filter processing logic in `CodeIgniter\Filters\Filters`.

### Additional context
The problem occurs both in `processGlobals()` and `processFilters()` where pattern matching was incorrectly implemented. The fix should ensure that the new matching logic (e.g., using a reusable path-matching method) correctly handles wildcards, path separators, and empty patterns.


As a maintainer ticket:

---

**Title:** Support multiple filter classes per alias in `Filters`

**Context:**
Our filter system (`CodeIgniter\Filters\Filters`) resolves filter aliases from `Config\Filters::$aliases`. Currently, each alias must map to a single fully-qualified class name (string). There is no way to attach multiple filters to the same alias and have them run sequentially.

**Request:**
Allow the `$aliases` values to be an array of class names in addition to a single string. When an alias resolves to an array, the filter runner should instantiate and execute **all** listed classes in order, for both `before` and `after` positions.

**Behavioral requirements:**

- Each class in the array must implement `CodeIgniter\Filters\FilterInterface`. If any does not, a `FilterException` should be thrown (same as currently for a single invalid class).
- For `before` filters, the classes run in the order defined.
  - If a `before` filter returns a `ResponseInterface`, execution of the remaining filters for that alias should stop immediately and the response should be returned (short-circuit, consistent with single-filter behavior).
  - If a `before` filter returns a `RequestInterface`, the request instance is updated for subsequent filters.
  - If a `before` filter returns an empty or unexpected value, it should be ignored (current behavior preserved).
- For `after` filters, the classes run in order.
  - If an `after` filter returns a `ResponseInterface`, the response is updated for subsequent filters.
- Arguments passed to the alias (e.g., route-level arguments) should be available to **each** filter via the existing `$this->arguments[$alias]` mechanism. The same argument value(s) should be passed to all classes in the alias.
- An empty array for an alias is acceptable and should simply result in no filters being executed for it (no-op).

**Config example (desired support):**
```php
// app/Config/Filters.php
public $aliases = [
    'csrf'     => \CodeIgniter\Filters\CSRF::class,
    'auth'     => [
        \App\Filters\EnsureLogin::class,
        \App\Filters\CheckPermissions::class,
    ],
];
```

Using `'auth'` in `$globals`, `$methods`, or `$filters` should then run both `EnsureLogin` and `CheckPermissions` sequentially.

**Non-obvious considerations:**
- The feature must not break existing single-class aliases.
- The short-circuit rule for `before` must behave identically to the single-filter case: once a `ResponseInterface` is returned, no further filters in that alias (or any other simultaneously defined filters for the route) should execute for the `before` stage.
- The feature should work both when filters are triggered via globals and per-route configurations.

**See also:** #2831 (user request for this capability).


Move request object creation logic to Services and remove generic inject method.

Currently `CodeIgniter::getRequestObject()` directly creates the request object (via `Services::clirequest()` or `Services::request()`), sets the protocol version for web requests, and then manually injects the resulting object into the shared service container using `Services::inject()`. This behaviour is inconsistent with other services and exposes an unnecessary generic injection method.

**Acceptance criteria:**

1. **Unchanged external behaviour**
   - On a web request, the system still creates an `IncomingRequest` and guesses the server protocol version from `$_SERVER['SERVER_PROTOCOL']` (falling back to `HTTP/1.1`).
   - On a CLI or sparked request, the system still creates a `CLIRequest`.
   - After framework initialisation, `Services::request()` (called without parameters) returns the current request object.

2. **Logic moved to Services**
   - The logic that creates the request object and injects it as a shared instance is relocated from `CodeIgniter::getRequestObject()` into a new internal method `Services::createRequest(App $config, bool $isCli = false): void`.
   - For web requests, the protocol version setup happens inside `createRequest` before the request is shared.
   - `CodeIgniter::getRequestObject()` calls `Services::createRequest($this->config, …)` appropriately and then retrieves the request via `Services::request()` (without parameters).

3. **Removal of `inject` method**
   - The public `Services::inject(string $name, $obj)` method is removed. No external code should rely on a generic injection point; the new creation method registers the request internally.

4. **Deprecation of `Services::request()` parameters**
   - The `$config` and `$getShared` parameters of `Services::request()` are deprecated. Calling them still works for backward compatibility (they delegate to a new `incommingrequest()` method), but the parameters are marked as deprecated and new code should use `Services::incommingrequest()` directly when a fresh `IncomingRequest` is needed.

5. **New internal factory method**
   - A new internal method `Services::incommingrequest(?App $config = null, bool $getShared = true)` creates an `IncomingRequest` instance and (when `$getShared` is true) stores it as the shared `request` service. This method is used by both `Services::request()` for backward compatibility and by `Services::createRequest()` for web requests.

refactor: move logic to create request object to Services


Add a `record` method to the `Timer` class that measures the execution time of a callable and returns its result. The method should start the timer with the given name, invoke the callable, stop the timer, and return whatever the callable produced (or `null` if nothing was returned).

Extend the global `timer()` function to accept an optional second argument: a `callable`.
- When both a timer name and a callable are provided, delegate to `Timer::record`.
- When only a name is provided, keep the existing start/stop behavior.
- When no name is provided, return the `Timer` instance and ignore any callable.


We need to harden `Query` so that `getQuery()` can be called safely after any sequence of `setQuery()`, `setBinds()`, and `swapPrefix()`. Currently the internal state management makes repeated calls fragile and lead to wrong results.

**Desired behavior / invariants**

1. **`getQuery()` is idempotent while inputs are unchanged**
   Calling `getQuery()` multiple times without changing the SQL or binds must return the same fully-compiled string every time, with no double‑escaping or repeated placeholder replacement.

2. **Binds and SQL changes invalidate the cached result**
   - `setBinds()` must cause the next `getQuery()` to recompile the binds, even if a previous call had already compiled them.
   - `setQuery()` (whether it supplies new binds or reuses the old binds) must force the next `getQuery()` to use the new SQL string and the current binds.

3. **Prefix swapping works independently of bind compilation**
   - `swapPrefix()` applies to the **original SQL** (before binds are substituted). The swapped SQL then becomes the base for bind compilation.
   - Multiple `swapPrefix()` calls chain (e.g. swap `A→B`, then `B→C` produces SQL with prefix `C`).
   - Calling `swapPrefix()` after `setBinds()` or even after `getQuery()` must produce the correct swapped-and-bound result.
   - Calling `swapPrefix()` **must invalidate** the previously compiled final query so the next `getQuery()` recompiles with the new prefix.

4. **Order independence**
   The following sequences must yield the same final output:
   - `swapPrefix()` then `setBinds()`
   - `setBinds()` then `swapPrefix()`
   - `setQuery()` → `getQuery()` → `swapPrefix()` → `getQuery()`

**Why this matters**
The current implementation (before the fix) caches the compiled result in `finalQueryString` but does not properly invalidate it on `setBinds` or `setQuery`, and `swapPrefix()` can directly overwrite the final string without considering whether binds have already been applied. This leads to stale or incorrectly‑ordered operations when the query object is reused, which is common in the query builder and test scenarios.

**Scope**
- Refine the caching and invalidation inside `Query::getQuery()`, `Query::setQuery()`, `Query::setBinds()`, and `Query::swapPrefix()`.
- No changes to the public API signatures; only behavioral correctness.

Allow calling getQuery() multiple times, and other improvements


The CLI command runner crashes or misbehaves when abstract command classes or non-command classes are placed in the Commands directory, preventing developers from using base command templates or keeping non-command files alongside commands.

**Observed behavior:**
- Attempting to run or list commands when an abstract class extending `BaseCommand` exists (e.g., an abstract helper command) causes a fatal error: “Cannot instantiate abstract class”.
- Classes that do not extend `CodeIgniter\CLI\BaseCommand` but reside in a scanned command directory are incorrectly treated as commands, leading to unexpected behavior when invoked.

**Expected behavior:**
The command discovery in `CommandRunner::createCommandList()` should silently skip any class that is not both:
1. Instantiable (concrete, not abstract or an interface), and
2. A subclass of `CodeIgniter\CLI\BaseCommand`.

Only valid, runnable commands should be registered and displayed in the command list. Abstract bases or unrelated classes must be ignored so they do not break the CLI.

**Impact:**
The defect makes it impossible to define abstract base command classes for shared logic, and risks instability if any non-command file is accidentally placed in a Commands folder. The fix would allow safe creation of abstract command templates and co-location of utility classes without runtime errors.


**Subject:** URL helpers produce incorrect URLs in subfolder installs and mishandle forced HTTPS, query strings, and fragments

### Problem

*   `base_url()` uses the current request’s path to resolve relative URIs instead of relying solely on the configured `$baseURL`. In subfolder installations this causes the returned base URL to leak segments from the current route.
*   `site_url()` does not handle relative paths that contain query strings (`?key=value`) or fragments (`#section`) correctly. Fragment parts are lost completely; query strings can lead to double slashes or misplacement.
*   `current_url()` ignores `$config->indexPage` and the subfolder portion of `$baseURL`, returning a path that does not match `site_url(uri_string())`. Additionally, when returning an object, fragment information is discarded.
*   None of the helpers consistently apply the `forceGlobalSecureRequests` config setting. When enabled, it is not propagated to `base_url()` or `current_url()`.
*   The old implementations duplicate URL-building logic, making it difficult to fix all helpers at once.

### Expected Behavior

1.  **`base_url($path, $scheme)`** should return a URL identical to `site_url($path, $scheme)` but with the index page removed and trailing `/` trimmed. It must **never** depend on the current request’s URI; only `App::baseURL` should be used.
2.  **`site_url($path, $scheme)`** must:
    *   Correctly append query strings and fragments from `$path` (e.g., `'foo?bar=bam'` → `http://example.com/index.php/foo?bar=bam`).
    *   Handle array segments, `'0'`, and absolute paths without breaking.
    *   Force the scheme to `https` when `App::$forceGlobalSecureRequests` is true and the base URL uses `http`.
3.  **`current_url($returnObject, $request)`** should:
    *   Build the full URL using the App config (baseURL + indexPage + path from the request), respecting `forceGlobalSecureRequests`.
    *   When returning a string, omit query and fragment parts (preserving existing behavior).
    *   When returning an object, include query and fragment as they appear in the request.
    *   Optionally accept a specific `IncomingRequest` instance for testing purposes.
4.  All URL helpers should share a single canonical routine for building a framework URI from the App config, removing dot segments and avoiding double separators.

### Impact

*   Sites hosted in subdirectories (e.g., `example.com/public/`) generate broken navigation links.
*   Redirects and form actions lose query parameters or hash fragments when `site_url()` is used.
*   Enforced HTTPS via configuration is silently ignored by `base_url()` and `current_url()`, creating mixed-content / redirect loops.
*   Code relying on `base_url(uri_string())` to reconstruct the current URL no longer works because `current_url()` omits the index page and subfolder prefix.


**Bug: Foreign keys silently missing when fields are defined as string literals in `Forge`**

### Summary
When I define all columns using the string shorthand (e.g. `'id' => 'INT NOT NULL AUTO_INCREMENT PRIMARY KEY'`) and then add a foreign key with `addForeignKey()`, the generated `CREATE TABLE` statement does **not** include the foreign key constraint. The table is created, but referential integrity is silently dropped.

### Steps to reproduce
```php
$forge->addField([
    'id'   => 'INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY',
    'name' => 'VARCHAR(255) NOT NULL',
]);
$forge->addKey('id', true);
$forge->createTable('users', true);

$forge->addField([
    'id'      => 'INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY',
    'user_id' => 'INT(11) NOT NULL',
    'name'    => 'VARCHAR(255) NOT NULL',
]);
$forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE');
$forge->createTable('orders', true);
```

### Expected behavior
The `orders` table should be created with a foreign key named something like `orders_user_id_foreign`, linking `orders.user_id` to `users.id`. The constraint should appear in the table DDL.

### Observed behavior
The `orders` table is created, but the foreign key constraint is absent. No error is thrown; the constraint is simply not part of the generated SQL.

### Context / impact
- This affects all database drivers.
- The string-syntax is a documented convenience for quick definitions, but currently it cannot be used in any schema that requires foreign keys.
- Migrations that rely on this shorthand will silently fail to produce valid foreign keys, leading to potential data integrity issues that are hard to notice during development.

### Possible root cause hints
The problem seems to be that when fields are given as raw strings, `Forge` does **not** correctly extract and store the column names as array keys. Later, when `_processForeignKeys` tries to match the foreign column against field definitions, it cannot find the field because the internal `$fields` array uses numeric indices instead of the actual column names. A similar issue may affect index definitions that reference columns by name.

Fix adding foreign keys with only string fields (#4988)


## Bug report: CSRF cookie not set by `csrf_hash()`, causing token verification failure on POST-only filters

### Summary
When the CSRF filter is configured to protect only POST requests (e.g., `$methods = ['post']`), calling `csrf_hash()` (or `csrf_field()`) does **not** include the corresponding CSRF cookie in the HTTP response. As a result, any subsequent POST request lacks the required cookie and fails verification with a `SecurityException`.

### Steps to Reproduce
1. Set the CSRF filter’s `$methods` property to `['post']` so that it only runs for POST requests.
2. In a controller or view, call `csrf_hash()` to get the token (for example, to include in a hidden form field).
3. Submit the form via POST.
4. Observe that the framework blocks the request with a CSRF token mismatch error, even though the token value is correctly inserted in the POST data.

### Expected Behavior
- `csrf_hash()` should always set the CSRF cookie on the client (via `Set-Cookie` header or an equivalent mechanism) regardless of the filter’s `$methods` configuration.
- The token returned by `csrf_hash()` should match the cookie sent to the browser, so that a subsequent POST request passes verification.

### Actual Behavior
No CSRF cookie is sent when `csrf_hash()` is called, so the browser never stores it. When the POST request arrives, `verify()` cannot find the cookie, considers the token invalid, and throws a `SecurityException`.

### Impact
- Forms that rely on the CSRF helper functions break exclusively when the filter is restricted to POST. Removing the `$methods = ['post']` limitation (or switching back to the default) makes the problem disappear, but then unnecessary verification runs on other HTTP methods.
- This prevents using the CSRF filter in a more targeted and performant manner.

### Additional Context
- The issue persists regardless of the `regenerate` and `expires` Security configuration values.
- Using `$this->hash` instead of reading `$_COOKIE` directly inside `verify()` might be needed to avoid reliance on superglobals that are not yet populated when the filter runs, but the primary missing piece is the cookie being sent in the response.

Fix CSRF filter does not work when set it to only post


### Bug Report: Error view template selection is based on exception code instead of HTTP status code

In `system/Debug/ExceptionHandler`, the protected method `determineView` decides which error view file to render by checking for a custom template file named `error_<code>.php`, where `<code>` comes from `$exception->getCode()`. This is incorrect because the exception’s integer code does not necessarily represent the HTTP status the server intends to send.

#### Observed problem
A runtime exception can have arbitrary codes (e.g., `404`) that have nothing to do with a “Page Not Found” response. In such cases, the handler picks an entirely wrong view (the `404` page), while the actual HTTP status code being returned might be `500` (Internal Server Error) or some other code. This leads to confusing error displays and misleading diagnostics, especially in environments that provide custom status-specific error pages.

#### Desired behavior
The view selection should be driven by the HTTP status code that `ExceptionHandler::handle()` already receives via its `$statusCode` parameter. A custom view file like `error_404.php` should only be chosen when the handler intends to emit a `404` status (i.e., `$statusCode === 404`), not when an arbitrary exception reports code `404`.

#### Implementation notes
- The `determineView` method must accept the intended HTTP status code and use it for the `error_<code>.php` check instead of `$exception->getCode()`.
- Callers inside `handle()` should pass the `$statusCode` argument through.
- All other view selection logic (production view, CLI view, generic `error_exception.php` fallback) should remain identical in their conditions and precedence.
- The method signature can be extended to include `int $statusCode` with a default of `500` to avoid unnecessary breakage for any external (though unlikely) subclasses.

No changes are needed to the `BaseExceptionHandler` or the interface because `$statusCode` is already available within the `handle` scope.

fix: bug that the error view is determined by Exception code


The `Message` class in `CodeIgniter\HTTP\Message` has several header-handling issues that need to be corrected.

### Observable issues & required behavior

1. **`headers()` must use direct `filter_var` on `$_SERVER`**
   Currently it calls an internal `fetchGlobal` helper that doesn’t apply the intended filter correctly. The method must read from `$_SERVER` directly and apply `filter_var` to each server header. If `$filter` is `null`, it should default to `FILTER_DEFAULT`.

2. **`header()` must never return stale data and must handle array values**
   – The method caches headers in a static variable, so after the first call any later `setHeader` modifications are ignored. The method must always return the current value from `$this->headers`.
   – It must support array header values (e.g., `Accept: application/json, text/html`). If the stored value is an array, apply `filter_var_array`; otherwise apply `filter_var`.
   – The header name lookup must remain case-insensitive (as already implemented via `getHeaderName`).

3. **`headerLine()` must only implode when the specific header’s value is a list**
   The existing check tests whether the whole `$this->headers` property is an array (always true), causing `implode(', ', ...)` to concatenate all headers incorrectly. The method must check whether the **value** for the requested header is an array. Only then should it join the elements with `', '`; otherwise it must return the value unchanged.

4. **`appendHeader()` must return `$this` for fluent chaining**
   Right now it returns a string. The method should return the `Message` instance so clients can chain calls (e.g., `$msg->appendHeader('Accept', 'xml')->setHeader('Host', 'example.com')`).

5. **Independent of request context**
   While `Message` was previously tested only through `Request`, these header behaviors belong to `Message` itself and should work consistently regardless of the concrete subclass. All the above fixes ensure `Message` is usable and correct on its own.

### Acceptance criteria (behavior-first)

- Calling `headers()` without a filter returns all headers, each filtered with `FILTER_DEFAULT`.
- Calling `headers($filter)` applies that filter to every header value via `filter_var`.
- After `setHeader('Accept', ['json', 'html'])`, `header('Accept')` returns the array `['json', 'html']`.
- After modifying a header via `setHeader`, subsequent `header('…')` calls reflect the change (no stale cache).
- `headerLine('Accept')` on the above setup returns `'json, html'`.
- `headerLine('Host')` on a scalar value returns the scalar string unchanged.
- `appendHeader('Accept', 'xml')` returns the `Message` instance, and the stored header now contains `['json', 'html', 'xml']`.
- All header-name lookups are case-insensitive.


**Title:** Constructor should accept an optional relative path to build the full site URI in one step

**Problem**
Currently, `SiteURI::__construct()` only accepts the `Config\App` object and initializes the path to `/`. To create a URI for a specific application route (e.g., `/controller/method` or `/controller/method?foo=bar`), users must call `setPath()` after construction. This is cumbersome and error-prone when query strings or fragments are involved. Additionally, if `Config\App::$indexPage` is set and the desired URI contains only a query string (e.g., `?foo=bar`), the existing code unconditionally appends a `/` after the index page, producing invalid URLs like `index.php/?foo=bar`.

**Expected behavior**
1. The constructor should accept an optional string `$relativePath` (default `''`).
2. When `$relativePath` is non-empty, the resulting site URI should be constructed as:
   `baseURL + indexPage + (conditional '/') + relativePath`
   where the conditional `/` is added **only** if all these hold:
   - `indexPage` is not empty
   - `$relativePath` is not empty
   - the first character of `$relativePath` is neither `/` nor `?`
3. Dot segments in `$relativePath` (e.g., `./`, `../`) should be resolved before the URI is parsed.
4. The route path (the portion of the path that represents the application route) must be correctly set from the path part of `$relativePath`.
5. Query and fragment components in `$relativePath` must be retained and placed correctly after the path.
6. When no `$relativePath` is provided, the constructor must behave exactly as before (path set to `/`).
7. Subfolder installations (`baseURL` ending with a segment like `/ci4/`) and forced HTTPS should continue to work as expected.

**Edge cases / Safety constraints**
- `$relativePath` starting with `?` (e.g., `?debug=1`) must not have a slash inserted, yielding `index.php?debug=1`.
- `$relativePath` starting with `/` (e.g., `/already/separated`) must not duplicate slashes.
- `$relativePath` with internal `/` but starting with a path segment (e.g., `one/two`) must receive a slash: `index.php/one/two`.
- `$relativePath` ending with `?` or containing both a path and query (e.g., `one/two?foo=1&bar=2`) must produce the correct full URL and set the route path to `/one/two`.
- Dot-segment removal must be applied before any other processing, so path traversal attempts are normalized.

feat: add param $relativePath to constructor


The `command()` helper in `system/Common.php` currently splits the entire command string on spaces and passes the resulting list directly to the command runner. This means that CLI-style options (e.g. `-opt1 val1`) are not distinguishable from positional arguments, making it impossible for command implementations to consume named options properly.

We need `command()` to parse its arguments according to common CLI conventions:

- Tokens that **do not** begin with `-` are positional arguments and should appear in the final params array under numeric indices (preserving order).
- Tokens that **begin with** one or more `-` (e.g. `-opt`, `--opt`) are treated as option names. The leading dashes should be stripped, and the remaining text used as a string key in the params array.
- If the next token after an option token **does not** begin with `-`, it should be consumed as the value for that option and assigned to the key. That token should **not** appear as a separate positional argument.
- If the next token **does** begin with `-`, or if the option is at the end of the input, the option is treated as a flag with no value (`null`).

For example, `'reveal seg1 seg2 -opt1 val1 seg3'` should produce:

```php
[
   'seg1',
   'seg2',
   'opt1' => 'val1',
   'seg3',
]
```

and `'reveal seg1 seg2 -opt1 -opt2'` should produce:

```php
[
   'seg1',
   'seg2',
   'opt1' => null,
   'opt2' => null,
]
```

This change only affects the parsing inside the `command()` helper. No changes to the command runner or base command class are required—commands already receive an associative array as `$params`. The adjustment simply ensures that named options arrive in a usable structure.


Fix `DownloadResponse::sendHeaders()` to add a `Date` header only when it is missing, not when it already exists.

Also ensure the following observable behaviors:

- `getReason()` returns `'OK'` (the default reason phrase).
- When `pretend(true)` is set, calling `send()` echoes the file contents rather than triggering a download.
- When `pretend(false)` (or not set), the response must emit a `Content-Length` header (derived from the file size) and a `Date` header (generated when none is present).


The `Security` class constructor currently requires a `Config\App` object as its only argument, but the class does not use any `App` configuration properties. Instead, it immediately loads its own configuration via `config('Security')` and uses that. This forces callers to supply an irrelevant dependency, creates confusion about the actual configuration source, and makes the dependency relationship unclear. The `Services::security()` factory also passes an `App` config instance to the constructor, compounding the inconsistency.

**Expected behavior:** The `Security` constructor should accept a `Config\Security` object directly, reflecting its true dependency and eliminating the need for the internal `config('Security')` call. The factory method `Services::security()` should be updated to provide the correct configuration object. This change is a breaking change for any external code that manually instantiates `Security` with an `App` instance, but it aligns the constructor with its actual usage and removes misleading coupling.

refactor: [BC] change Security constructor param from Config\App to Config\Security


Make the `debugbar:clear` command return an explicit exit code from its `run()` method:

- Return `EXIT_SUCCESS` when debugbar JSON files are cleared successfully.
- Return `EXIT_ERROR` if `delete_files()` fails, after emitting the error message.
- Remove any `@codeCoverageIgnore` annotations that previously suppressed coverage of the failure branch, so that the error path is testable.

refactor: add full testing for `debugbar:clear` command (#10093)