crell/attributeutils
Utilities for PHP 8+ attributes: a cached AttributeReader that fetches and instantiates attributes on classes, methods, properties, and parameters, with helpers for repeatable attributes, inheritance, and object mapping. A lightweight base for attribute-driven config and dependency injection.
Install via Composer — there is no Laravel service provider or facade, so it's a plain library:
composer require crell/attributeutils
First look at three things:
Crell\AttributeUtils\ClassAnalyzer — the main entry point. Given a class name and an attribute class, it returns a fully-populated "definition" object aggregating every attribute instance on the class, its properties, methods, and parameters.Crell\AttributeUtils\AttributeUtils — factory helpers (createClassAnalyzer(), createClassScanner()) so you don't wire interfaces manually.Crell\AttributeUtils\Attributes\Reflect\... — the attribute interfaces (ScansClassDefinition, ScansProperties, ScansMethods) that let your own attribute class opt in to deeper scanning.Minimal first use case — annotate a controller and read the attribute back:
use Crell\AttributeUtils\Attributes\Reflect\ReflectClass;
use Crell\AttributeUtils\Http\Attribute\Route;
use Crell\AttributeUtils\ClassAnalyzer;
use Crell\AttributeUtils\AttributeUtils;
#[Attribute(Attribute::TARGET_CLASS)]
class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
) {}
}
#[Route('/dashboard', 'GET')]
class DashboardController {}
$analyzer = AttributeUtils::createClassAnalyzer();
$route = $analyzer->analyze(DashboardController::class, Route::class);
echo $route->path; // /dashboard
For a single attribute, plain ReflectionAttribute::newInstance() is fine; reach for this package once you need inheritance, merging, repeatables, or caching.
Analyzer as a singleton, resolved from the container. Bind ClassAnalyzer once and inject it. Do not instantiate it per request in a hot path — reflection is the expensive part.
// A service provider
$this->app->singleton(ClassAnalyzer::class, fn () => AttributeUtils::createClassAnalyzer());
Definition objects. The real value isn't a single attribute — it's the aggregated definition. Have your attribute's target be a small value object you build in analyze(), then treat that object as your runtime metadata. This is the same shape as Symfony's routing metadata or how Laravel resolves policy/ability metadata — good for:
#[Route], #[Middleware]).Gate::policy().#[Validate('required|email')] on properties).#[SerializeAs('json')] on a property).Interface-driven scanning. Implement ScansClassDefinition, ScansProperties, ScansMethods, ScansParameters on your attribute class to tell the analyzer how deep to recurse. This is where the package earns its keep versus raw reflection: you declare what you care about and let inheritance/merge/repeat rules work.
Attribute inheritance and merge order. Classes can inherit attribute values from parents and interfaces. Order is child → parent → interface; last-write wins per property, but interfaces contribute first. This is what makes the package usable for base controller/model attributes that subclasses should be able to override.
Repeatable attributes. Use #[Attribute(Attribute::IS_REPEATABLE)] and collect them into an array. The scanner preserves declaration order — useful when order encodes priority (middleware chains, layered rules).
Property attribute collection. Scanning a class scans all properties at once, so batch work into one analyze() call instead of one call per attribute. If you need ReflectionProperty, ReflectionMethod, ReflectionParameter, the analyzer gives you a ReflectionClass you can hang on to — don't re-reflect.
Caching layer. The analyzer accepts a PSR-16 SimpleCache. In Laravel, wrap around the cache store:
$cache = new \Crell\AttributeUtils\Cache\Psr16Cache(
Cache::store('array')->psr(), // or a file store
'attribute-meta',
);
$analyzer = AttributeUtils::createClassAnalyzer($cache);
Key on class name + attribute class. Use a deploy-time tag/cache-version bump so a code change invalidates everything.
Custom analyzers. Implement ClassAnalyzer for domain-specific behavior (e.g. scanning only app/Http/Controllers/** and returning a registry keyed by route signature). Wrap the library analyzer rather than reimplementing reflection.
#[\Attribute] targets are enforced silently-ish. If TARGET_PROPERTY is set but the attribute is placed on a method, PHP will reject it at newInstance() time — but only when the analyzer reaches that node. Misplaced attributes can surface as confusing "no definition" outcomes rather than a clear error.ReflectionClass::getAttributes() does not walk parents for you. The library handles inheritance only when your attribute implements the appropriate Scans* interface — if you skip it, parent attributes are invisible.filemtime() of the class file, a git SHA in the namespace, or flush the tag on deploy.composer dump-autoload.boot(). Do the scanning lazily inside the container binding closure or on-first-use, not during the service provider boot — otherwise every request pays for it even when the metadata isn't needed.ReflectionParameter, not ReflectionProperty, even though the value is a property. Use the parameter scanning interface if you need them.var_dump($analyzer->analyze(Foo::class, MyAttr::class)) is the fastest diagnostic. If it returns null, the attribute likely isn't #[\Attribute]-flagged for that target, or the class is in a namespace the autoloader can't reflect (e.g. eval'd or dynamically generated strings).ScansClassDefinition (etc.) on your attribute to add per-node logic. That's the intended hook — subclassing ClassAnalyzer is the escape hatch for cross-cutting changes, but it will tie you to internals.How can I help you explore Laravel packages today?