Skip to content

Introduce Attribute resolver subsystem#19206

Open
josbeir wants to merge 38 commits into6.xfrom
5.next-attribute-resolver
Open

Introduce Attribute resolver subsystem#19206
josbeir wants to merge 38 commits into6.xfrom
5.next-attribute-resolver

Conversation

@josbeir
Copy link
Contributor

@josbeir josbeir commented Jan 18, 2026

Refs #19167

Add Attribute Resolver Component

This PR introduces a new Attribute Resolver component that enables efficient discovery and querying of PHP 8 attributes across the codebase.

Overview

The Attribute Resolver provides a fluent interface for scanning source files, discovering attributes, and filtering them based on various criteria. It's designed to support runtime attribute discovery for features like auto-discovery of routes, commands, and other framework components.

Key Features

  • Fluent Query Interface: Chain methods to filter attributes by class, namespace, target type, or plugin
  • Performance Optimized: PhpEngine with lazy hydration and O(1) indexes provides 145x faster warm cache lookups with OPcache
  • Plugin Aware: Automatically detects and filters attributes by plugin
  • Magic Methods: Convenient static method forwarding to default configuration
  • PhpEngine Cache: Uses brick/varexporter to generate native PHP files optimized for OPcache - ideal for any long-running cache that prioritizes fast reads over fast writes

Architecture

Component Description
AttributeResolver Main static facade following the Cache/Log pattern. Manages configurations and coordinates scanning/caching.
Scanner Handles filesystem traversal across application and all loaded plugin paths using Cake\Utility\Fs\Finder. Discovers PHP files matching configured glob patterns while respecting exclusion patterns. Automatically detects plugin boundaries and file metadata.
Parser Parses PHP files using Reflection API to extract attribute metadata. Captures attribute arguments, target information (class/method/property/parameter), line numbers, and file paths. Supports filtering to exclude specific attribute classes.
AttributeCollection Provides a fluent query interface for filtering discovered attributes. Supports filtering by attribute class, namespace, target type (class/method/property/etc), declaring class, and plugin name.
AttributeCache Manages cached attribute metadata using PhpEngine (native PHP files) or generic CacheEngine. PhpEngine uses brick/varexporter for maximum performance with lazy hydration and O(1) indexes for fast filtering. Supports optional file validation to detect stale caches when source files are modified.
ValueObjects AttributeInfo holds complete metadata about a discovered attribute. AttributeTarget describes what the attribute is attached to (class, method, property, etc). AttributeTargetType enum defines valid target types. All are readonly classes that serialize automatically with PHP.

Cache System

The resolver uses a high-performance caching system to store discovered attribute metadata for instant loading on subsequent requests. Instead of re-scanning and parsing files every time, the resolver simply reads from the cache.

PhpEngine (Recommended):

The default cache engine uses native PHP files generated with brick/varexporter for maximum performance. When combined with OPcache, this provides 145x faster warm cache lookups compared to cold scanning.

PhpEngine is ideal for any long-running cache that prioritizes fast reads over fast writes. Once cached, subsequent reads are essentially "free" as PHP's OPcache keeps the compiled bytecode in memory. This makes it perfect for configuration data, route definitions, or any metadata that changes infrequently but is read constantly.

PhpEngine features:

  • Lazy Hydration: Attribute data is stored as arrays and only converted to objects when accessed
  • O(1) Indexes: Pre-built indexes enable instant filtering by attribute name, class name, and target type
  • OPcache Optimized: PHP files are cached by OPcache for near-instant loading
  • File Validation: Optional timestamp checking to detect when source files change

Generic CacheEngine (Fallback):

When using other cache adapters (File, Redis, Memcached, etc.), with serialization enabled. While still performant, it's significantly slower than PhpEngine:

  • PhpEngine (warm OPcache): 0.12ms - 145.9x speedup
  • PhpEngine + validation: 0.28ms - 59.3x speedup
  • Generic CacheEngine: 0.86ms - 19.7x speedup

