## Feature Request: `--overwrite` option for plugin assets symlink/copy commands

### Motivation
When running the `plugin assets symlink` and `plugin assets copy` shell commands, existing target files or directories are skipped with a verbose message. There is no built-in way to force replacement of those assets without manually deleting them first. This adds unnecessary manual steps during plugin updates or when fixing broken symlinks.

### Desired behavior
1. **New `--overwrite` flag**
   Both `plugin assets symlink` and `plugin assets copy` commands accept an optional `--overwrite` boolean flag (default: `false`). The flag appears in the command help.

2. **When `--overwrite` is not provided** (current behavior unchanged)
   - If the target symlink or directory already exists, the command outputs a verbose message (`<dest> already exists`) and continues without changes.

3. **When `--overwrite` is provided**
   - If the target does **not** exist, the symlink or copy proceeds normally (same as today).
   - If the target **already exists**, the command attempts to remove it (unlink for symlinks, delete for directories).
     - **If removal succeeds**, the symlink or copy is immediately created, effectively replacing the old asset.
     - **If removal fails** (e.g., permission error), the command reports the failure and **does not attempt to create** the asset. The plugin asset operation is skipped for that plugin; other plugins are still processed.

4. **Consistency across operations**
   The flag works identically whether the user runs `symlink` or `copy`. Removal logic reuses the existing asset removal capability but adapts its outcome to drive the overwrite decision.

### External constraints / edge cases
- Symbolic links are replaced as links, not as directory copies, when used with `symlink --overwrite`.
- On operating systems where symlinks are not supported (`Windows`, when `DS = '\\'`), the `symlink` command falls back to copying; `--overwrite` must still replace the target directory in that scenario.
- The command must not leave a partially removed state if removal succeeds but creation fails (creation failure is independent; but removal should only proceed if creation is intended, not before).
  *Implementation note: removal should happen first, then creation. If creation fails after a successful removal, the asset is gone – this is acceptable because the original request was to overwrite, and the error for failed creation will be reported by existing logic.*
- Verbose output still reports “already exists” only when `--overwrite` is not used and an existing target is skipped.
- The flag is available when a specific plugin name is passed or when processing all plugins.

### Example usage
```shell
# Replace existing symlinks for all plugins
bin/cake plugin assets symlink --overwrite

# Replace only the assets of a specific plugin using copy
bin/cake plugin assets copy MyPlugin --overwrite
```

Add "overwrite" option for plugin assets symlink/copy operation.


**Request: Align Form validators with ValidatorAwareTrait and event-based building**

The `Form` class should support multiple named validators via `ValidatorAwareTrait`, bringing it in line with the pattern already used by `Table` and other repository objects. This enables plugins and application code to hook into validator construction through a well-defined event, and allows forms to expose multiple validation sets (e.g., different profiles for create vs. update).

### Acceptance criteria

- `Form` implements `ValidatorAwareInterface` and uses `ValidatorAwareTrait`.
- A provider name of `'form'` and a build event of `'Form.buildValidator'` are defined as class constants.
- The form sets up an event manager and registers itself as an `EventListenerInterface` listener for the `'Form.buildValidator'` event, mapping it to a `buildValidator` method (the same callback signature used by `Table`).
- The first call to `getValidator($name)` (or to `validator($name)`) for a given name creates a fresh `Validator` instance, dispatches the `'Form.buildValidator'` event with the validator and the name, and then returns it. Event listeners—including the form’s own `buildValidator` method—can modify the validator at this point.
- The existing `_buildValidator` hook remains available for subclasses to define basic rules before the event fires, preserving backward compatibility.
- `validate()` continues to operate on the `'default'` validator (the name used when none is explicitly passed), and `errors()` works as before.
- Multiple named validators can be set via `setValidator($name, $validator)` and retrieved independently, exactly as with `Table`.
- The `Form` implements `EventDispatcherInterface` so that other code can attach listeners to the form’s events.

### Non-obvious constraints

- The change must not break existing subclasses that override `_buildValidator()` or rely on `validator()` returning the default validator. The `_buildValidator` method should still be invoked as part of the default validator’s building process.
- The `validator()` method should accept an optional name, falling back to `'default'` when no name is given, mirroring `Table`’s behavior.
- The event dispatching must happen after the base `Validator` is instantiated but before it is returned and stored, allowing listeners to add or modify rules in a standard way.


Provide a static API on `TranslateBehavior` to set and retrieve a default translation‑strategy class that applies when no `strategyClass` is given in behavior config. The default must stay `EavStrategy` until explicitly changed. Remove the `strategyClass` key from the instance default config; `createStrategy()` should fall back to the static default when the config key is absent. This lets applications using the shadow‑table strategy opt in once globally without subclassing tables or repeating `strategyClass` on every `addBehavior('Translate')` call.

Add methods to get/set default strategy class.


**Title:** Invoke path-scoped middleware from the routing layer

**Background**
Route scopes support registering and applying middleware via `$routes->registerMiddleware()` and `$routes->applyMiddleware()`. The `Router` can now resolve which middleware apply to a given path, but `RoutingMiddleware` currently ignores them. After a route matches, only the request’s route parameters are set; the middleware pipeline continues directly to the next handler (typically the controller or application middleware). Scoped middleware intended to run for specific URL spaces (e.g., auth, throttling) are never invoked.

**Request**
Update `RoutingMiddleware::__invoke()` so that, after successful route matching (and any redirect handling), the middleware attached to the matching route scopes are executed. The behavior should be:

1. Retrieve the middleware stack that applies to the current request path using the router’s appropriate method (e.g., `Router::getMatchingMiddleware()`).
2. If a stack is returned, append the application’s `$next` handler as the final handler in that stack, then execute the entire stack using `Cake\Http\Runner`. Return the resulting response.
3. If no matching middleware exist, call `$next($request, $response)` exactly as before.

**Acceptance criteria / edge cases**
- Scoped middleware run **after** route parameters are set, so they have access to resolved routing data on the request.
- Middleware from outer scopes run before those from inner scopes when both apply to the request path? (The exact ordering within a scope should respect the order given to `applyMiddleware()`; ordering across nested scopes should be deterministic and consistent with how the router accumulates them.)
- Scoped middleware may short-circuit the pipeline by returning a response without calling `$next`. The final `$next` must only be invoked if the entire middleware stack passes control forward.
- When no path-specific middleware are defined, there must be zero behavioral change (no additional overhead aside from the lookup).
- Must be compatible with the existing redirect exception handling already present in `RoutingMiddleware`.

Connect path specific middleware up to the routing process.


Fix `EntityContext::isRequired()` incorrectly adding `required="required"` when `allowEmpty` uses a callable condition

At render time, `EntityContext::isRequired()` evaluates whether a field is required by calling `Validator::isEmptyAllowed()`. However, when a field’s `allowEmptyString()` (or similar) is given a callable condition (e.g., `allowEmptyString('title', 'message', fn($context) => $context['newRecord'])`), the callable may depend on submitted form data that is not yet available. The current logic treats the callable as a truthy value, leading `isRequired()` to return `true`, which causes `FormHelper` to add `required="required"` to the input even though the actual requirement is conditional on the POST body.

To fix this, `isRequired()` must return `null` whenever the allow-empty condition is a callable. Returning `null` signals to `FormHelper` that the required status cannot be determined at render time, so the attribute must not be added. Non‑callable conditions (`true`/`false`) should continue to work as before.

Ensure this change applies across all `isRequired` lookups, including those for nested/associated data (e.g., hasMany collections).

