josbeir / cakephp-attribute-registry
CakePHP Attribute Registry Plugin - A plugin to manage and utilize PHP 8 attributes within CakePHP applications.
Installs: 30
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 0
Forks: 0
Open Issues: 0
Type:cakephp-plugin
pkg:composer/josbeir/cakephp-attribute-registry
Requires
- php: >=8.2
- cakephp/cakephp: ^5.2
Requires (Dev)
- cakephp/cakephp-codesniffer: ^5.2
- cakephp/debug_kit: ^5.2
- cakephp/plugin-installer: ^2.0.1
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.1.3 || ^12.0
- rector/rector: ^2.1
This package is auto-updated.
Last update: 2025-12-24 18:54:35 UTC
README
CakePHP Attribute Registry Plugin
A powerful CakePHP plugin for discovering, caching, and querying PHP 8 attributes across your application and plugins.
Table of Contents
- Overview
- Requirements
- Installation
- Configuration
- Usage
- Console Commands
- DebugKit Panel
- Testing
- Contributing
- License
- Credits
Overview
The CakePHP Attribute Registry Plugin provides a centralized system for discovering and managing PHP 8 attributes throughout your CakePHP application. It scans your codebase for attributes on classes, methods, properties, parameters, and constants, then caches the results for fast retrieval.
Key Features:
- 🔍 Automatic Discovery - Scans PHP files for attributes using configurable glob patterns
- 💾 Built-in Caching - Caches discovered attributes
- 🔎 Flexible Querying - Find attributes by name, class, or target type
- 🔌 Plugin Support - Automatically scans all loaded CakePHP plugins
- 🖥️ CLI Tools - Console commands for discovery, listing, and inspection
- 🏗️ Service-Oriented - Clean architecture with dependency injection via CakePHP's container
- 🐛 DebugKit Panel - Visual panel for browsing discovered attributes during development
Supported Attribute Targets:
| PHP Target | Constant | Supported | Example |
|---|---|---|---|
| Class | Attribute::TARGET_CLASS |
✅ | #[Route] class MyController |
| Method | Attribute::TARGET_METHOD |
✅ | #[Get] public function index() |
| Property | Attribute::TARGET_PROPERTY |
✅ | #[Column] public int $id |
| Parameter | Attribute::TARGET_PARAMETER |
✅ | function show(#[FromPath] int $id) |
| Class Constant | Attribute::TARGET_CLASS_CONSTANT |
✅ | #[Deprecated] const STATUS = 1 |
| Function | Attribute::TARGET_FUNCTION |
❌ | Standalone functions not supported |
Note
Standalone functions (TARGET_FUNCTION) are not supported because the plugin uses class-based reflection. In typical CakePHP applications, attributes are primarily used on classes and their members.
Requirements
- PHP 8.2 or higher
- CakePHP 5.2 or higher
Installation
Install the plugin using Composer:
composer require josbeir/cakephp-attribute-registry
Load the plugin in your src/Application.php:
bin/cake plugin load AttributeRegistry
Configuration
The plugin works out of the box with sensible defaults.
Configuration Options
// config/app_attribute_registry.php return [ 'AttributeRegistry' => [ 'cache' => [ // Enable/disable caching (default: true) // When disabled, attributes are re-discovered on every request 'enabled' => true, // Cache directory (defaults to CACHE . 'attribute_registry' . DS) 'path' => CACHE . 'attribute_registry' . DS, // Validate file hashes on cache retrieval (default: false) // When enabled, cached entries are validated against file content changes // Useful in development to auto-rebuild cache when files change // Example: Configure::read('debug') enables validation only in debug mode 'validateFiles' => false, ], 'scanner' => [ // Glob patterns for files to scan (relative to base paths) 'paths' => [ 'src/**/*.php', ], // Glob patterns for paths to exclude 'exclude_paths' => [ 'vendor/**', 'tmp/**', 'logs/**', 'tests/**', 'webroot/**', ], // Attribute classes to exclude from discovery 'exclude_attributes' => [ 'Override', // Exact FQCN match 'App\\Internal\\*', // Namespace wildcard ], ], ], ];
Disabling Cache
You can disable caching for development purposes by setting cache.enabled to false:
'AttributeRegistry' => [ 'cache' => [ 'enabled' => false, ], ],
Warning
Disabling cache will cause attributes to be re-discovered on every request, which may impact performance. Only use this for development.
Smart Cache Validation
Instead of disabling the cache entirely, you can enable file hash validation to automatically invalidate stale cache entries when files change:
use Cake\Core\Configure; 'AttributeRegistry' => [ 'cache' => [ 'enabled' => true, // Auto-enable validation in debug mode 'validateFiles' => Configure::read('debug', false), ], ],
When validateFiles is enabled:
- ✅ Cache remains active - Fast performance
- ✅ Auto-detects changes - Validates file content hashes (xxh3) on cache load
- ✅ Granular invalidation - Only rebuilds when cached files are modified
Important
Limitation: This only validates existing cached entries. When you add new attributes to your codebase, you still need to manually refresh the cache:
bin/cake attribute discover # or in code: $registry->clearCache();
Tip
This is the recommended approach for development - you get cache performance benefits and automatic detection of changes to existing attributes, while only needing manual cache clearing when adding new ones.
Cache Configuration
The plugin uses compiled cache files for zero-cost attribute caching. Discovered attributes are stored as pre-compiled PHP files with direct object instantiation, leveraging OPcache for maximum performance.
Cache Location: By default, cache files are stored in tmp/cache/attribute_registry/. You can customize this path:
'AttributeRegistry' => [ 'cache' => [ 'path' => CACHE . 'attributes' . DS, // Custom cache directory ], ],
How it Works:
- During discovery, attributes are scanned and serialized into executable PHP files
- The compiled files contain direct
AttributeInfoobject instantiation code - OPcache automatically optimizes these files for near-zero overhead on subsequent loads
Performance: Compiled cache eliminates deserialization overhead entirely - loading cached attributes is as fast as requiring a PHP file with predefined objects.
Automatic Cache Clearing: When you run bin/cake cache clear_all, the AttributeRegistry cache is automatically refreshed via the attribute discovery process. Disable this behavior if needed:
'AttributeRegistry' => [ 'disableCacheClearListener' => true, ],
Attribute Argument Requirements
The compiled cache uses var_export() to serialize attribute arguments. Most PHP types work seamlessly:
Supported Types (no special handling needed):
- Scalars:
string,int,float,bool,null - Arrays of supported types
- Enums (PHP 8.1+, natively supported)
Object Arguments require __set_state() implementation:
class MyArgument { public function __construct( public string $value, ) {} // Required for cache serialization public static function __set_state(array $data): self { return new self( value: $data['value'], ); } } #[Attribute] class MyAttribute { public function __construct( public MyArgument $arg, // Object argument - needs __set_state() ) {} }
Tip
If caching fails because an object lacks __set_state(), an error will be logged at logs/error.log and the attribute will be skipped from the cache.
Usage
Accessing the AttributeRegistry
The AttributeRegistry can be accessed in two ways:
Option 1: Singleton
Use getInstance() anywhere in your application without requiring dependency injection:
use AttributeRegistry\AttributeRegistry; // Anywhere in your code $registry = AttributeRegistry::getInstance(); $routes = $registry->findByAttribute('Route');
Option 2: Dependency Injection
The registry is also available via CakePHP's dependency injection container:
use AttributeRegistry\AttributeRegistry; // In a Controller class MyController extends AppController { public function index(AttributeRegistry $registry): Response { $routes = $registry->findByAttribute('Route'); // ... } } // In a Command class MyCommand extends Command { public function __construct( private readonly AttributeRegistry $registry, ) { parent::__construct(); } }
Both approaches return the same singleton instance, ensuring consistent caching behavior.
Discovery Methods
The AttributeRegistry service provides several methods for finding attributes:
Note
All query methods (findByAttribute, findByClass, findByTargetType) internally call discover(). The discovery result is cached after the first call, so subsequent queries within the same request are fast. When adding new attributes to your codebase, clear the cache using $registry->clearCache() or run bin/cake attribute discover to refresh the registry.
Discover All Attributes
The discover() method returns an AttributeCollection - a powerful extension of CakePHP's Collection with domain-specific filter methods:
// Get all discovered attributes as a collection $attributes = $registry->discover(); // Convert to array when needed $array = $attributes->toList();
Fluent Filtering with AttributeCollection
The AttributeCollection provides chainable filter methods for building complex queries. All filter methods accept multiple arguments with OR logic (matches any):
use AttributeRegistry\Enum\AttributeTargetType; // Filter by exact attribute class (single or multiple) $routes = $registry->discover() ->attribute(Route::class) ->toList(); // Multiple attributes - matches Route OR Get OR Post $httpAttributes = $registry->discover() ->attribute(Route::class, Get::class, Post::class) ->toList(); // Filter by namespace pattern (supports wildcards) $appAttributes = $registry->discover() ->namespace('App\\Controller\\*') ->toList(); // Multiple namespaces $attributes = $registry->discover() ->namespace('App\\Controller\\*', 'App\\Model\\*') ->toList(); // Filter by target type (single or multiple) $methodAttributes = $registry->discover() ->targetType(AttributeTargetType::METHOD) ->toList(); // Methods OR properties $memberAttributes = $registry->discover() ->targetType(AttributeTargetType::METHOD, AttributeTargetType::PROPERTY) ->toList(); // Filter by exact class name (single or multiple) $controllerAttributes = $registry->discover() ->className(UsersController::class, PostsController::class) ->toList(); // Partial matching with contains methods $routeAttributes = $registry->discover() ->attributeContains('Route') ->toList(); $controllerAttributes = $registry->discover() ->classNameContains('Controller') ->toList();
Combining Filters
Chain multiple filters to narrow down results (uses AND logic between different filter types):
// Find all Route attributes on methods in the App\Controller namespace $routes = $registry->discover() ->attribute(Route::class) ->namespace('App\\Controller\\*') ->targetType(AttributeTargetType::METHOD) ->toList();
Using Standard Collection Methods
Since AttributeCollection extends CakePHP's Collection, all standard methods are available:
// Group attributes by class $grouped = $registry->discover() ->attribute(Route::class) ->groupBy(fn($attr) => $attr->className) ->toArray(); // Filter with custom logic $postRoutes = $registry->discover() ->attribute(Route::class) ->filter(fn($attr) => ($attr->arguments['method'] ?? 'GET') === 'POST') ->toList(); // Map to extract specific data $paths = $registry->discover() ->attribute(Route::class) ->map(fn($attr) => $attr->arguments['path'] ?? '/') ->toList(); // Count results $count = $registry->discover() ->attribute(Route::class) ->count(); // Check if any exist $hasRoutes = $registry->discover() ->attribute(Route::class) ->isEmpty() === false;
Find by Attribute Name
// Find all usages of a specific attribute $routes = $registry->findByAttribute(Route::class); $columns = $registry->findByAttribute(Column::class); // Partial matching is also supported $routes = $registry->findByAttribute('Route');
Find by Class Name
// Find attributes on a specific class $attributes = $registry->findByClass(UserController::class); // Partial matching is also supported $attributes = $registry->findByClass('Controller');
Find by Target Type
use AttributeRegistry\Enum\AttributeTargetType; // Find all class-level attributes $classAttributes = $registry->findByTargetType(AttributeTargetType::CLASS_TYPE); // Find all method-level attributes $methodAttributes = $registry->findByTargetType(AttributeTargetType::METHOD); // Find all property-level attributes $propertyAttributes = $registry->findByTargetType(AttributeTargetType::PROPERTY);
Cache Management
// Clear all cached attribute data (removes compiled cache files) $registry->clearCache(); // Warm the cache (clear and rediscover - regenerates compiled files) $registry->warmCache(); // Check if cache is enabled if ($registry->isCacheEnabled()) { // Cache is active }
The cache records file modification timestamps (filemtime) for discovered attributes, which you can use to detect when source files have changed and decide when to rebuild or clear the cache.
Working with AttributeInfo
Each discovered attribute is returned as an AttributeInfo value object:
use AttributeRegistry\ValueObject\AttributeInfo; foreach ($registry->discover() as $attr) { // Basic information echo $attr->attributeName; // Full attribute class name echo $attr->className; // Class containing the attribute echo $attr->filePath; // File where attribute was found echo $attr->lineNumber; // Line number in file // Attribute arguments print_r($attr->arguments); // Array of constructor arguments // Target information echo $attr->target->type->value; // 'class', 'method', 'property', etc. echo $attr->target->targetName; // Name of the target element echo $attr->target->parentClass; // Parent class (for methods/properties) // Instantiate the actual attribute $instance = $attr->getInstance(); // With type safety $route = $attr->getInstance(MyRoute::class); }
Example: Building a Route Registry
use Attribute; #[Attribute(Attribute::TARGET_CLASS)] class Route { public function __construct( public string $path, public string $method = 'GET', ) {} } #[Attribute(Attribute::TARGET_METHOD)] class Get { public function __construct( public ?string $path = null, ) {} } // Controller #[Route('/users')] class UsersController { #[Get('/')] public function index(): void {} #[Get('/{id}')] public function view(int $id): void {} } // In your application $routes = $registry->findByAttribute(Route::class); foreach ($routes as $routeInfo) { $route = $routeInfo->getInstance(Route::class); echo "Route: {$route->path} ({$route->method})"; }
Console Commands
The plugin provides three console commands for managing attributes:
Discover Attributes
Scan and cache all attributes:
bin/cake attribute discover
Output:
Clearing attribute cache...
Discovering attributes...
Discovered 42 attributes in 0.234s
List Attributes
List discovered attributes with optional filtering:
# List all attributes bin/cake attribute list # Filter by attribute name bin/cake attribute list --attribute Route # Filter by class name bin/cake attribute list --class UserController # Filter by target type bin/cake attribute list --type method
Output:
Found 5 attributes:
+---------------------------+----------------------------------+--------+-----------------+
| Attribute | Class | Type | Target |
+---------------------------+----------------------------------+--------+-----------------+
| App\Attribute\Route | App\Controller\UsersController | class | UsersController |
| App\Attribute\Get | App\Controller\UsersController | method | index |
| App\Attribute\Get | App\Controller\UsersController | method | view |
| MyPlugin\Attribute\Cache | MyPlugin\Model\Table\PostsTable | method | findPublished |
| MyPlugin\Attribute\Queue | MyPlugin\Service\EmailService | method | sendWelcome |
+---------------------------+----------------------------------+--------+-----------------+
Inspect Attributes
View detailed information about specific attributes:
# Inspect by attribute name bin/cake attribute inspect Route # Inspect attributes on a specific class bin/cake attribute inspect --class UserController bin/cake attribute inspect -c UserController
Output:
Found 2 attributes for attribute "Route":
1. App\Attribute\Route
Class: App\Controller\UsersController
Target: UsersController (class)
File: /path/to/src/Controller/UsersController.php:12
Arguments:
- path: /users
- method: GET
DebugKit Panel
When DebugKit is installed, the plugin automatically registers a panel for browsing discovered attributes.
The panel provides:
- Overview of all discovered attributes grouped by type or file
- Search functionality to filter attributes
- Re-discover button to refresh the attribute cache
Testing
Run the test suite:
# Run all tests composer test # Run with coverage composer test-coverage # Run PHPStan analysis composer phpstan # Check code style composer cs-check # Fix code style composer cs-fix # Run Rector checks composer rector-check
Contributing
Contributions are welcome! Please follow these guidelines:
- Code Standards: Follow CakePHP coding standards
- Tests: Add tests for new features
- PHPStan: Ensure level 8 compliance
- Documentation: Update README for new features
Development Setup
# Clone the repository git clone git@github.com:josbeir/cakephp-attribute-registry.git cd cakephp-attribute-registry # Install dependencies composer install # Run tests composer test # Run static analysis composer phpstan
License
This plugin is open-sourced software licensed under the MIT license.
Credits
- Built with CakePHP