Cache Configuration:

The resolver uses a dedicated cache configuration _cake_attributes_ by default. For maximum performance, use the File adapter which enables PhpEngine:

// In config/app.php
Cache::setConfig('_cake_attributes_', [
    'className' => 'File', // Required for PhpEngine
    'path' => CACHE,
    'duration' => '+1 year',
]);

When validateFiles is enabled, the cache checks each source file's modification time against the stored fileTime. If any source file is newer, the cache is considered stale and will be regenerated.

Configuration

Configure the resolver with paths to scan and optional caching. Paths support glob/wildcard patterns and are automatically expanded against both the application root and all loaded plugin paths:

// In config/bootstrap.php or Application::bootstrap()
use Cake\Attribute\Resolver;

AttributeResolver::setConfig('default', [
    'paths' => [
        'src/**/*.php', // Scans app src/ and all plugin src/ directories
    ],
    'excludePaths' => ['vendor', 'tmp'],
    'cache' => '_cake_attributes_', // Cache configuration name (default)
    'validateFiles' => true, // Check if source files changed
    'basePath' => ROOT, // Optional: defaults to ROOT
]);

// Optional: Configure multiple collections
AttributeResolver::setConfig('routes', [
    'paths' => ['src/Controller/**/*.php'], // Scans Controllers in app and plugins
    'cache' => '_cake_attributes_',
    'excludeAttributes' => [SomeAttribute::class],
]);

// Disable caching (not recommended for production)
AttributeResolver::setConfig('nocache', [
    'paths' => ['src/**/*.php'],
    'cache' => false,
]);

Usage Examples

use Cake\Attribute\Resolver;

// Get all attributes from default configuration
$collection = AttributeResolver::collection();

// Filter by specific attribute class (uses magic method forwarding)
$routes = AttributeResolver::withAttribute(Route::class);

// Chain multiple filters
$adminRoutes = AttributeResolver::withAttribute(Route::class)
    ->withNamespace('App\Controller\Admin');

// Filter by target type
$classAttributes = AttributeResolver::withAttribute(MyAttribute::class)
    ->withTargetType(AttributeTargetType::CLASS_TYPE);

// Filter by plugin
$pluginCommands = AttributeResolver::withAttribute(ConsoleCommand::class)
    ->withPlugin('MyPlugin');

// Use named configurations
$commands = AttributeResolver::collection('commands')
    ->withAttribute(ConsoleCommand::class);

// Clear cache
AttributeResolver::clear('default');

CLI Commands

Three console commands are provided for managing and inspecting attributes:

Cache Warming:

# Warm up the attribute cache
bin/cake attributes warm

# Output:
# Warming attribute cache...
# Cached 28 attributes in 0.15s

# Use a specific configuration
bin/cake attributes warm --config custom

# Clear cache using built-in cache command
bin/cake cache clear ...

List Attributes:

# List all discovered attributes
bin/cake attributes list

# Filter by attribute type, class, or namespace
bin/cake attributes list --type method --class UsersController

# Output (table format):
# Found 5 attributes:
# +--------------------------------------+------------------------------------------+--------+--------+-----------------+
# | Attribute                            | Class                                    | Plugin | Type   | Target          |
# +--------------------------------------+------------------------------------------+--------+--------+-----------------+
# | App\Attribute\Route                  | ...ibute\Controller\UsersController      | -      | class  | UsersController |
# | App\Attribute\Get                    | ...ibute\Controller\UsersController      | -      | method | index           |
# | App\Attribute\Post                   | ...ibute\Controller\UsersController      | -      | method | add             |
# +--------------------------------------+------------------------------------------+--------+--------+-----------------+

# Show full class names without truncation
bin/cake attributes list --verbose

Inspect Details:

# Inspect specific attributes with full metadata
bin/cake attributes inspect Route

