/ About
The Problem With PHP Attributes
PHP 8 introduced native attributes, a declarative way to attach metadata to classes, methods, and properties. The syntax is nice:
#[Attribute]
class RateLimit
{
public function __construct(
public int $maxAttempts,
public int $decayMinutes = 1
) {}
}
#[RateLimit(maxAttempts: 60, decayMinutes: 1)]
class ApiController
{
#[RateLimit(maxAttempts: 10, decayMinutes: 5)]
public function store() { }
}
But actually reading those attributes? That's where PHP leaves you on your own. You need the Reflection API:
$reflection = new ReflectionClass(ApiController::class);
$attributes = $reflection->getAttributes(RateLimit::class);
if (count($attributes)) {
$instance = $attributes[0]->newInstance();
// Now you can use $instance->maxAttempts, etc.
}
For a single lookup, that's manageable. But if you're using attributes across your application for routing metadata, authorization rules, validation constraints, or feature flags, you'll end up writing the same reflection boilerplate everywhere.
What This Package Does
Laravel PHP Attributes handles three things:
- Auto-discovery: scans your
appdirectory for all PHP 8 Attributes and their usages - Caching: stores the results so subsequent requests skip the filesystem scan entirely
- Simple API: provides an
Attributesfacade for querying attributes without touching Reflection
The Facade API
use TWithers\LaravelAttributes\Facades\Attributes;
// Find all attributes on a class
$target = Attributes::findByClass(ApiController::class);
$attrs = $target->allAttributes();
// Find attributes on a specific method
$target = Attributes::findByClassMethod(ApiController::class, 'store');
$rateLimit = $target->findByName(RateLimit::class);
// Find every class/method/property that uses a specific attribute
$targets = Attributes::findTargetsWithAttribute(RateLimit::class);
The AttributeTarget
Every result comes back as an AttributeTarget with a consistent interface:
$type: whether the target is a class, method, or property$className: the fully qualified class name$identifier: the method or property name (null for class-level attributes)allAttributes(): returns all attached attributes asAttributeInstanceobjectshasAttribute(string $name): quick boolean checkfindByName(string $name): filtered lookup
Each AttributeInstance gives you both the attribute $name and the instantiated $instance with all constructor values populated.
Configuration
Publish the config to customize behavior:
php artisan vendor:publish --provider="TWithers\LaravelAttributes\AttributesServiceProvider" --tag="config"
The config lets you:
- Toggle caching: enabled by default (you should leave it on)
- Restrict attributes: list specific attribute classes to scan for instead of discovering all of them
- Limit directories: narrow the scan to specific namespaces instead of the entire
appdirectory
return [
'use_cache' => true,
'attributes' => [
// Empty = discover all. Or list specific ones:
// App\Attributes\RateLimit::class,
],
'directories' => [
'App' => app_path(),
// Or narrow it down:
// 'App\Http\Controllers' => app_path('Http/Controllers'),
],
];
Standalone Usage
If you don't want the service provider's automatic loading, you can use the AttributeAccessor class directly:
use TWithers\LaravelAttributes\AttributeAccessor;
$attrs = AttributeAccessor::forClass(SomeClass::class);
$attrs = AttributeAccessor::forClassMethod(SomeClass::class, 'someMethod');
$attrs = AttributeAccessor::forClassProperty(SomeClass::class, 'someProperty');
These methods use Reflection on demand, no caching, no auto-discovery. Useful for one-off lookups or testing.
Cache Management
The attribute cache lives at bootstrap/cache/attributes.php. Clear it during deployments:
php artisan attributes:clear
Add this to your deployment script alongside config:cache and route:cache to ensure newly added or modified attributes are picked up.
Installation
composer require twithers/laravel-php-attributes
Requires PHP 8.0+ and Laravel 8+. The package auto-discovers, no manual service provider registration needed.