diff --git a/.vitepress/toc_en.json b/.vitepress/toc_en.json
index fdb5e2f947..a68016425f 100644
--- a/.vitepress/toc_en.json
+++ b/.vitepress/toc_en.json
@@ -193,6 +193,10 @@
"items": [
{ "text": "App", "link": "/core-libraries/app" },
{ "text": "Plugin", "link": "/core-libraries/plugin" },
+ {
+ "text": "Attribute Resolver",
+ "link": "/core-libraries/attribute-resolver"
+ },
{
"text": "Registry Objects",
"link": "/core-libraries/registry-objects"
diff --git a/docs/en/core-libraries/attribute-resolver.md b/docs/en/core-libraries/attribute-resolver.md
new file mode 100644
index 0000000000..e986f3cc05
--- /dev/null
+++ b/docs/en/core-libraries/attribute-resolver.md
@@ -0,0 +1,604 @@
+---
+title: "Attribute Resolver"
+description: "Discover and query PHP attributes across your application with CakePHP's AttributeResolver: configure scanning paths, filter by attribute type or class, cache results, and use console commands for inspection."
+---
+
+# Attribute Resolver
+
+`class` Cake\\AttributeResolver\\**AttributeResolver**
+
+The `AttributeResolver` is a static class that scans PHP files in configured
+paths, discovers all PHP attributes applied to classes, methods, properties,
+parameters, and constants, and makes them available for efficient querying.
+
+Attribute routing uses `AttributeResolver` under the hood, but it is also a
+general-purpose tool. You can use it to build your own attribute-driven systems,
+such as event listener discovery, dependency injection metadata, or custom
+annotation processors.
+
+::: info Added in version 6.0.0
+The Attribute Resolver was added.
+:::
+
+## Configuration
+
+Resolver configurations can be defined in **config/app.php** under an
+`'AttributeResolver'` key:
+
+```php
+// config/app.php
+return [
+ // ...other config...
+ 'AttributeResolver' => [
+ 'default' => [
+ 'paths' => [
+ 'Controller/*Controller.php',
+ 'Controller/**/*Controller.php',
+ ],
+ 'basePath' => APP,
+ 'cache' => '_cake_attributes_',
+ ],
+ ],
+];
+```
+
+Then load the configuration in your application bootstrap:
+
+```php
+// In config/bootstrap.php or Application::bootstrap()
+use Cake\AttributeResolver\AttributeResolver;
+use Cake\Core\Configure;
+
+AttributeResolver::setConfig(Configure::read('AttributeResolver'));
+```
+
+Alternatively, you can define configurations inline in bootstrap without using
+**config/app.php**:
+
+```php
+use Cake\AttributeResolver\AttributeResolver;
+
+AttributeResolver::setConfig('default', [
+ 'paths' => [
+ 'Controller/*Controller.php',
+ 'Controller/**/*Controller.php',
+ ],
+ 'basePath' => APP,
+ 'cache' => '_cake_attributes_',
+]);
+```
+
+### Configuration Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `paths` | `string[]` | `[]` | Glob patterns of PHP files to scan, relative to `basePath`. |
+| `basePath` | `string` | `APP` | Absolute directory path that `paths` patterns are relative to. |
+| `excludePaths` | `string[]` | `[]` | Glob patterns of paths to skip during scanning. |
+| `excludeAttributes` | `string[]` | `[]` | Fully-qualified attribute class names to ignore. |
+| `cache` | `string\|false` | `false` | Cache configuration name to use. Set to `false` to disable caching. |
+| `validateFiles` | `bool` | `false` | When `true`, re-scan if any scanned file has been modified since the cache was written. |
+
+#### `paths`
+
+Patterns are relative to `basePath` and support standard PHP glob syntax. Use
+`**` for recursive directory matching:
+
+```php
+'paths' => [
+ 'Controller/*Controller.php', // Top-level controllers
+ 'Controller/**/*Controller.php', // Nested (e.g. prefixes)
+ 'Model/Table/*Table.php', // Table classes
+],
+```
+
+#### `excludePaths`
+
+Exclude specific subdirectories or files from scanning. Patterns follow the same
+rules as `paths`:
+
+```php
+'excludePaths' => [
+ 'Controller/Admin/*Controller.php',
+],
+```
+
+#### `excludeAttributes`
+
+Prevent specific attribute classes from being indexed. Useful when third-party
+attributes you don't own would otherwise pollute your queries:
+
+```php
+'excludeAttributes' => [
+ SomeThirdParty\Attribute\IgnoreThis::class,
+],
+```
+
+#### `cache` and `validateFiles`
+
+Caching is strongly recommended for production. The resolver stores its results
+using the CakePHP cache layer. The recommended engine is `PhpEngine`, which
+stores data as executable PHP files that OPcache compiles into shared memory for
+near-zero-cost reads. See
+[PhpEngine Options](../core-libraries/caching#caching-phpengine) for
+configuration details.
+
+Configure the `_cake_attributes_` cache engine in **config/app.php**:
+
+```php
+// config/app.php
+use Cake\Cache\Engine\PhpEngine;
+
+'Cache' => [
+ '_cake_attributes_' => [
+ 'className' => PhpEngine::class,
+ 'prefix' => 'myapp_attributes_',
+ 'path' => CACHE . 'attributes' . DS,
+ 'duration' => 0, // 0 = indefinite; clear at deploy time
+ ],
+],
+```
+
+Set `validateFiles` to `true` during development to automatically detect file
+changes without manually clearing the cache. This adds file `mtime` checks on
+each request and should be `false` in production:
+
+```php
+use Cake\Core\Configure;
+
+AttributeResolver::setConfig('default', [
+ 'paths' => ['Controller/**/*Controller.php'],
+ 'basePath' => APP,
+ 'cache' => '_cake_attributes_',
+ 'validateFiles' => Configure::read('debug'),
+]);
+```
+
+## Querying Attributes
+
+Call `AttributeResolver::collection()` to get a filterable
+`AttributeCollection`. On first call the resolver scans the configured paths and
+builds the index; subsequent calls return the cached result.
+
+```php
+use Cake\AttributeResolver\AttributeResolver;
+use App\Attribute\MyAttribute;
+
+$collection = AttributeResolver::collection();
+```
+
+You can also call filter methods directly on `AttributeResolver` without
+`collection()`. The call is forwarded to the `'default'` configuration:
+
+```php
+// Equivalent to AttributeResolver::collection()->withAttribute(MyAttribute::class)
+$results = AttributeResolver::withAttribute(MyAttribute::class)->toArray();
+```
+
+### Using a Named Configuration
+
+Pass the configuration name to `collection()` to query a non-default resolver:
+
+```php
+$collection = AttributeResolver::collection('controllers');
+```
+
+## Filtering Collections
+
+`class` Cake\\AttributeResolver\\**AttributeCollection**
+
+`AttributeCollection` provides a fluent, chainable API for narrowing results.
+Each filter method returns a new collection; the original is unchanged:
+
+```php
+use Cake\AttributeResolver\AttributeResolver;
+use Cake\AttributeResolver\Enum\AttributeTargetType;
+use App\Attribute\MyAttribute;
+
+$results = AttributeResolver::collection()
+ ->withAttribute(MyAttribute::class)
+ ->withTargetType(AttributeTargetType::METHOD)
+ ->toArray();
+```
+
+### Filter Methods
+
+#### `withAttribute(string|array $names)`
+
+Restrict results to attributes whose class name exactly matches one of the given
+names:
+
+```php
+$collection->withAttribute(MyAttribute::class);
+$collection->withAttribute([RouteAttribute::class, GetAttribute::class]);
+```
+
+#### `withAttributeContains(string $search)`
+
+Filter by a partial attribute class name match:
+
+```php
+// Matches any attribute whose class name contains 'Route'
+$collection->withAttributeContains('Route');
+```
+
+#### `withNamespace(string $pattern)`
+
+Filter by a namespace glob pattern. Use `*` as a wildcard:
+
+```php
+// All attributes in the App\Routing namespace
+$collection->withNamespace('App\Routing\*');
+```
+
+#### `withClassName(string|array $names)`
+
+Restrict to attributes found on a specific class or set of classes:
+
+```php
+$collection->withClassName(ArticlesController::class);
+```
+
+#### `withClassNameContains(string $search)`
+
+Filter by a partial class name match:
+
+```php
+$collection->withClassNameContains('Controller');
+```
+
+#### `withTargetType(AttributeTargetType|array $types)`
+
+Filter by what PHP construct the attribute is applied to:
+
+```php
+use Cake\AttributeResolver\Enum\AttributeTargetType;
+
+$collection->withTargetType(AttributeTargetType::METHOD);
+$collection->withTargetType([AttributeTargetType::CLASS_, AttributeTargetType::METHOD]);
+```
+
+Available target types:
+
+| Value | Constant | Meaning |
+|-------|----------|---------|
+| `'class'` | `AttributeTargetType::CLASS_` | Applied to a class declaration |
+| `'method'` | `AttributeTargetType::METHOD` | Applied to a method |
+| `'property'` | `AttributeTargetType::PROPERTY` | Applied to a property |
+| `'parameter'` | `AttributeTargetType::PARAMETER` | Applied to a function/method parameter |
+| `'constant'` | `AttributeTargetType::CONSTANT` | Applied to a class constant |
+
+#### `withPlugin(?string $pluginName)`
+
+Restrict results to attributes discovered in a specific plugin. Pass `null` to
+return only application-level attributes:
+
+```php
+$collection->withPlugin('MyPlugin');
+$collection->withPlugin(null); // App-level only
+```
+
+#### `filter(Closure $callback)`
+
+Apply an arbitrary filter when none of the built-in methods fit. The closure
+receives an `AttributeInfo` instance and must return `bool`:
+
+```php
+use Cake\AttributeResolver\ValueObject\AttributeInfo;
+
+$collection->filter(fn(AttributeInfo $info): bool =>
+ count($info->arguments) > 0
+);
+```
+
+### Materializing Results
+
+| Method | Return type | Description |
+|--------|-------------|-------------|
+| `toArray()` | `AttributeInfo[]` | All matching `AttributeInfo` objects |
+| `toList()` | `AttributeInfo[]` | Alias for `toArray()` |
+| `first()` | `?AttributeInfo` | First matching result, or `null` |
+| `count()` | `int` | Number of matching results |
+
+`AttributeCollection` also implements `IteratorAggregate`, so you can use it
+directly in a `foreach`:
+
+```php
+foreach (AttributeResolver::collection()->withAttribute(MyAttribute::class) as $info) {
+ // $info is an AttributeInfo instance
+}
+```
+
+## The AttributeInfo Value Object
+
+`class` Cake\\AttributeResolver\\ValueObject\\**AttributeInfo**
+
+Each result from a collection is an `AttributeInfo` readonly value object with
+the following properties:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `className` | `string` | FQCN of the class where the attribute was found |
+| `attributeName` | `string` | FQCN of the attribute class |
+| `arguments` | `array` | Arguments passed to the attribute constructor |
+| `filePath` | `string` | Absolute path to the PHP file |
+| `lineNumber` | `int` | Line number of the attribute declaration |
+| `target` | `AttributeTarget` | Metadata about what the attribute is applied to |
+| `fileTime` | `int` | Unix timestamp of the file's last modification |
+| `pluginName` | `?string` | Plugin name, or `null` for application code |
+
+### `getInstance(?string $expectedClass = null): object`
+
+Instantiates the actual attribute object using its stored arguments. Optionally
+pass the expected class name to validate the type:
+
+```php
+$info = AttributeResolver::collection()
+ ->withAttribute(MyAttribute::class)
+ ->first();
+
+if ($info !== null) {
+ $attr = $info->getInstance(MyAttribute::class);
+ // $attr is an instance of MyAttribute
+}
+```
+
+### The AttributeTarget Value Object
+
+`class` Cake\\AttributeResolver\\ValueObject\\**AttributeTarget**
+
+The `target` property of `AttributeInfo` describes the PHP construct the
+attribute was applied to:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `type` | `AttributeTargetType` | `class`, `method`, `property`, `parameter`, or `constant` |
+| `name` | `string` | Name of the method, property, parameter, or constant. Empty for class targets. |
+| `declaringClass` | `string` | FQCN of the class that declares this target |
+| `isDeclaringClassAbstract` | `bool` | Whether the declaring class is abstract |
+| `declaringClassType` | `DeclaringClassType` | `class`, `interface`, `trait`, or `enum` |
+| `methodVisibility` | `?MethodVisibility` | Visibility for method targets; `null` otherwise |
+
+## Multiple Configurations
+
+You can define more than one resolver configuration to scan different sets of
+paths independently:
+
+```php
+// Bootstrap
+AttributeResolver::setConfig('controllers', [
+ 'paths' => ['Controller/**/*Controller.php'],
+ 'basePath' => APP,
+ 'cache' => '_cake_attributes_',
+]);
+
+AttributeResolver::setConfig('models', [
+ 'paths' => ['Model/Table/*Table.php'],
+ 'basePath' => APP,
+ 'cache' => '_cake_attributes_',
+]);
+
+// Query each independently
+$controllerAttrs = AttributeResolver::collection('controllers');
+$modelAttrs = AttributeResolver::collection('models');
+```
+
+Attribute routing uses the `'default'` configuration by default. You can pass a
+different name to `connectAttributes()` if you need separate scanning settings
+for routing:
+
+```php
+$routes->connectAttributes('routing');
+```
+
+## Caching and Cache Warming
+
+Results are cached after the first scan and reused on subsequent requests.
+Use `warm()` to proactively populate the cache, or `clear()` to invalidate it:
+
+```php
+// Force a fresh scan and write to cache
+AttributeResolver::warm('default');
+
+// Invalidate in-memory and persistent cache
+AttributeResolver::clear('default');
+```
+
+### Warming the Cache via Console
+
+Use the `attributes warm` command to pre-populate the cache during deployment,
+so the first web request doesn't bear the scanning cost:
+
+```bash
+# Warm the default configuration
+bin/cake attributes warm
+
+# Warm a named configuration
+bin/cake attributes warm --config controllers
+```
+
+If caching is disabled (`'cache' => false`), the command exits with a warning.
+
+### Automating Cache Warming
+
+Manually running `bin/cake attributes warm` is fine during initial setup, but
+easy to forget. When the cache is stale — for example after baking a new
+controller or installing a plugin that adds attributes — the resolver falls back
+to a full scan on the next request, adding latency at the worst possible moment.
+Automating the warm step removes that risk and keeps cache hits consistent across
+all environments.
+
+#### After Composer install or update
+
+Add `bin/cake attributes warm` to your **composer.json** scripts so it runs
+automatically whenever a package (including plugins) is installed or updated:
+
+```json
+{
+ "scripts": {
+ "post-install-cmd": [
+ "@php bin/cake attributes warm"
+ ],
+ "post-update-cmd": [
+ "@php bin/cake attributes warm"
+ ]
+ }
+}
+```
+
+This ensures the attribute cache is refreshed whenever `composer install` or
+`composer update` completes — covering plugin installation, removal, and version
+changes.
+
+#### After bake commands
+
+CakePHP dispatches a `Command.afterExecute` event after every command finishes.
+You can listen to this event in your **src/Application.php** to automatically
+warm the cache after any `bake` command:
+
+```php
+// src/Application.php
+use Cake\AttributeResolver\AttributeResolver;
+use Cake\Console\BaseCommand;
+use Cake\Event\EventInterface;
+use Cake\Event\EventManagerInterface;
+
+public function events(EventManagerInterface $eventManager): EventManagerInterface
+{
+ $eventManager->on('Command.afterExecute', function (EventInterface $event): void {
+ $command = $event->getSubject();
+ if (!$command instanceof BaseCommand) {
+ return;
+ }
+ // Re-warm after any bake command
+ if (str_starts_with($command->getName(), 'bake ')) {
+ AttributeResolver::warm('default');
+ }
+ });
+
+ return $eventManager;
+}
+```
+
+## Console Commands
+
+The attribute resolver ships three console commands for inspecting discovered
+attributes from the command line.
+
+### `attributes list`
+
+Displays all discovered attributes in a table. Supports several filters and
+output formats:
+
+```bash
+bin/cake attributes list
+
+# Filter by attribute class (partial match)
+bin/cake attributes list --attribute Route
+
+# Filter by target class (partial match)
+bin/cake attributes list --class ArticlesController
+
+# Filter by namespace pattern (wildcard supported)
+bin/cake attributes list --namespace "App\Controller\*"
+
+# Filter by target type: class, method, property, parameter, constant
+bin/cake attributes list --type method
+
+# Filter by plugin
+bin/cake attributes list --plugin MyPlugin
+
+# Output as JSON
+bin/cake attributes list --format json
+
+# Show full class names without truncation
+bin/cake attributes list --verbose
+```
+
+### `attributes inspect`
+
+Shows detailed information for a single attribute, including its arguments and
+the source location where it was declared:
+
+```bash
+bin/cake attributes inspect
+
+# Filter by attribute class name (partial match)
+bin/cake attributes inspect Route
+
+# Filter by class name
+bin/cake attributes inspect --class ArticlesController
+
+# Use a named configuration
+bin/cake attributes inspect --config controllers
+```
+
+### `attributes warm`
+
+Warms the attribute cache:
+
+```bash
+bin/cake attributes warm
+
+# Use a named configuration
+bin/cake attributes warm --config controllers
+```
+
+## Plugin Scanning
+
+The resolver automatically includes all loaded plugins when scanning. Plugin
+files are discovered using each plugin's registered paths. Discovery respects
+the following rules:
+
+- Plugins not registered with the Plugin collection are skipped.
+- Debug-only plugins are excluded when `debug` mode is off.
+- CLI-only plugins are still scanned in web contexts so the cache stays
+ consistent between web and CLI requests.
+
+To restrict scanning to a single plugin's attributes, use `withPlugin()`:
+
+```php
+$pluginAttrs = AttributeResolver::collection()->withPlugin('MyPlugin');
+```
+
+## Building Custom Integrations
+
+`AttributeResolver` is not limited to routing. Any feature that needs to
+discover PHP attributes at runtime can use it. The example below finds all
+methods tagged with a hypothetical `#[ListensTo]` attribute and registers them
+as event listeners:
+
+```php
+namespace App\Event;
+
+use App\Attribute\ListensTo;
+use Cake\AttributeResolver\AttributeResolver;
+use Cake\AttributeResolver\Enum\AttributeTargetType;
+use Cake\Event\EventManager;
+
+class AttributeListenerLoader
+{
+ public static function load(EventManager $manager): void
+ {
+ $collection = AttributeResolver::collection()
+ ->withAttribute(ListensTo::class)
+ ->withTargetType(AttributeTargetType::METHOD);
+
+ foreach ($collection as $info) {
+ $attribute = $info->getInstance(ListensTo::class);
+ $listener = new ($info->className)();
+ $manager->on($attribute->eventName, [$listener, $info->target->name]);
+ }
+ }
+}
+```
+
+Configure the paths to include whatever classes your integration scans:
+
+```php
+AttributeResolver::setConfig('listeners', [
+ 'paths' => ['Listener/**/*.php'],
+ 'basePath' => APP,
+ 'cache' => '_cake_attributes_',
+]);
+```
diff --git a/docs/en/core-libraries/caching.md b/docs/en/core-libraries/caching.md
index 904a99c17a..2e5c121b65 100644
--- a/docs/en/core-libraries/caching.md
+++ b/docs/en/core-libraries/caching.md
@@ -32,6 +32,12 @@ build your own backend. The built-in caching engines are:
- `Apcu` APCu cache uses the PHP [APCu](https://php.net/apcu) extension.
This extension uses shared memory on the webserver to store objects.
This makes it very fast, and able to provide atomic read/write features.
+- `Php` Stores cache data as executable PHP files using
+ [brick/varexporter](https://github.com/brick/varexporter). Because PHP's
+ OPcache compiles these files into shared memory, reads are extremely fast
+ with no deserialization overhead. Best suited for infrequently written,
+ static data such as attribute metadata, route caches, and schema
+ information. Does not support atomic increment or decrement.
- `Array` Stores all data in an array. This engine does not provide
persistent storage and is intended for use in application test suites.
- `Null` The null engine doesn't actually store anything and fails all read
@@ -163,6 +169,50 @@ FileEngine uses the following engine specific options:
- `mask` The mask used for created files
- `path` Path to where cachefiles should be saved. Defaults to system's temp dir.
+
+
+### PhpEngine Options
+
+::: info Added in version 6.0.0
+:::
+
+`PhpEngine` serializes cache values as executable PHP files using
+[brick/varexporter](https://github.com/brick/varexporter) and relies on
+PHP's OPcache to compile them into shared memory. After the first read,
+OPcache serves subsequent reads directly from memory without touching the
+disk or deserializing any data.
+
+PhpEngine uses the following engine-specific options:
+
+- `path` Path to where cache files should be saved. Defaults to
+ `sys_get_temp_dir()/cake_php_cache/`.
+- `mask` The mask used for created files. Defaults to `0664`.
+- `dirMask` The mask used for created directories. Defaults to `0777`.
+
+A typical configuration for static, deploy-time caches:
+
+```php
+use Cake\Cache\Engine\PhpEngine;
+
+// config/app.php
+'Cache' => [
+ '_cake_attributes_' => [
+ 'className' => PhpEngine::class,
+ 'prefix' => 'myapp_attributes_',
+ 'path' => CACHE . 'attributes' . DS,
+ 'duration' => 0, // 0 = never expires; clear at deploy time
+ ],
+],
+```
+
+OPcache invalidation is handled automatically: `opcache_invalidate()` is
+called whenever a cache file is written or deleted, so stale compiled
+bytecode is never served.
+
+> [!NOTE]
+> PhpEngine does not support `increment()` or `decrement()`. Use APCu,
+> Redis, or Memcached for counter-based caching.
+
### RedisEngine Options
@@ -537,8 +587,8 @@ Cache::increment('initial_count');
```
> [!NOTE]
-> Incrementing and decrementing do not work with FileEngine. You should use
-> APCu, Redis or Memcached instead.
+> Incrementing and decrementing do not work with FileEngine or PhpEngine.
+> You should use APCu, Redis, or Memcached instead.
## Using Cache to Store Common Query Results
diff --git a/docs/en/development/attribute-routing.md b/docs/en/development/attribute-routing.md
index 8ff5fe3a47..99950c4655 100644
--- a/docs/en/development/attribute-routing.md
+++ b/docs/en/development/attribute-routing.md
@@ -21,7 +21,11 @@ Attribute routing was added.
## Getting Started
Attribute routing relies on the `AttributeResolver` to discover attributes on
-your controller classes. Enable it in your **config/routes.php**:
+your controller classes. See the [Attribute Resolver](../core-libraries/attribute-resolver)
+documentation for advanced configuration, caching, and querying attributes from
+your own code.
+
+Enable it in your **config/routes.php**:
```php
// config/routes.php