# Output:
# Found 3 attribute(s):
#
# 1. Route
#    Attribute Class: App\Attribute\Route
#    Target Class: App\Controller\UsersController
#    Plugin: -
#    Target: index (method)
#    File: src/Controller/UsersController.php:42
#    Arguments:
#      - path: /users/index
#      - methods: ["GET"]

# Inspect all attributes on a class
bin/cake attributes inspect --class ArticlesController

Performance

Benchmark Configuration:

  • 130 test classes across Controllers, Entities, Services, and Commands
  • ~1,195 total attributes discovered
  • All benchmarks run twice to ensure warm OPcache (CLI limitation)

Cache Performance:

Engine Time Speedup Notes
Cold Start (no cache) 20.10ms 1.0x Full filesystem scan and reflection parsing
PhpEngine (warm OPcache) 0.14ms 145.3x Native PHP files cached in OPcache memory
PhpEngine + validateFiles 0.34ms 58.3x With source file timestamp validation
FileEngine (serialized) 1.02ms 19.7x Standard PHP serialization without OPcache benefit

Key Findings:

  1. PhpEngine with OPcache is 145x faster than cold scanning - subsequent reads are essentially free as bytecode stays in memory
  2. File validation adds only 0.20ms overhead while ensuring cache freshness
  3. PhpEngine outperforms FileEngine by 7.3x due to OPcache optimization and lazy hydration
  4. O(1) indexes enable instant filtering - filtering 356 attributes from 1,195 total takes ~0.001ms

OPcache CLI Note: In CLI mode, OPcache only caches files from previous PHP processes. Run your application/commands twice after cache generation to see true warm OPcache performance.

Notes

Currently uses Plugin::getCollection() to get active plugin paths which works but is not reliable when sharing the cache with CLI as the plugin list loaded in CLI is not the same as the one from HTTP. Resolved in #19208

@josbeir josbeir added this to the 5.4.0 milestone Jan 18, 2026
@josbeir josbeir self-assigned this Jan 18, 2026
@josbeir josbeir added the needs squashing The pull request should be squashed before merging label Jan 18, 2026
@josbeir josbeir force-pushed the 5.next-attribute-resolver branch 5 times, most recently from fd5a3f9 to e68d789 Compare January 20, 2026 15:13
@josbeir josbeir changed the base branch from 5.next to 6.x January 20, 2026 15:27
@josbeir josbeir force-pushed the 5.next-attribute-resolver branch from e68d789 to a555f74 Compare January 20, 2026 15:37
@josbeir josbeir modified the milestones: 5.4.0, 6.0 Jan 20, 2026
@josbeir josbeir force-pushed the 5.next-attribute-resolver branch 2 times, most recently from 1fa6b3a to 9f8b140 Compare January 20, 2026 20:36
@josbeir josbeir force-pushed the 5.next-attribute-resolver branch from 9f8b140 to 8bcf96e Compare January 20, 2026 20:40
@josbeir josbeir changed the title WIP: Attribute resolver Introduce Attribute resolver subsystem Jan 22, 2026
@LordSimal
Copy link
Contributor

I think this is a great foundation for any future attribute based system we want to add to the framework 👍🏻

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 50 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@LordSimal
Copy link
Contributor

If no further adjustments need to be made here this can be merged so our marathon runner @josbeir can continue his sprint towards attribute based routing 🏃🏻

@jamisonbryant
Copy link
Contributor

