bfg/attributes
bfg/attributes brings PHP 8 attributes to Laravel, giving you a clean, declarative way to configure models, controllers, and services. Reduce boilerplate and keep metadata close to your code with simple, reusable attributes.
composer require bfg/attributes. Check composer.json first — you need PHP 8.0+ (8.1+ realistically for readonly/enums in attributes) and a Laravel version that supports package auto-discovery.src/ for the attribute classes and whatever helper reads them (commonly a Reader, Resolver, Attributes facade, or a trait). With 0 stars and 0 dependents the README will likely be thin, so treat the source as the documentation. Confirm whether it ships a service provider (extra.laravel.providers) or is purely a static utility.tinker:$ref = new ReflectionClass(App\Models\Post::class);
foreach ($ref->getAttributes() as $attribute) {
dump($attribute->getName(), $attribute->getArguments());
}
If that works natively, the package is only worth adopting for the convenience layer it adds on top — so verify what that layer actually buys you (caching? attribute inheritance? a discovery registry?).
const. E.g. tagging a model, job, or exporter with #[SlugSource('title')] or #[Retry(3)] and reading it at boot time rather than hard-coding a switch.Because the package is essentially unproven (0 stars, 0 dependents, score 0.03), do the spike before you let it into a shared codebase, and keep the call sites behind your own thin wrapper.
Declare attributes, don't consume them inline. Define your attribute classes under app/Attributes/ with explicit targets and flags — this is plain PHP 8 and does not depend on the package:
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Feature
{
public function __construct(public string $flag) {}
}
Then use the package's reader (or a small helper of your own) as the single entry point for resolution.
Boot-time discovery in a service provider. The most valuable day-to-day pattern: scan a directory of classes once, read their attributes, and register bindings/tags. Do this in register() or a dedicated AttributeServiceProvider and cache the result — never reflect on every request.
foreach (File::allFiles(app_path('Services')) as $file) {
$class = /* resolve FQCN from path */;
foreach ((new ReflectionClass($class))->getAttributes(Feature::class) as $attr) {
$this->app->tag($class, 'feature:'.$attr->newInstance()->flag);
}
}
Use attributes as declarative config for your own abstractions. A strong pattern is an interface + attribute pair: the interface defines behavior, the attribute carries the parameters (#[RateLimit(60)], #[Cacheable(ttl: 300)]). Your resolver reads the attribute and hands off to the concrete implementation. This keeps the package at the edges of your code rather than tangled through it.
Bridge to the container lazily. Resolve and instantiate attributes only for classes you actually touch (getAttributes() + newInstance()), not for the whole app. Attribute constructors run on newInstance(), so keep them side-effect free — an attribute constructor that does I/O will surprise you.
Wrap it. Given the maturity level, put a single AttributeResolver service in front of the package. If you later swap to raw reflection or a maintained alternative, one class changes instead of fifty.
Test through reflection. Tests are straightforward — assert on the resolver output rather than on attribute instances:
$meta = app(AttributeResolver::class)->for(ExportJob::class);
$this->assertSame('csv', $meta->format);
composer.json and budget for forking it — the underlying PHP 8 reflection API is stable, so a fork is cheap.ReflectionClass::getAttributes() on a subclass returns only the subclass's own attributes. Use getAttributes(Base::class, ReflectionAttribute::IS_INSTANCEOF) when you need to match subclasses of an attribute, and walk getParentClass() yourself if you want inheritance semantics. This is the #1 source of "why is my attribute not firing" bugs.getAttributes() returns ReflectionAttribute, not your class. You must call newInstance() (or getArguments()), and you must pass the FQCN or an IS_INSTANCEOF flag — passing a short name silently returns an empty array.config(), no env(), no now() inside #[...]. If you need runtime values, store a config key string and resolve it later in your reader.Attribute::TARGET_* and IS_REPEATABLE matter. Omitting the target makes the attribute legal everywhere, which hides mistakes; omitting IS_REPEATABLE makes a second application a fatal error. Set both explicitly.opcache.validate_timestamps=0 in production masking a stale map after a hotfix.php artisan about / bootstrap/cache/packages.php. If it doesn't and you expected it to, add it manually to config/app.php — a silent no-op is a common failure mode for low-traffic packages.dd((new ReflectionClass($c))->getAttributes()) — before suspecting the package. Most failures are a wrong FQCN, a missing IS_INSTANCEOF, or an attribute applied to a method/property when it's declared TARGET_CLASS.How can I help you explore Laravel packages today?