Fix isRequired() incorrectly adding required="required" when allowEmp… (#19291)


A bug report describing the observed misbehavior, expected resolution, and impact:

---

**Title:** PSR-15 closures are incorrectly wrapped as double-pass middleware in MiddlewareQueue

**Description:**
When adding a PSR-15 compliant closure to `MiddlewareQueue` (e.g. `function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { ... }`), the queue unconditionally wraps all non-`MiddlewareInterface` callables in `DoublePassMiddleware`. This causes a parameter mismatch at runtime because `DoublePassMiddleware` expects a three-argument signature (`$request, $response, $next`), leading to `TypeError` or unexpected behavior.

Developers who want to use PSR-15 closures directly are forced to manually wrap them in `CallableMiddleware` before adding them to the queue, which is unintuitive and breaks the convenience of passing callables directly to `add()`/`prepend()`/`insert*()`.

**Expected behavior:**
The queue should intelligently detect PSR-15 closures (closures with two parameters, accepting a request and a request handler) and wrap them in `CallableMiddleware` instead of `DoublePassMiddleware`. Closures with three parameters (the legacy double-pass signature) and other callable forms (arrays, strings, invokable objects) should continue to be wrapped as double-pass middleware for backward compatibility.

This change would allow developers to pass PSR-15 closures directly and have them correctly integrated into the middleware pipeline without extra boilerplate.

**Additional need:**
`CallableMiddleware` should expose the original callable (e.g., via a `getCallable()` method) for inspection, testing, or debugging, similar to the existing method on `DoublePassMiddleware`. This would make it easier to verify middleware composition in tests.

**Impact:**
- Cannot use standard PSR-15 closures natively with the middleware queue, violating PSR-15 integration expectations.
- Forces manual wrapping, cluttering application code.
- Increases risk of subtle runtime errors if a developer forgets to wrap a PSR-15 closure.

---

Use reflection of avoid explicitly creating CallableMiddleware instance.


The I18n Package stores translations that can be either a plain string or an array (for plural forms and context-based messages). The current type hints and checks incorrectly assume messages are always strings, causing static analysis warnings and potential runtime type errors.

Update the `Package` class so all message-related methods (including the constructor, `setMessages`, `addMessage`, `addMessages`, `getMessages`, `getMessage`) accept and return `string|array` for individual message values and `array<string, string|array>` for the full map. In the `Translator`, adjust the `_context` handling in `translate()` to check that the resolved message is an array before accessing `$message['_context']`, preventing a type error when a string message is returned for a key that happens to be requested with context tokens.


## Feature Request: Plain-Text Error Rendering via the ErrorRendererInterface

### Motivation

Debugger currently formats all error output through its internal template system (`$_templates`). For console and log-file contexts, a simpler, machine-friendly plain-text format is desirable — one that leverages the newer `PhpError` value object and `ErrorRendererInterface` contract instead of relying on the older template-string interpolation approach currently baked into `Debugger::outputError()`.

### Acceptance Criteria

1. **New `txt` output format**
   When `Debugger::setOutputFormat('txt')` is active and `Debugger::outputError($data)` is called, the error is rendered as plain text instead of going through the existing template pipeline.

2. **Rendering delegate**
   The `'txt'` format is backed by a class implementing `ErrorRendererInterface`. The implementation receives a fully populated `PhpError` instance (code, message, file, line, and stack trace) and returns the rendered string. Debugger outputs that string directly without further transformation.

3. **Output structure**
   The plain-text output includes at minimum:
   - The human-readable error label (e.g., `notice`, `warning`)
   - The numeric error code
   - The error message
   - The file and line where the error occurred
   - A `Trace:` header followed by the formatted stack trace

   The exact format is a single `sprintf`-style string, but the precise layout is an implementation detail. What matters is that all key diagnostic fields are present and the output is suitable for CLI and log consumption (no HTML markup, no template variables).

4. **Backward compatibility**
   - Formats other than `'txt'` continue to use the existing template-based rendering path without any change in behavior.
   - The `HtmlFormatter`, `ConsoleFormatter`, `TextFormatter` classes used by `Debugger::exportVar()` are unaffected.
   - Custom formats registered via `Debugger::addFormat()` are unaffected.

5. **PhpError receives the stack trace in the expected shape**
   The `PhpError` constructor accepts a `$trace` array. The trace points passed to it include `file`, `line`, and a `reference` key for each frame (consistent with what the updated `formatTrace` with `'points'` format produces). This ensures the renderer can produce meaningful stack traces.

6. **No user-facing API surface beyond `setOutputFormat('txt')`**
   End users only need to call `Debugger::setOutputFormat('txt')` to opt in. The mapping from format name to renderer class is internal. There is no new public method on `Debugger` required for this change.

### Constraints / Safety Notes

- The renderer must not emit output itself; it returns a string. `Debugger::outputError()` remains responsible for `echo` and output buffering semantics.
- The renderer must not depend on any template strings or global state — it operates solely on the `PhpError` value object it receives.
- The renderer should be registered in such a way that additional renderers (e.g., a future `HtmlRenderer`) can be added by extending the internal format-to-renderer mapping, without further structural changes to `outputError()`.

Add a TextRenderer and integrate it into Debugger


---
**Title:** Static route cache does not invalidate when route extensions are added after registration

**Area:** Routing (`RouteCollection`, `Route`)

**Severity:** Medium – incorrect route matching can occur in applications that conditionally enable extensions on routes after they are added to a collection.

---

### Description

When a `Route` is added to a `RouteCollection`, the collection optimizes lookups by placing the route into a `staticPaths` map if the route’s template is a literal path with no placeholder tokens **and** the route currently has no extensions. Later, if `Route::setExtensions()` is called on that same route instance (e.g., enabling JSON or XML extension parsing), the route remains in the `staticPaths` map unchanged. During `parse()` and `parseRequest()`, the collection does a direct indexed lookup in `staticPaths` and attempts to match the route without ever checking whether the route now has extensions. This causes the route to match URLs without honoring its extension requirements, and can shadow other routes that should have matched instead.

### Expected Behavior

- If a route gains extensions after it has already been added to the collection, it must **no longer be matched via the static-paths fast path**.
- The `staticPaths` fast path should only be used for routes that are extension-free *at the time of parsing*, not merely at the time of registration.
- The `routes()` method should continue to return all registered routes in the appropriate order (descending by path-segment length), regardless of whether they are in a static or dynamic cache.

### Concrete Scenario

```php
$builder = new RouteBuilder($collection, '/');
$route = $builder->connect('/foo', ['controller' => 'Articles']);
// At this point the route has no extensions and is placed in staticPaths.

$route->setExtensions(['json']);
// The route now expects an extension, but is still in staticPaths.

// A request to /foo.json should be parsed by this route and get _ext => 'json'.
// A request to /foo (no extension) should NOT match this route.
```

Without the fix, `/foo` continues to match the route because the static-paths fast path skips extension validation.

### Constraints

- The fix must not change the public API of `RouteCollection` in a breaking way.
- The ordering semantics of `routes()` must remain stable: routes with longer path segments should sort first.
- The performance of static-path matching for routes that genuinely remain static and extension-free should not regress meaningfully.
- The `$_paths` (currently called `$dynamicPaths` in older versions) internal storage may need to become the authoritative list of all routes to simplify caches and avoid stale-state bugs like this one.


Replace uses of `Cake\Filesystem\Folder` in `Cake\TestSuite\TestSuite` with the `Cake\Filesystem\Filesystem` class. The `Folder` class is deprecated.

- `addTestDirectory` must collect only `.php` files directly in the given directory (non‑recursive). Use `Filesystem::find` with a `'/\.php$/'` regex filter.
- `addTestDirectoryRecursive` must collect `.php` files recursively, but **skip hidden directories** (names starting with `.`). Use a new `Filesystem::findRecursive` method that accepts the same filter types as `find` (regex string or callback).
- Implement `findRecursive` in `Filesystem`, returning a flat iterator over all matching files while excluding hidden directories before applying the user filter.


### Feature request: Content-type negotiation fallback and NegotiationRequiredView

#### Context
When a controller returns a list of view classes via `viewClasses()`, the framework uses the `Accept` header to pick the best matching view class. If no match is found, the current implementation returns `null` and falls through to a default error response, which typically renders an HTML error page. For API-only applications this is undesirable: it sends an HTML body (often larger and slower than necessary) and does not signal to the client that it *must* pick a supported content type.

Developers have no way to define a true “fallback” view class that should be used when no specific content-type matches, nor is there a built-in means to enforce strict negotiation (return a 406 Not Acceptable with an empty body).

#### Ask
1. **Allow a “match-all” fallback view class**
   In the content-type resolution logic driven by `viewClasses()`, support a reserved content-type value that matches **any** `Accept` header when no explicit content-type has a higher preference. When such a class is present, it must be used instead of returning `null`.

2. **Provide `NegotiationRequiredView`**
   Ship a ready-to-use view class that leverages the fallback mechanism. It must:
   - Return an HTTP `406 Not Acceptable` status.
   - Render an empty body (no template or layout).
   - Not set a `Content-Type` header (the fallback sentinel must not leak into the response).

3. **Ensure the fallback does not pollute the response content-type**
   When the selected view class reports its content-type as the fallback sentinel, the `View` layer must skip setting the `Content-Type` header entirely, as the value would be invalid.

#### Why
This gives application authors a lightweight way to require clients to negotiate content types (by placing `NegotiationRequiredView` at the end of their `viewClasses()` list). It also provides a general extension point for other fallback strategies (e.g., a custom view that returns a JSON error body) without breaking existing semi-default HTML error handling.

Add fallbacks for `viewClasses` and a view to require negotiation.


As a developer using CakePHP’s form helper to render datetime inputs, I want to control the exact format of the displayed value so that I can omit seconds or adjust presentation to match user expectations, without relying on automatic format derivation.

**Acceptance criteria:**

- The `DateTimeWidget` (used by `FormHelper`) accepts an optional `'format'` key in its options array. The value must be a string compatible with PHP’s `date()` function.
- When `'format'` is provided:
  - The rendered `<input>` element’s `value` attribute uses that format string to display the date/time value (converted to the input’s timezone if specified).
  - The `step` attribute is **not** inferred from the database column type or the input `type`. Instead, no `step` attribute is generated unless an explicit `step` value is passed in the options.
  - The custom format works for all supported input types (`'date'`, `'time'`, `'datetime-local'`).
- When `'format'` is **not** provided, the existing behavior is completely unchanged:
  - The format is determined by the input `type` (e.g., `'Y-m-d'` for date, `'H:i:s'` for time, `'Y-m-d\TH:i:s'` for datetime-local).
  - The `step` attribute is automatically set based on the input `type` and, when applicable, the fractional-second precision of the underlying database column (for `datetime-local` and `time` types).
- Any explicitly set `step` option (whether or not a custom format is given) is respected and passed through to the HTML output.

**Example scenarios:**

1. **Custom datetime format without seconds**
   Options: `'type' => 'datetime-local'`, `'val' => '2019-02-03 10:11:12'`, `'format' => 'Y-m-d\TH:i'`
   Expected output: `<input type="datetime-local" name="..." value="2019-02-03T10:11" />` (no `step` attribute).

2. **Custom time format without seconds**
   Options: `'type' => 'time'`, `'val' => '10:11:12'`, `'format' => 'H:i'`
   Expected output: `<input type="time" name="..." value="10:11" />` (no `step`).

3. **Custom format with explicit step**
   Options: `'type' => 'datetime-local'`, `'val' => '2019-02-03 10:11:12'`, `'format' => 'Y-m-d\TH:i'`, `'step' => 120`
   Expected output: `<input type="datetime-local" name="..." step="120" value="2019-02-03T10:11" />`.

4. **No custom format – preserves default behavior**
   If no `'format'` is set, the widget behaves exactly as it did before (e.g., for a `datetime-local` with a fractional-seconds column, it uses the `'Y-m-d\TH:i:s.v'` format and an appropriate `step`).

**Additional constraints:**

- The `'format'` string must be a valid `date()`-compatible pattern. Invalid patterns may cause an exception during formatting (acceptable as user error).
- The presence of `'format'` should not interfere with other options like `'timezone'` or `'val'`; it only changes the output format and the `step` defaulting logic.
- The feature must work through the public `render()` method of `DateTimeWidget`, which is the standard entry point used by `FormHelper`.

Allow setting custom formats for datetime inputs.


**Title:** Add `cacheKeyPrefix` connection config for schema metadata cache keys

**Background**
When `'cacheMetadata' => true` is set on a database connection, the schema collection (`CachedCollection`) caches table descriptors. The cache key is currently formed as `<connectionName>_<tableName>`, where the prefix is always the connection’s `name`.

**Problem**
In deployments where multiple connections share the same cache store (e.g., different database servers with the same logical connection name, or multi-tenant setups) the fixed prefix can lead to cache key collisions. Users need a way to override the prefix per connection so that they can isolate or group cached metadata intentionally.

**Proposed change**
1. Allow a new configuration key `'cacheKeyPrefix'` in the connection’s config array.
2. When `CachedCollection::cacheKey()` is called, if `'cacheKeyPrefix'` is set in the connection config, use that value as the prefix; otherwise use the connection’s `name` (current behaviour).
3. Document the new option in the `Connection` constructor’s docblock with a description of its effect, alongside the existing `cacheMetadata` option.

**Acceptance criteria**
- A connection configured with `'cacheKeyPrefix' => 'foo'` and `'cacheMetadata' => true` produces cache keys like `'foo_tableName'`.
- A connection without `'cacheKeyPrefix'` continues to produce keys like `'connectionName_tableName'`.
- The option is discoverable via the constructor’s PHPDoc.

Backport of PR #13472 of 4.x, issue #13471


<title>Deprecate custom format strings for Syslog log engine and unify formatter behavior</title>

**Background**

Currently, `Cake\Log\Engine\SyslogLog` accepts a `format` config key (e.g., `'format' => '%s: %s'`) to control how messages are rendered before being sent to syslog. This bypasses the standard formatter pipeline, making the engine inconsistent with other log engines (`FileLog`, `ConsoleLog`) where custom formatting is handled through dedicated formatter classes.

The dedicated `SyslogFormatter` class is essentially a thin wrapper around `sprintf` with a `format` config key. Its only job is to produce a `"$level: $message"` string—and the level is rendered with `ucfirst()`, unlike other formatters that keep the level in its original case.

Meanwhile, `DefaultFormatter` always prepends a timestamp to messages. For syslog, timestamps are typically handled by the system logger itself, so the extra date prefix is redundant.

**Desired behavior**

1. **Deprecate the `format` config option on `SyslogLog`.** Users who currently set `'format' => '...'` should receive a deprecation warning instructing them to switch to a custom formatter class. For backward compatibility during the deprecation period, passing a `format` config should still work but should route through a newly added `LegacySyslogFormatter` that preserves the old behavior (including `ucfirst` on the level).

2. **Make `SyslogLog` use `DefaultFormatter` by default**, with a new `includeDate` option set to `false` so that no timestamp is prepended (syslog provides its own). The `DefaultFormatter` should gain this `includeDate` config key (defaulting to `true` for other engines).

3. **Lowercase log levels in `DefaultFormatter` output.** To match syslog conventions and keep output consistent with what `LegacySyslogFormatter` produces (aside from the date), `DefaultFormatter::format()` should render the level as-is instead of running `ucfirst()` on it. This affects all engines that use `DefaultFormatter`, not just syslog.

4. **Remove the old `SyslogFormatter` class.** It is superseded by `DefaultFormatter` (for non-deprecated usage) and `LegacySyslogFormatter` (for the deprecation path).

**Acceptance criteria**

- Calling `new SyslogLog(['format' => '...'])` triggers a deprecation warning and routes formatting through `LegacySyslogFormatter`.
- Calling `new SyslogLog()` with no `format` key uses `DefaultFormatter` with `includeDate = false` and no deprecation.
- `DefaultFormatter` with `includeDate = true` (the default) produces `"Y-m-d H:i:s level: message"` with the level in its original case (lowercase).
- `DefaultFormatter` with `includeDate = false` produces `"level: message"`.
- The `SyslogFormatter` class is deleted.
- A new `LegacySyslogFormatter` class exists with the old `sprintf`-based behavior and is marked `@deprecated 4.3.0`.
- No `ucfirst()` call remains on the level in `DefaultFormatter::format()`.

Deprecate syslog formatting with custom format string


When a controller’s `$modelClass` property is set to a fully qualified class name (e.g. `App\Model\Table\ArticlesTable`), accessing the corresponding magic property (e.g. `$this->Articles`) should automatically load the model using that FQCN. Currently automatic loading fails because the property resolution incorrectly applies `pluginSplit` to the FQCN, which does not handle namespaces. The fix must detect a FQCN (presence of a backslash) and derive the short alias from it, then call `loadModel()` with the FQCN, so that the model is instantiated correctly. The behavior for non-FQCN values (short names, plugin-prefixed names) must remain unchanged.

Fix auto loading of model when ModelAwareTrait::modelClass is set to FQCN.


**Feature Request: Customizable Asset URL Building in UrlHelper**

Currently, `UrlHelper` hardcodes all asset URL generation (image, CSS, script, generic asset, timestamp, webroot) to the static methods of `Cake\Routing\Asset`. There is no built-in way for an application to alter or extend this logic without replacing the entire helper.

We need to make the asset URL builder replaceable via configuration so that applications can implement their own URL building rules (for example, appending a cache-busting hash, rewriting paths for a CDN, or integrating with a local asset pipeline) while still using `UrlHelper`.

**Acceptance Criteria**

1. **Configurable class name**
   `UrlHelper` must accept a configuration key, e.g. `assetUrlClassName`, that specifies a fully qualified class name to use for asset URL generation.
   When this key is not set, the helper must default to `Cake\Routing\Asset` – no change in observable behavior.

2. **Delegation for all asset methods**
   When a custom class is configured, the following helper methods must delegate to the corresponding static method of the custom class:
   - `image()` → `imageUrl()`
   - `css()` → `cssUrl()`
   - `script()` → `scriptUrl()`
   - `assetUrl()` → `url()`
   - `assetTimestamp()` → `assetTimestamp()`
   - `webroot()` → `webroot()`

   The custom class must receive the same parameters and options (including any theme option from the view) as the core `Asset` class does. The return values should still be HTML‑escaped by the helper if that is the current behavior.

3. **Error when class is missing**
   If the configured class name cannot be resolved (i.e., the class does not exist or cannot be loaded), a `Cake\Core\Exception\CakeException` must be thrown during helper initialization, before any method call.

4. **Backward compatibility**
   Omitting the `assetUrlClassName` configuration must result in identical behavior to previous CakePHP versions. Existing use cases must not be affected.

**Non‑obvious Constraints**

- The custom class must implement the same public static method signatures as `Cake\Routing\Asset`. There is no interface to enforce this, but the helper must rely on duck typing; no additional runtime checks are required.
- Theme and plugin path handling should remain as transparent as possible: the custom class receives the same `$options` array (including `'theme'`) and is responsible for incorporating it into the URL, just as the core class does.
- The feature should not alter any other behavior of `UrlHelper` (e.g., `build()`, `buildFromPath()`, event methods).

Allowing apps to change asset URL building


**Title:** Deprecate `Form::errors()` and introduce `getErrors()` getter for consistency with `EntityTrait`

**Background**
- `EntityTrait` already has both `getErrors()` (getter) and `errors()` (get/set), but the docblock examples for `errors()` still use the old `errors()` call pattern.
- `Form` only exposes `errors()` as the getter for validation errors, with no corresponding `getErrors()` method.
- Internal callers like `FormContext` and the `Form` debug info use `errors()` directly.

**Request**
1. Add a new public method `getErrors()` to `cake\Form\Form` that returns the `$_errors` array.
2. Deprecate `Form::errors()` – keep it but add a deprecation warning and delegate to `getErrors()`.
3. Update all internal usages of `Form::errors()` to call `getErrors()`:
   - `Form::__debugInfo()`
   - `Cake\View\Form\FormContext::error()`
4. Update the README example to use `$form->getErrors()`.
5. Update the docblock examples in `EntityTrait::errors()` so they reference `getErrors()` for clear getter usage, removing any lingering `$entity->errors('salary')` examples.
6. Ensure all tests are updated to use the new `getErrors()` getter and that the deprecated `errors()` path still works but triggers the deprecation notice.

**Acceptance Criteria**
- `$form->getErrors()` returns the same array as `$form->errors()` did.
- `$form->errors()` still works but emits a deprecation warning.
- `FormContext::error()` uses `getErrors()` internally.
- Entity `errors()` examples reflect `getErrors()` for the single-field and all-fields getter scenarios.


Add an 'events' hook to plugins and the application so they can register event listeners before request or command dispatch, analogous to the existing bootstrap, routes, middleware, services hooks.

Details:
- Plugins may optionally define an `events(EventManagerInterface $eventManager): EventManagerInterface` method that adds listeners and returns the event manager.
- The base plugin class should provide a no-op implementation, guard the hook with an `eventsEnabled` flag, and include `'events'` in the set of valid hooks.
- `BaseApplication` must expose `pluginEvents(EventManagerInterface)` that calls the events method on every loaded plugin that has the hook enabled, then an `events(EventManagerInterface)` method that the application itself can override to register application-level listeners.
- The event manager should be wired in both `BaseApplication::handle()` (before invoking middleware/controller) and `CommandRunner::runCommand()` (before running the command). In both cases, call `pluginEvents()` then `events()`, and set the resulting event manager on the application / command dispatcher so events fire correctly.

5.next: add new events hook to BaseApplication and BasePlugin (#17849)


We need a bug fix in the `CsrfProtectionMiddleware` to preserve backward compatibility with legacy CSRF tokens after the introduction of salted tokens.

**Context**
Before salting was added, CSRF tokens were hexadecimal strings of length `TOKEN_WITH_CHECKSUM_LENGTH` (currently 56 characters). The recent salting improvement introduced base64 encoding and a salt XOR step. However, old hexadecimal tokens happen to also be valid base64 strings; when the middleware blindly applies `base64_decode` and the salting/unsalting process to them, the token becomes garbled or causes validation failures. Users who hold an existing hex token cookie from a previous deployment can no longer submit forms or use the application.

**Observable acceptance criteria**

1. **Detection of old tokens**
   The middleware must recognise a token as “old” when it matches the pattern `/^[a-f0-9]{TOKEN_WITH_CHECKSUM_LENGTH}$/`. No other heuristic (e.g., failed base64 decode) should be used, because hex strings are valid base64 but yield a different binary interpretation.

2. **Token in request attribute**
   When an old token arrives in the `csrfToken` cookie, the `csrfToken` request attribute must be set to the raw hexadecimal string, not a salted/base64‑decoded version.

3. **Validation of old tokens**
   The HMAC checksum validation for old tokens must operate directly on the hex string (no base64 decode). The token’s structure and length checks (`strlen > TOKEN_VALUE_LENGTH`) must still apply.

4. **Salting / unsalting are no‑ops for old tokens**
   - `saltToken($token)` must return `$token` unchanged when the input is an old token.
   - `unsaltToken($token)` must return `$token` unchanged when the input is an old token.
   This prevents the middleware from accidentally mangling the token on subsequent requests or cookie writes.

5. **New tokens remain unchanged**
   `createToken()` must continue to produce fresh salted, base64‑encoded tokens. The backwards compatibility logic must only affect the handling of tokens that match the old hex format, not the generation of new ones.

**Side constraints**
- The fix must not alter cookie names or the overall CSRF flow (double submit cookie, token in request data/headers).
- All safe HTTP methods (GET, HEAD, OPTIONS) must still pass through without CSRF enforcement, regardless of token format.
- Post/put/patch/delete requests with a correct old token must succeed; incorrect or missing tokens must still throw `InvalidCsrfTokenException`.


The `Controller::paginate()` method unconditionally loads the deprecated `PaginatorComponent` internally via `$this->loadComponent('Paginator')`, even when the user has not explicitly added it to their controller.
This forces every paginate call to instantiate a deprecated component, making it impossible to fully adopt the non‑component `Cake\Datasource\Paging\Paginator` class and removing the ability to avoid deprecation notices from the core.

**Expected behavior**
- `Controller::paginate()` should **not** automatically load `PaginatorComponent`.
- When a user has manually loaded `PaginatorComponent` (e.g. `$this->loadComponent('Paginator')`), the existing `$this->Paginator` property should still be used for pagination to preserve backward compatibility.
- In all other cases, `paginate()` should directly use a `PaginatorInterface` implementation (by default `Cake\Datasource\Paging\Paginator`).
- The `'paginator'` configuration key in pagination settings should be honoured: if a fully qualified class name (implementing `PaginatorInterface`) or an instance is passed, it must be used for pagination.
- When the paginator throws a `PageOutOfBoundsException`, the controller should throw a `NotFoundException` to keep the behaviour consistent with the deprecated component.
- The `PaginatorComponent` constructor itself should emit a clear deprecation warning, so that any remaining explicit usage is flagged immediately.

**Impact / motivation**
- Users cannot migrate away from `PaginatorComponent` because the core implicitly re-loads it on every `paginate()` call.
- This undermines the deprecation of `PaginatorComponent` (introduced in 4.4.0) and blocks its removal in a future major version.
- The implicit load also introduces an unnecessary performance overhead and prevents custom paginator classes from being used without the legacy component.

A fix should decouple the controller’s pagination logic from the component, while still respecting any manually provided `PaginatorComponent` instance for existing applications.

Remove implicit use of PaginatorComponent from controller.


Add getter and setter methods for the request and response instances in `Controller`, and deprecate direct public property access. The new methods should be:

- `getRequest(): ServerRequest`
- `setRequest(ServerRequest $request): $this`
- `getResponse(): Response`
- `setResponse(Response $response): $this`

The `setRequest` method must replicate the side effects of setting `$this->request` directly: it must update `$this->plugin` from the request's `plugin` param and `$this->passedArgs` from the request's `pass` param. Return the controller instance to allow chaining. The existing `$request` and `$response` public properties must be marked as deprecated with a note pointing to the new methods.

Add setters and getters for request and response.


There is currently no way to temporarily disable query logging on a `Database\Driver` instance after a logger has been attached. Once a logger is set (e.g. via `setLogger()` or by configuring `'log' => true` in the connection options), all queries are unconditionally logged. This makes it impossible to suppress logging for specific blocks of code without removing and re-adding the logger, which is error-prone and can interfere with other logging consumers.

It should be possible to toggle query logging at runtime without altering the logger itself. The driver should expose `enableQueryLogging()` and `disableQueryLogging()` methods that control whether calls to `Driver::log()` actually send messages to the configured logger.

Expected behavior:
- Calling `disableQueryLogging()` stops `log()` from forwarding messages, even if a logger is present. `log()` should return `false` in that state.
- Calling `enableQueryLogging()` resumes normal logging to the currently configured logger.
- When `setLogger()` is called, query logging should be implicitly enabled (so existing code that attaches a logger continues to work out of the box).
- The initial logging state (logger presence, enabled/disabled) should still respect the `'log'` connection option: when `'log'` is `false`, no logger is created, and logging stays inactive.

This would give developers a straightforward way to silence SQL logging for specific query sequences without dismantling the logging infrastructure.

#17951 restore functionality to disable query logging in runtime


We need the `Socket` class to support connecting to Unix domain sockets (file sockets) by allowing the `port` configuration to be explicitly set to `null`.

Currently, when `Socket::connect()` builds the remote target, it unconditionally appends `:port` to the host, ending the target string with a colon even when the port is null.
This corrupts Unix socket addresses like `unix:///var/run/service.sock`, causing `stream_socket_client()` to fail.

**Acceptance Criteria**

1. When a `Socket` instance is configured with `'port' => null`, the final connection target used with `stream_socket_client()` must **not** include a colon or any port component.
   - For example, a host of `unix:///tmp/app.sock` and `port => null` should produce `unix:///tmp/app.sock`, not `unix:///tmp/app.sock:`.

2. When `port` is a non-null value (integer or string), the existing behavior must remain unchanged—the port is appended as `:port`.

3. All other connection settings (`timeout`, `persistent`, stream context, etc.) must continue to be honored exactly as before, regardless of whether `port` is null.

4. If the underlying `stream_socket_client()` call encounters an error (e.g., socket not found, permission denied), `connect()` must still raise a `SocketException` with the appropriate error number and message, just as it would for any failed connection.

No other public API changes are expected. The fix should be invisible to existing usage that passes a concrete port.

Socket connect works with unix file sockets using port null


**Title:** Application event listeners registered via `EventAwareApplicationInterface` are not active during `Console.buildCommands`

**Environment:** CakePHP ^5.1, Console CommandRunner

**Description:**
When a `ConsoleApplicationInterface` also implements `EventAwareApplicationInterface`, the `events()` and `pluginEvents()` methods are intended to register event listeners that should be available throughout the console request lifecycle. However, these listeners are **not** active when the `Console.buildCommands` event is dispatched. This prevents developers from using application-level event listeners to dynamically modify the command collection (e.g., adding, removing, or aliasing commands) before commands are resolved.

**Steps to reproduce:**
1. Create a Cake application that implements `EventAwareApplicationInterface`.
2. In the `events` method, register a listener on `Console.buildCommands` (e.g., to add a custom command).
3. Run any console command (e.g., `bin/cake version`).
4. Observe that the listener is never invoked.

**Expected behavior:**
Listeners attached via `events()` and `pluginEvents()` should be called when the `Console.buildCommands` event is dispatched. This would allow applications and plugins to hook into the command-building process, consistent with the event-driven architecture.

**Actual behavior:**
The `CommandRunner` currently calls `$app->events()` and `$app->pluginEvents()` inside `runCommand()`, which is invoked *after* the `Console.buildCommands` event has already been dispatched. As a result, any listeners targeting that event are not yet registered and never fire.

**Impact:**
Any logic that relies on `Console.buildCommands` to alter the command collection (common when building modular or plugin-based console suites) is silently ignored, reducing extensibility and breaking expected event flow.

**Root cause analysis:**
In `CommandRunner::run()`, the initial command collection is built and the `Console.buildCommands` event is dispatched *before* the application’s `events()` and `pluginEvents()` methods are ever called. The event manager is augmented with these listeners only later, inside `runCommand()`.

```php
// In run():
$commands = new CommandCollection([...]);
$this->dispatchEvent('Console.buildCommands', ['commands' => $commands]);

// ... later, for each command invocation:
protected function runCommand(...) {
    // event manager is augmented here – too late for buildCommands
    if ($this->app instanceof EventAwareApplicationInterface) {
        $eventManager = $this->app->events($eventManager);
        $eventManager = $this->app->pluginEvents($eventManager);
    }
}
```

**Proposed solution:**
Move the call to `events()` and `pluginEvents()` into the `run()` method, before the initial command collection is created and before `Console.buildCommands` is dispatched. The augmented event manager should then be set on the runner so that subsequent event dispatches (including `Console.buildCommands`) honor the registered listeners.

This ensures that:
- Application and plugin event subscribers are available for the `Console.buildCommands` event.
- Any other early event dispatched in `run()` also sees the complete listener set.
- The existing behavior of injecting the event manager into commands later in `runCommand` remains unchanged.

**Additional context:**
- The order of listener registration is important: `$app->events()` should be called before `$app->pluginEvents()` to maintain the usual layering (application overrides plugins).
- The event manager proxy chain (if any) must be preserved correctly when the augmented instance is set back on the runner.
- This fix does not alter when commands themselves receive the event manager; that can continue to happen in `runCommand()`.

Prioritize event manager setup in CommandRunner


**Feature Request: Dedicated Method for Quarter Date Ranges**

As a developer using CakePHP’s date/time utilities, I want a clear, self-documenting way to get the start and end date of the fiscal or calendar quarter that a given date falls into, without passing a cryptic boolean flag. Currently, `DateTime::toQuarter($range = false)` and `TimeHelper::toQuarter($dateString, $range = false)` mix two distinct responsibilities (returning the quarter number vs. returning a date range) into a single method via an optional boolean.

### Acceptance Criteria

1. **New `toQuarterRange()` method on `DateTime`**
   - `DateTime::toQuarterRange()` returns an array with exactly two elements: `[start_date, end_date]`.
   - Both values are strings in `'Y-m-d'` format.
   - The range corresponds to the calendar quarter containing the date instance:
     - Q1: `'YYYY-01-01'` – `'YYYY-03-31'`
     - Q2: `'YYYY-04-01'` – `'YYYY-06-30'`
     - Q3: `'YYYY-07-01'` – `'YYYY-09-30'`
     - Q4: `'YYYY-10-01'` – `'YYYY-12-31'`
   - Examples:
     - `(new DateTime('2007-12-25'))->toQuarterRange()` → `['2007-10-01', '2007-12-31']`
     - `(new DateTime('2007-01-01'))->toQuarterRange()` → `['2007-01-01', '2007-03-31']`
     - Leap year dates (e.g., `2024-02-29`) fall within Q1 and return `['2024-01-01', '2024-03-31']`.

2. **New `toQuarterRange()` method on `TimeHelper`**
   - `TimeHelper::toQuarterRange($dateString)` accepts any valid date input (timestamp, string, or DateTime object) and returns the same `['Y-m-d', 'Y-m-d']` range for that input.
   - Examples:
     - `$helper->toQuarterRange('2007-12-25')` → `['2007-10-01', '2007-12-31']`
     - `$helper->toQuarterRange('2007-05-15')` → `['2007-04-01', '2007-06-30']`

3. **Deprecation of `$range` flag in `toQuarter()`**
   - Calling `DateTime::toQuarter(true)` or `TimeHelper::toQuarter($date, true)` must continue to work and return the same range array as `toQuarterRange()`.
   - However, those calls must emit an `E_USER_DEPRECATED` warning with a message directing the developer to use `toQuarterRange()` instead.
   - The `$range` parameter will be removed in a future major release; the deprecation message should make that clear.

4. **No breaking change to quarter number retrieval**
   - `DateTime::toQuarter()` (without argument, or with `false`) must continue to return the integer quarter number (1, 2, 3, or 4) based on the month.
   - `TimeHelper::toQuarter($dateString)` (without the second argument, or with `false`) must continue to return the integer quarter number.

5. **Edge Case Handling**
   - All methods must correctly handle dates at the boundaries between quarters (e.g., 31 December returns Q4, 1 April returns Q2).
   - The year used in `toQuarterRange()` must be the year of the date being processed, even when the quarter spans a year boundary (Q4 extends to December 31 of the same year; no special handling for fiscal years needed unless specified by a future parameter).

By providing a dedicated `toQuarterRange()` method and deprecating the boolean flag, the API becomes more expressive and reduces the risk of misinterpreting a `true`/`false` argument in calling code.

Split out toQuarterRange() from TimeHelper::toQuarter() (#18816)


Add `Cache::getEngine($config)` as the preferred method to retrieve a cache engine instance and deprecate the existing `Cache::engine()` method. The new method must behave identically: return the engine for the given configuration name, or return a `NullEngine` when caching is disabled.

Adding getEngine() to Cache and updating tests


When a `ComponentRegistry` is created with a `ContainerInterface`, component factories defined in the container that depend on the `ComponentRegistry` instance cannot resolve the registry because the registry never registers itself in the container. This prevents container‑managed component creation from receiving the correct registry (for example, to pass it to a component’s constructor).

### Expected Behavior
When `ComponentRegistry` is instantiated with a container, the registry should make itself available in that container under the `ComponentRegistry::class` key. That way, any factory closure or service definition that type‑hints `ComponentRegistry` (or requests it as an argument) will receive the concrete registry instance that is currently loading the component.

### Observed Problem
- If a container defines a component factory such as `function (ComponentRegistry $registry, array $config) { … }`, the container cannot inject `$registry` because the instance is missing from the container.
- This means container‑based component factories cannot rely on an injected registry, breaking patterns where the factory needs to pass the registry to the component’s constructor.

### Impact
- Controller factories, controllers, and any other code that use a container with `ComponentRegistry` cannot have components built via container factories that depend on the registry.
- The fix would not affect registries that are instantiated without a container, and it would not change the public API. The registry would simply be added to the container during construction, making it available for component resolution during `load()`.

### Additional Considerations
- The registry must be added to the container **after** the constructor’s existing logic so that it does not interfere with other service definitions.
- Only the active registry instance should be registered; multiple registries (e.g., in different scopes) should each register themselves independently.


**Feature Request: Brace-enclosed placeholder syntax for route templates**

**Motivation**
CakePHP’s current route placeholder syntax (`:param`) works well, but it has two usability limitations:

- Placeholders are hard to spot when mixed with literal colons or URL structures.
- A placeholder cannot sit directly next to other alphanumeric characters without a separator (e.g. `:widthx:height` is not possible).

Adding an alternative `{param}` syntax addresses both issues: it is visually distinct, and it allows placeholders to be embedded mid-word.

---

**Acceptance Criteria**

1. **Brace placeholders are valid route parameters**
   A route template may use `{name}` in place of `:name`.
   *Example:* `/products/{id}/reviews/{reviewId}` must match and generate URLs with the same behavior and semantics as `/products/:id/reviews/:reviewId`. All parameter names, defaults, patterns, and pass lists work identically.

2. **No mixing of syntaxes in a single route**
   If a template contains at least one `{`, only `{param}` tokens are treated as placeholders. Any `:param` segments are treated as **literal** text (part of the static path).
   A template that uses only colon placeholders continues to behave exactly as before, with zero change.

3. **Mid-word placeholders**
   Brace placeholders can be placed directly next to other word characters without a separator.
   *Example:* `/images/{id}/{width}x{height}` matches URLs like `/images/123/640x480`, and URL generation produces the same string.
   This enables patterns such as `/files/{filename}.{ext}` or `/users/{username}-{id}`.

4. **No regression for existing colon-style routes**
   Every existing route definition that uses only `:param` syntax must compile, parse, match, and generate URLs exactly as it did before. The behavior for such routes is unchanged.

5. **Consistent matching semantics**
   The brace syntax is purely an alternative way to **declare** placeholders in the template string. All other route features—such as option patterns, greedy matching, optional segments, extension handling, host conditions, and middleware—must behave identically regardless of which placeholder style is used.

---

**Additional Notes**
- The new syntax is an opt-in enhancement; developers must explicitly choose to use `{param}` when defining routes.
- The change must be backward-compatible with any application that currently uses colon placeholders.

Add initial implementation of brace placeholders.


The HTTP client test suite emits deprecation warnings when executed. Update the tests and the supporting production code so that running the full suite produces **zero deprecation warnings**, while all existing behavioural assertions remain valid.

### Acceptance criteria

1. **No deprecation warnings from production code used by tests**
   - Calls to deprecated methods `Request::method()` and `Request::version()` in the Stream adapter must be replaced with their PSR‑7 equivalents (`getMethod()`, `getProtocolVersion()`) so that request building does not trigger warnings.
   - The deprecation warning message inside `Request::cookie()` must correctly reference the method name `Request::cookie()` rather than `Request::header()`.
   - Internal use of `Request::header()` for setting the `Content-Type` header in the body-processing path must switch to the non‑deprecated `Request::addHeaders()`.

2. **Test code uses non‑deprecated request construction and accessors**
   - Tests that previously built requests through fluent calls like `$request->url(...)->header(...)->cookie(...)` should instead construct requests directly with the constructor (`new Request($url, $method, $headers)`), eliminating calls to deprecated `header()` and `cookie()`.
   - Assertions on responses must use the PSR‑7 accessors (`getHeaderLine()`, `getBody()`, `getStatusCode()`, `getCookie()`) instead of the deprecated `header()`, `body()`, `statusCode()`, and `cookie()` methods.

3. **Deprecated test classes and fixtures are handled without warnings**
   - The deprecated `CookieCollection` test case (and any other test using that class directly) must be annotated with a `@group deprecated` marker, and instantiation of deprecated classes in such tests must be wrapped in a deprecation suppressor so that the tests still execute but do not emit warnings.
   - Tests that exercise deprecated authentication adapters should be similarly marked and suppressed.

4. **Cookie retrieval matches the new cookie collection API**
   - In the redirect test, `$client->cookies()->get($url)` must be replaced with the new `CookieCollection` API (e.g., checking membership with `$client->cookies()->has('redirect1')`), since the returned collection no longer has a `get($url)` method.

5. **Preserve existing test behaviour**
   - Every test that currently passes must continue to pass. No assertions should be weakened or removed; only the way they are invoked may change to avoid deprecated interfaces.
   - The order of headers in the stream context is not contractually required, but tests must confirm that all expected headers are present.

6. **No accidental side‑effects in production code**
   - The changes to production code (points 1‑2) must not alter the behaviour of the HTTP client when used outside the test suite—only the internal method calls should change, not the observable request/response handling.


Add a method to `DateTimeType` that accepts a user timezone to be used during marshalling.

When an application renders localized datetime inputs, submitted string values are in the user’s timezone. `DateTimeType::marshal()` currently parses those strings assuming the system/default timezone. This leads to incorrect results for users in other timezones.

The new `setUserTimezone(string|DateTimeZone|null $timezone): self` method should:

- store the given timezone (converted to `DateTimeZone` if a string is supplied).
- when marshalling a non‑empty string (including locale‑parsed strings), parse the value with the user timezone. After creating the `DateTime` object, if the user timezone differs from `defaultTimezone`, convert the result to the default timezone so that the internal representation remains consistent.
- when marshalling an array of date parts (`year`, `month`, `day`, etc.), use the user timezone as the default if no `timezone` key is present; a supplied `timezone` key takes precedence.

If a user timezone is not set (null), behaviour is unchanged. The change must apply to both direct format parsing and locale‑based parsing.
</output>

Allow specifying timezone to use when marshaling DateTimeType.


**Ticket: Paginator should populate `sort` and `direction` in paging parameters from default `order` when no request `sort` is given**

**Context**
`Cake\Datasource\Paginator::validateSort()` merges pagination options and prepares them for the query. When a default `order` is configured in the controller’s `$paginate` settings but no explicit `sort` parameter is present in the request/options, the resulting `pagingParams` currently have `sort => null` (and `direction` possibly defaulting to `'asc'` or remaining unset). This misrepresents the actual sort applied to the dataset.

**Desired behavior**
When `sort` is `null` (i.e. not provided via request or settings):

- If the configured `order` option contains exactly one non-numeric key (a single field with a direction, e.g. `['id' => 'asc']`), then `sort` should be set to that field name and `direction` to the corresponding value.
- If `order` contains multiple entries, or is a numerically-indexed list, or is otherwise ambiguous, `sort` should remain `null` (current behaviour).

This must only apply when `sort` was genuinely missing – any explicitly passed `sort` value (even empty string) should not be overridden. The change should affect the options returned by `validateSort()` (and consequently the `pagingParams` array returned by `getPagingParams()`).

**Why**
Helpers and templates often rely on `$this->Paginator->sort('field')` being correctly active for the column that is actually being sorted, including the initial page load. Without this fix, a default `'order' => ['id' => 'asc']` leaves no active sort column highlighted, even though the data is ordered by `id`.

**Acceptance Criteria**
- Paginating `$this->Paginator->paginate($table, [], ['order' => ['id' => 'asc']])` results in paging params with `'sort' => 'id'` and `'direction' => 'asc'`.
- Paginating with `['order' => ['field1' => 'asc', 'field2' => 'desc']]` or numerically-keyed arrays preserves `sort => null`.
- Providing a `'sort'` option (even empty) must not trigger the inference; the given value is used as-is.

Ensure paging params are properly set based on "order" in settings.


```markdown
Title: `.html`/`.htm` extensions cause `NotFoundException` instead of falling back to default view rendering

Component: `RequestHandlerComponent`

Description:
Requests that carry a `.html` or `.htm` extension in the URL (for example `/articles/index.html`) should be handled as standard HTML requests, using the default application view (e.g., `AppView`) without any extension-specific rendering modifications. Currently, the component throws a `NotFoundException` because `.html` and `.htm` are not mapped to a recognized MIME type.

Expected behaviour:
- When the extension is `html` or `htm`, no alternative view class, layout path, or subdirectory should be applied. The request should render identically to a request without any special extension.
- The response Content-Type should remain `text/html`.
- The extension should still be available in the request (e.g., `$this->RequestHandler->ext === 'html'`), but it should not influence the rendering stack.
- Accept header-driven content negotiation should continue to work: if an `Accept` header requests a different format (e.g., `application/xml`), that preference should still override the `.html` extension and trigger the corresponding view (e.g., `XmlView`).
- Any other unrecognized extension (not `html`/`htm`) must still raise a `NotFoundException`.

Steps to reproduce:
1. Enable URL extensions in routing (e.g., `Router::extensions(['html', 'htm'])`).
2. Request an action with a `.html` or `.htm` suffix, e.g., `/posts/view/1.html`.
3. Observe a 404 response because `NotFoundException` is thrown.
4. After the fix, the same request should render the default template with no layout/ rendering modifications.
```

Fix handling of `html` and `htm` extensions.


## Feature request: Scoped middleware registration and matching in RouteCollection

Currently, `RouteCollection` has no way to associate HTTP middleware with URL path prefixes.  Middleware cannot be registered once and reused, nor can it be scoped to particular sections of the route tree.  This forces developers to handle middleware attachment entirely outside the routing layer, making it difficult to (for example) apply an authentication middleware to all `/api/*` routes but not to public routes.

### Desired behaviour

1. **Middleware registration**
   - Introduce a method to register a named middleware callable inside `RouteCollection`.
   - The callable **must be an object** (a `Closure` or an invokable object).  Plain function-name strings (e.g. `'strlen'`) must be rejected with a `RuntimeException`.
   - After registration, the middleware can be referenced by its name elsewhere in the collection.

2. **Enabling middleware on path scopes**
   - Provide a way to activate one or more registered middleware names on a given **static URL path prefix** (placeholders / route parameters are out of scope for this iteration).
   - If any name in the list has **not** been registered, the operation must throw a `RuntimeException` indicating which name is missing and on which path.

3. **Retrieving applicable middleware for a URL**
   - Expose a method that, given a concrete URL path, returns the set of applicable middleware callables.
   - Applicable middleware is determined by **prefix matching**: any enabled path that is a **prefix** of the requested URL contributes its middleware.
   - **Order**: middleware from shorter matching paths appear first, followed by longers paths (shortest‑to‑longest path order).
   - **Deduplication**: if the same middleware name would be added more than once (because it is enabled on multiple matching path prefixes), it must be included only **once**, at its earliest position in the ordered set.
   - The returned value is a list of the actual callable objects, ready for use in a middleware pipeline.

### Use cases / impact

With this feature, developers can:

- Register a reusable “auth” middleware and apply it to `/api`, `/admin` etc., without duplicating the callable.
- Layer additional middleware (e.g. version‑specific logging) on deeper paths like `/api/v1`; the matching logic ensures both the global API middleware and the deeper middleware run, in a predictable order.
- Avoid accidental double‑execution when the same middleware is configured on multiple overlapping scopes.

Placeholder/parameterised scopes are explicitly not required for this initial implementation, but the design should leave room for adding them later.


I want the HTTP client to follow redirects automatically so that I don’t have to wire adapter-specific redirect handling into every request.

**Acceptance criteria**

- When I call `$client->send($request, $options)` and include `'redirect' => N` in the options array, the client follows up to N redirects before returning the final response.
- The `'redirect'` option is **not** forwarded to the underlying adapter; the adapter receives only the remaining options for each individual request.
- After receiving a redirect response (status code 3xx), the client reads the `Location` header and resolves it into an absolute URL. The resolved URL must take into account the scheme, host, and port of the original request when the `Location` is relative, and must preserve any query string and fragment.
- The next request sent to follow the redirect must use this resolved absolute URL.
- Any cookies sent via `Set-Cookie` in a redirect response are stored in the client’s cookie jar and automatically included in the next request to the matching domain and path (based on the resolved URL), without me having to manage them manually.
- If the number of redirects exceeds N, the client stops following and returns the last response it received (the one that caused the redirect count to exceed the limit).
- The behavior is consistent for all HTTP methods and does not depend on which adapter is configured.
- When no redirect limit is specified or is zero, no redirect following occurs. The original response is returned immediately.


As a user configuring SMTP mail transport, I need the ability to force a specific authentication method regardless of what the server advertises. This is critical when using OAuth2 (XOAUTH2), where the transport must use the token-based flow even if the server also supports PLAIN or LOGIN. Additionally, the transport must give clear, immediate errors when the chosen or advertised authentication method is unsupported, instead of delaying failure to a later, confusing step.

### Acceptance Criteria

1. **New configuration key `authType`**
   The SMTP transport configuration accepts an optional `authType` string. If omitted, behaviour must be backward-compatible with automatic detection.

2. **Explicit auth type selection**
   When `authType` is set to one of the supported schemes (`PLAIN`, `LOGIN`, `XOAUTH2`), the transport must use that exact authentication type during connection. It must not consider any `AUTH` lines from the server’s EHLO response.

3. **Invalid explicit auth type**
   If `authType` is set to a value not in `['PLAIN', 'LOGIN', 'XOAUTH2']`, the connection must immediately throw an exception with a message describing the unsupported type and listing the valid options.

4. **Automatic detection of unsupported type from server**
   When `authType` is not set (null) and the server’s EHLO response includes an `AUTH` line but none of the supported schemes (`PLAIN`, `LOGIN`, `XOAUTH2`) appear in it, the transport must throw an exception identifying the unsupported type, rather than silently skipping authentication.

5. **No AUTH advertised**
   When `authType` is not set and the EHLO response contains no `AUTH` line, the transport must proceed without authentication, consistent with existing behaviour.

6. **Access to resolved auth type**
   After connection, the resolved authentication type (whether explicitly configured or automatically detected) is available via the existing public accessor, allowing callers to inspect which method was used.


```text
Title: File::_basename() should be callable as a static utility

Cake\Filesystem\File::_basename() extracts the file’s base name from a path and optionally strips a given extension. The method does not depend on any instance state, yet it is currently declared as a non‑static `protected` method and is invoked internally via `$this->_basename()`.

Problems:
- The method cannot be called without creating (or faking) a File instance, even though it is a pure utility that mirrors PHP’s `basename()`.
- Because the internal calls use `$this->_basename()`, subclasses that override `_basename()` are not invoked when the parent’s `name()` or `safe()` methods run—the overridden logic is silently ignored.
- There is no straightforward way to unit‑test the `_basename` logic in isolation, as it is tied to an object instance and protected visibility.

Expected behavior:
- `_basename()` should be declared `protected static`.
- Calls from `name()` and `safe()` should use `static::_basename()` to respect late static binding and allow subclasses to override the method correctly.
- The method must correctly handle:
  * UTF‑8 multibyte filenames (including non‑Latin scripts).
  * Paths with a leading directory separator (e.g., `/etc/passwd`).
  * The optional suffix removal, stripping only when the suffix matches the end of the filename.
  * Edge cases such as dots, directory paths (ending with a separator), and special entries like `.` or `/`.

Impact:
Developers who subclass File can override the base‑name extraction logic and have it consistently used by `name()` and `safe()`. The method can be tested and reused in static contexts without constructing dummy File objects.

Note: The method should remain protected; there is no request to change its visibility to public.
```


**Describe the bug**
When generating shell autocompletion suggestions via `completion commands`, the output includes every registered command name, including plugin-prefixed aliases. This causes duplicate entries for the same underlying command, making autocompletion lists noisy and harder to use.

**To Reproduce**
- Register a plugin command such that `example` is available as both a plain `example` command and a plugin‑prefixed alias like `test_plugin.example` (pointing to the same command class).
- Run `bin/cake completion commands`.
- See both `example` and `test_plugin.example` in the output.

**Expected behavior**
- The plain (short) command name should be listed, but the plugin‑prefixed alias should be **excluded** when it is a true duplicate – that is, when the short form already exists and both resolve to the same command class.
- If multiple plugins provide commands that share the same base name but belong to different classes, the plugin‑prefixed names must still appear so users can distinguish them.
- A verbose mode (`-v`) should restore the complete list, including all aliases, for debugging purposes.

**Context / Impact**
This is particularly problematic when applications load several plugins and commands get registered both directly and via their plugin‑prefixed names. Shell autocompletion becomes cluttered with duplicates, reducing its effectiveness. Filtering out only those aliases that are redundant while preserving necessary disambiguation handles the common case cleanly without losing information.

Fix completion command to exclude plugin-prefixed aliases (#19094)


We need a public way to shut down a database connection from the connection object. Currently, there is no method to explicitly release the underlying connection resource, which is a problem for long-running processes, resource-limited environments, and test teardown.

## Acceptance Criteria

1. **Public `close()` method on `BaseConnection`**
   A new public method named `close` is available on the base connection class. It must be callable on any connection object.

2. **Calling `close()` releases the driver connection**
   When `close()` is invoked, the driver-specific connection handle (e.g., MySQLi or PostgreSQL resource) is properly closed using the appropriate system call.
   - For MySQLi: call `$mysqli->close()`.
   - For PostgreSQL: call `pg_close()`.
   - Other drivers: each driver provides its own mechanism that matches the underlying library’s close semantics.

3. **Connection marked as closed**
   After a successful call to `close()`, the internal connection identifier (`$this->connID`) is set to `false`. This signals to all other connection-dependent methods that the connection is no longer active.

4. **Idempotent / safe to call multiple times**
   Calling `close()` on an already-closed connection must not produce errors or warnings. It should check whether `$this->connID` is still truthy before attempting to close.

5. **Subsequent operations fail**
   After the connection is closed, any attempt to query or otherwise interact with the database must behave as if no connection exists (typically throwing a `DatabaseException` or producing an error consistent with the class’s existing error handling for a missing connection).

6. **Mock/driver stubs updated**
   The mock connection class used in tests (e.g., `MockConnection`) must also implement the internal close behavior (even if a no-op) so that the public `close()` can be called without issue.

7. **No breaking changes to existing API**
   The `close()` method does not alter the behavior of other connection methods except when called. The connection object remains usable for reconnection if `reconnect()` is supported, as the class’s own lifecycle otherwise remains unchanged.

Adding close method to Database layer. Fixes #267


Given the `Filters` class, we need a few behavioral improvements and edge-case hardening:

**1. Idempotent & chainable initialization**
- Calling `initialize($uri)` must return `$this` so that callers can chain additional configuration (e.g., `$filters->initialize('foo/bar')->addFilter(...)`).
- Subsequent calls to `initialize()` after the first must be safe no-ops: they should not re‑process globals, methods, or filters, and must preserve any filters that were added *after* the first initialization.

**2. Method‑filter matching must be exact**
- When the request method (e.g., `DELETE`) does not match *any* key in the `methods` configuration array, **no** method‑based filters should be applied. Currently only defined methods like `post` or `get` add filters; missing methods should leave the filter list empty for that grouping.

**3. Adding filters to the `globals` section**
- `addFilter($class, $alias, $when, 'globals')` must correctly place the filter into the `before` or `after` global list, and it must be included after `initialize()` processes the URI.

**4. Enabling an unknown filter must fail explicitly**
- Calling `enableFilter($name, $when)` where `$name` is not defined in the `aliases` configuration should throw a `FilterException`. This prevents silent misconfiguration when a non‑existent alias is enabled at runtime.

These changes ensure that the `Filters` pipeline behaves predictably when used with chaining, avoids accidental re‑initialization, respects the full set of HTTP methods, and fails loudly on invalid alias references.


When a `CodeIgniter\Model` is used like a Query Builder by calling `getCompiledSelect()`, `getCompiledInsert()`, or `getCompiledUpdate()`, the calls currently succeed and return compiled SQL, but that SQL bypasses the model's internal data handling and produces unexpected behaviour. This makes it easy for users to misuse the model as a raw query builder in contexts where model features (allowed fields, timestamps, validation, etc.) are needed.

**Acceptance criteria:**

- Calling `getCompiledSelect()`, `getCompiledInsert()`, or `getCompiledUpdate()` on any instance of `CodeIgniter\Model` (or a subclass) **must throw a `ModelException`**.
- The exception message must clearly identify the method that is not available, the fully qualified class name of the model, and the reason, in the form:
  *“You cannot use `{methodName}()` in `{ModelClass}`. The CodeIgniter\Model is not the Query Builder.”*
- No other query-builder methods normally available on the model (e.g. `where()`, `select()`, `insert()`, `update()`, etc.) should be affected; they remain callable exactly as before.
- The refusal applies to any model, regardless of its database connection or configuration.

fix: throws Exception when getCompiled*() in the Model called


We've identified several gaps in the HTTP layer that need attention before the next release. The core request-parsing and content-negotiation logic lacks coverage for common server setups, edge‑case headers, and fallback behavior.

Key areas to address:

- **Path detection (`IncomingRequest::detectPath` and internal helpers `parseRequestURI`, `parseQueryString`):** The existing logic doesn’t handle all real‑world configurations, particularly sub‑directory installations, servers that inject the script name differently, Nginx-style redirect URIs, and environments where `SCRIPT_NAME` is suppressed. The detection must correctly strip the script name (including nested paths) and resolve relative segments so that the returned path is consistent across Apache, Nginx, and CLI.

- **Content negotiation:**
  - The `negotiate` method should throw a `HTTPException` when an unrecognised negotiation type is passed.
  - The `Negotiate` class must reliably handle **empty** `Accept` headers (returning an appropriate default or an empty string under strict matching), **low‑quality** values (including `q=0`), and the case where no acceptable value matches and we fall back to the first supported item.
  - Ensure the negotiator always uses the request’s current headers (especially after the request headers are modified in the same test/exection scope).

- **Locale handling:** When `setLocale` is called with a value not in `$supportedLocales`, the request must silently fall back to the configured `$defaultLocale` without throwing an error or changing the locale to an unsupported value.

- **Old input retrieval:** `getOldInput` should accept dot‑notation keys to access nested `post`/`get` values stored in the session’s `_ci_old_input` array.

- **Request property checks:** `isSecure` must recognise the `HTTP_X_FORWARDED_PROTO` and `HTTP_FRONT_END_HTTPS` proxy headers in addition to the standard `HTTPS` server variable. `isAJAX` should detect the `X-Requested-With` header. `isCLI` should continue to work correctly.

- **File handling:** The `getFiles` method must properly instantiate `FileCollection` from the `$_FILES` superglobal and return `UploadedFile` objects.

- **Code‑coverage pragmas:**
  - The `intl` extension try/catch and the CLI‑only `baseURL` exception in `detectURI` should be annotated with `@codeCoverageIgnore` so they don’t skew coverage reports.

Please ensure the implementation is backed by thorough tests that exercise these scenarios, especially the various server-rewrite combinations for path detection and the negotiation fallbacks (empty, low‑quality, and non‑matching `Accept` headers).

Beef up IncomingRequest & Negotiate unit testing


When `Config\Routing::$translateURIDashes` is `true`, the `spark routes` command lists auto-routing routes with underscores in the controller class, directory, and method segments, but the actual router translates underscores to dashes. Ensure that the route listing produced by `ControllerMethodReader` applies the same dash translation so the displayed routes match live routing behavior. Do not change routes when `translateURIDashes` is `false`.

fix: `spark routes` shows incorrect routes when translateURIDashes is enabled


The `delete()` method of the Query Builder silently ignores the `$limit` parameter – the generated `DELETE` SQL never contains a `LIMIT` clause. This means a call like `$builder->delete(['country' => 'US'], 1)` deletes **all** rows matching the condition, not just one row as intended.

Additionally, PostgreSQL does not permit `LIMIT` in `DELETE` statements. The builder should detect this and prevent execution with a clear error, rather than risking an accidental full-table deletion.

### Steps to reproduce
1. Insert several rows with `country = 'US'`.
2. Call `$db->table('user')->delete(['country' => 'US'], 1)`.
3. Inspect the database afterward.

**Expected**: Only one matching row is removed, because `LIMIT 1` is applied. On PostgreSQL the call should throw a `DatabaseException` stating that `LIMIT` is not allowed on `DELETE` queries.

**Actual**: All matching rows are deleted. No exception is raised on PostgreSQL – the limit is simply dropped from the query.

### Impact
- Data loss: limit-based deletions inadvertently remove all qualifying rows.
- PostgreSQL users have no guard rail when using an unsupported clause, potentially deleting far more data than expected.

### Desired behavior
- The compiled `DELETE` query must include a `LIMIT` clause when `$limit` is passed (or `QBLimit` is set).
- For platforms that do not support `LIMIT` in `DELETE` (e.g., PostgreSQL), the builder should throw a descriptive `DatabaseException` before executing the query, preventing accidental mass deletion.


**Bug report: IncomingRequest and Request::fetchGlobal break in CLI and test environments**

**Observed behavior**

- `Request::fetchGlobal()` (used by `get()`, `post()`, `server()`, `cookie()`, etc.) calls `filter_input()` for most input types, which returns `null` or fails silently when PHP is running in CLI mode (e.g., unit tests) or under some FastCGI configurations. This makes it impossible to test request data retrieval reliably.
- Passing `null` as `$filter` to these methods does not fall back to `FILTER_DEFAULT` early enough; the null filter reaches `filter_input` and causes values to be lost (returned as `null` even when the key exists).
- `IncomingRequest::detectURI()` emits `E_NOTICE: Undefined index` when `$_SERVER['SERVER_NAME']` or `$_SERVER['HTTP_HOST']` are missing (common in CLI/test environments), because it uses `! empty()` without an `isset` guard.

**Expected behavior**

- All input retrieval methods (`get`, `post`, `server`, `cookie`, `env`, etc.) should read directly from the corresponding PHP superglobal arrays (`$_GET`, `$_POST`, `$_SERVER`, `$_COOKIE`, …) and then apply `filter_var()`, instead of relying on `filter_input()` which is environment-dependent.
- A `null` `$filter` argument should be treated identically to `FILTER_DEFAULT` immediately, before any superglobal access or filtering occurs, so that the raw value (with default sanitization) is always returned.
- Host and port detection in `IncomingRequest::detectURI()` should never trigger PHP notices. If `SERVER_NAME` or `HTTP_HOST` are missing, the host should remain unset (or be skipped) without an error.

**Impact**

Cannot write unit tests for `IncomingRequest`, `Request`, and any code depending on their input retrieval because methods either return wrong values or throw notices in test suites. This prevents CI and developer testing of HTTP request handling.

Some basic tests for IncomingRequest. Needs additional, but unsure how to test some of it just yet.


**Title:** SQLSRV `createDatabase` with `$ifNotExists = true` fails to quote identifiers correctly

### Summary
When calling `$forge->createDatabase('my db', true)` using the SQL Server driver, the generated SQL does not properly quote the database name, causing a syntax error or potential SQL injection. The same issue may affect any database name that needs identifier quoting (e.g., names with spaces, reserved words, or special characters).

### Observed Behavior
- `$forge->createDatabase('[test_db]', true)` produces a dynamic SQL statement like:
  ```sql
  DECLARE @DBName VARCHAR(255) = '[test_db]'
  DECLARE @SQL VARCHAR(max) = 'IF DB_ID( ''' + @DBName + ''' ) IS NULL CREATE DATABASE ' + @DBName
  EXEC( @SQL )
  ```
  The database name is concatenated directly without escaping, treating `[test_db]` as a literal string instead of a quoted identifier. This results in SQL Server attempting to create a database named `[test_db]` (including the brackets literally), rather than a database named `test_db` that requires quoting.

- Creating a database with a name containing spaces or special characters that need delimiters (`[]`) leads to a runtime error because the identifier is not safely escaped.

### Expected Behavior
- The database name should be escaped using SQL Server’s identifier quoting rules (`[database_name]`) before being injected into the dynamic SQL.
- The `CREATE DATABASE IF NOT EXISTS` mechanism should correctly handle any valid SQL Server database name that the driver supports, without risking SQL injection or syntax errors.
- The simple (non‑IF NOT EXISTS) path should also respect escaping, keeping the behavior consistent with the base Forge’s `createDatabase` implementation.

### Impact
- It is impossible to safely create a database with a name that requires quoting (for example, names with spaces, hyphens, or that are reserved words).
- Unescaped names in dynamic SQL open the door to SQL injection if an attacker can control the `$dbName` argument.

### Suggested Fix
Override `createDatabase()` in `CodeIgniter\Database\SQLSRV\Forge` so that:
1. It uses the driver’s `escapeIdentifier()` to quote the database name.
2. The `createDatabaseIfStr` template is adjusted to accept an already‑escaped identifier placeholder instead of relying on string concatenation with literal quotes.
3. The method returns `true`/`false` and throws `DatabaseException` on failure consistent with other drivers.


Add methods to `Publisher` for in-place content modification of already-published files. All new methods must enforce the same file restriction rules as copy/merge (`Config\Publisher::$restrictions`), rejecting disallowed files with a `PublisherException`.

- `replace(string $file, array $replaces): bool` – replace each search string (keys) with its replacement (values). Apply all replacements in order to the file’s content and write back.
- `addLineAfter(string $file, string $line, string $after): bool` – insert `$line` immediately after the first line containing `$after`. Return false if no line matches `$after`.
- `addLineBefore(string $file, string $line, string $before): bool` – insert `$line` immediately before the first line containing `$before`. Return false if no line matches `$before`.

The file is read, the operation is performed, and the modified content is written back atomically (single `file_put_contents`). The methods return true on success; any write failure is indicated by a false return.

feat: add Publisher::replace(), addLineAfter(), addLineBefore()


In `XMLFormatter::normalizeXMLTag()`, the rule that prevents element names from starting with the reserved string `xml` (case‑insensitive) is too aggressive. The current regex `/^[xml]+/iu` catches any key that begins with *any* of the characters `x`, `m`, or `l` (e.g. `Xtest` becomes `itemXtest`).

Change the logic so that **only** keys starting with the exact prefix `xml` (case‑insensitive) are treated as invalid and prefixed with `item`. The fix should use a pattern like `^(xml).*/iu` so that:

- `xml` → `itemxml`
- `XmlTest` → `itemXmlTest`
- `XML_value` → `itemXML_value`
- `Xtest`, `mydata`, `last_login` are left unchanged.


## Bug Report: `route_to()` always uses current request locale – no way to generate URLs for other locales

### Observed behavior
When a route includes the `{locale}` placeholder, calling `route_to()` always substitutes `{locale}` with the locale of the *current request* (as returned by `Services::request()->getLocale()`). There is no way to generate a URL that targets a different locale – for example, building a language switcher that needs to produce URLs for `fr` while on an `en` page.

### Expected behavior
`route_to()` should accept an optional, trailing **locale parameter**. When the route contains `{locale}` and an explicit locale string is provided, that value should be used for the placeholder instead of the current request locale. If the route does *not* contain `{locale}`, the extra parameter should be safely ignored (or handled gracefully) so existing calls don’t break.

**Safety constraints / edge cases:**
- The locale parameter must be the **last argument** after all route-segment parameters (e.g., `route_to('namedRoute', $segParam1, $segParam2, 'fr')`).
- If the provided locale string is **not** listed in `App::$supportedLocales`, it should be treated as invalid and fall back to the application’s **default locale** (not the current request locale, to avoid a broken or misleading URL).
  *This prevents typos or unsupported locale codes from creating non-functional links.*
- If no locale argument is given, behavior must remain unchanged (current request locale is used).

### Impact
Multilingual applications cannot build cross-language navigation or language switchers using `route_to()`. Workarounds (e.g., manually manipulating the generated URL) are fragile and error-prone. This missing capability directly blocks clean internationalized frontend development.

feat: you can set locale manually to route_to()


## Feature Request: Add `--host` option to `spark routes` to simulate host‑based route filtering

### Context
Route definitions can include `hostname` or `subdomain` constraints (e.g. `$routes->get('/', 'Blog::index', ['hostname' => 'blog.example.com'])`).
Currently the `routes` command ignores these constraints and lists **all** registered routes, making it impossible to inspect which routes would actually match a specific host.

### Requested Change
Add a `--host` option to `spark routes` that accepts a hostname value.
When provided, the command should:

* Simulate a request originating from that host so that the route collection’s host‑based filtering is applied.
* Display only routes that match the given host (plus routes with no host restriction).
* Show which host was used (e.g. `Host: blog.example.com`), so the output is self‑explanatory.
* Not persistently mutate the server environment after execution.

### Example Behavior
```
php spark routes --host blog.example.com
```
→ Only routes that are either unrestricted or whose `hostname`/`subdomain` matches `blog.example.com` appear in the table.
The output starts with a line like `Host: blog.example.com`.

```
php spark routes --host sub.example.com
```
→ Routes with `'subdomain' => 'sub'` are included; routes restricted to a different hostname are omitted.

### Constraints / Safety
- The host override must be temporary and not leak into subsequent route loads or other commands.
- The command must still work correctly when `--host` is **not** given (retaining the current behaviour of listing all routes).
- The implementation should reuse the existing request service and route loading mechanism, only altering the relevant server variable.

feat: add --host option to `spark routes`


**Bug: Passing `null` to HTTP Header value methods causes errors and inconsistent state**

When `null` is supplied as a header value – whether through `Header::setValue()`, `Header::appendValue()`, `Header::prependValue()`, or the `Message` header methods – the resulting internal value can become `null` or contain `null` elements. This leads to downstream failures (e.g., warnings, type errors, or malformed output) when the header is later converted to a string via `getValueLine()` or `__toString()`, because `implode` and string concatenation do not handle `null` gracefully.

**Observed problems**
1. Setting a header to `null` stores `null` directly, making `getValueLine()` produce an empty string with potential PHP warnings.
2. Appending or prepending `null` to a header that currently holds a scalar value incorrectly converts the value to an array containing `null`, and then inserts `null` as a separate entry.
3. When using `Message::setHeader()` on an existing header with an array of values, passing `null` causes `null` to be injected into the array, rather than clearing it or being handled safely.
4. `Message::appendHeader()` does not accept `null` as a value (parameter type restricts to `string`), limiting flexibility when header values originate from untrusted or optional sources.

**Expected behavior**
- **`Header::setValue(null)`** should reset the header value to an empty string (`''`), not retain `null`.
- **`Header::appendValue(null)` and `Header::prependValue(null)`** should be no-ops – the existing value(s) remain unchanged.
- **`Message::setHeader(string $name, null)`** should result in a valid, non-null header value (e.g., empty string) and must not cause errors or warnings when retrieving the header line later.
- **`Message::setHeader()` on an existing array-valued header** should uniformly treat non-array passed values (including `null`) by wrapping them in an array, but the resulting array must not contain `null` (or else `getValueLine()` should safely ignore it and return a clean string like an empty line).
- **`Message::appendHeader()`** should accept `null` (allow nullable `?string`) and treat it as a no-op.

These changes will make header value handling robust when `null` appears from parsed input, default parameters, or conditional logic, and prevent silent corruption of header lines.

Fixes handle Header value set/append/prepend null


We need to verify that the configuration system handles environment overrides correctly, especially for edge cases that aren’t currently covered. Please add tests to confirm the following behaviors:

- A property with a default non‑null value must not be overwritten when the corresponding .env entry is missing or explicitly null.
- An explicit empty string in .env is a valid override and should be applied when the property default is also an empty string.
- Boolean‑like strings `"true"` / `"false"` from the environment must be cast to actual PHP booleans.
- After loading a `.env` file, configuration should also read from `$_SERVER` (e.g., values set before or during loading) and map them to the correct properties using the documented prefix rules.

The goal is to ensure consistent, predictable overrides so that application configuration behaves reliably across different hosting setups and local development environments.


The tempdata handling in the Session class has several gaps that cause stale data and inconsistent behavior. The following changes are needed:

1. **Per‑key TTL support for `setTempdata`**
   When an associative array is passed as the first argument to `setTempdata`, the values should be treated as key → seconds‑to‑live. A plain list of keys (numeric array) should still work with the third‑parameter default TTL applied uniformly.
   Example:
   `$session->setTempdata(['foo' => 300, 'bar' => 100])` must set `foo` to expire in 300 seconds and `bar` in 100 seconds.
   `$session->setTempdata(['baz', 'qux'], null, 200)` must set both keys to expire in 200 seconds.

2. **Cleanup on `removeTempdata` and `unmarkTempdata`**
   When a tempdata key is removed (`removeTempdata`) or unmarked (`unmarkTempdata`), the corresponding session variable must be deleted (or set to null), not merely stripped of its tempdata marker. Calling `getTempdata()` afterwards must not return any value for that key.

3. **Null‑value tempdata keys must be tracked**
   Marking a key as tempdata when its session value is `null` must correctly record the expiration marker (i.e., `__ci_vars` entry). Currently such keys are silently ignored because `isset()` returns false for `null`; the check must use `array_key_exists` to allow them.

More Session tests. Fixing up tempdata logic.