nitpick: I do not love the namespace Cake\Utility\Fs\Finder - how do we feel about e.g. Cake\Utility\Files\Finder (I know ns are normally singular, however I'm kind of fudging that rule here and pretending it reads as "Files(ystem)"

kudos: All of the new ValueObjects classes would be extremely useful in our app, so we should make sure these are accessible to userland code and not e.g. marked @internal or final.

thought: We should ensure we document the File/PhpEngine recommendation in cakephp/app, as it clearly makes a huge difference in the speed of this feature and may help improve adoption.

nitpick: Cake\Attribute\Resolver\Resolver is a bit of a generic class name. We use the word "Resolver" to describe a LOT of classes in our apps. I know I could just use ... as but wouldn't it be better to be a bit more verbose e.g. AttributeResolver?

thought: How do we feel about adding format flags e.g. --json to commands like bin/cake attributes inspect Route? I have been trying to do this more lately because it makes outputs a bit more LLM-friendly. If we add --xml too then that's even better.

question: Is the Plugin::getCollection() unreliability a core issue we should address or is it expected behavior?

@josbeir
Copy link
Contributor Author

josbeir commented Feb 3, 2026

nitpick: I do not love the namespace Cake\Utility\Fs\Finder - how do we feel about e.g. Cake\Utility\Files\Finder (I know ns are normally singular, however I'm kind of fudging that rule here and pretending it reads as "Files(ystem)"

This was actually already merged in #19171 as part of one of things needed for this PR

kudos: All of the new ValueObjects classes would be extremely useful in our app, so we should make sure these are accessible to userland code and not e.g. marked @internal or final.

These are indeed ment to be used in userland to get information about attributes, not only core attributes but also custom attributes an app would implement.

thought: We should ensure we document the File/PhpEngine recommendation in cakephp/app, as it clearly makes a huge difference in the speed of this feature and may help improve adoption.

This is exactly the reason why i made it as a Cache engine, can be easily adopted for future caching needs.

nitpick: Cake\Attribute\Resolver\Resolver is a bit of a generic class name. We use the word "Resolver" to describe a LOT of classes in our apps. I know I could just use ... as but wouldn't it be better to be a bit more verbose e.g. AttributeResolver?

That's a though i also had. Its in Attribute/Resolver so the namespace could clarify things, Attribute/AttributeResolver also works for me.

thought: How do we feel about adding format flags e.g. --json to commands like bin/cake attributes inspect Route? I have been trying to do this more lately because it makes outputs a bit more LLM-friendly. If we add --xml too then that's even better.

Great idea!

question: Is the Plugin::getCollection() unreliability a core issue we should address or is it expected behavior?

This was addressed in #19208 and got merged so this should be resolved :-)

@jamisonbryant jamisonbryant self-requested a review February 3, 2026 15:33
@josbeir
Copy link
Contributor Author

josbeir commented Feb 7, 2026

@jamisonbryant --format json option added, i don't really see the added benefit of adding XML too.

@josbeir
Copy link
Contributor Author

josbeir commented Feb 13, 2026

Before we merge: i still have a few open questions

  • Are we sure about the new Attribute namespace at root level? The main reason i ask is that i'm not sure this namespace will contain anything other than the resolver in the future as specific attributes will probably be part of the package they belong to.
  • Currently we have the Resolver.php static facade class that lives in src/Attribute, should we rename this to a more explicit AttributeResolver ?

@jamisonbryant
Copy link
Contributor

jamisonbryant commented Feb 13, 2026

Are we sure about the new Attribute namespace at root level?

For my attributes, I use the namespace App\Support...in my mind, Attributes should rarely if ever contain business logic, and in that vein they fit my definition of a "support class" (TF2 Medic says hi). I would be in favor of Cake\Support\Attribute or even Cake\Meta\Attribute. Not sure if others will agree with me or not :D

should we rename this to a more explicit AttributeResolver

As someone who uses 'Resolver' a lot in their Cake codebase: yes, please.

@ADmad
Copy link
Member

ADmad commented Feb 13, 2026

The main reason i ask is that i'm not sure this namespace will contain anything other than the resolver in the future as specific attributes will probably be part of the package they belong to.

But the attribute resolver itself has multiple supporting classes, so having it under it's own namespace is fine. We could maybe in the future move it under Core itself.

Currently we have the Resolver.php static facade class that lives in src/Attribute, should we rename this to a more explicit AttributeResolver ?

Using AttributeResolver seems better even if "Attribute" gets repeated in the FQCN

@josbeir
Copy link
Contributor Author

josbeir commented Feb 13, 2026

✔️ Attribute => AttributeResolver rename

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs squashing The pull request should be squashed before merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants