A clean API for reading PHP attributes original

by Freek Van der Herten – 3 minute read

PHP 8.0 introduced attributes, and they're a great way to add structured metadata to classes, methods, properties, constants, and parameters. The concept is solid, but the reflection API you need to actually read them is surprisingly verbose. What should be a simple one-liner ends up being multiple lines of boilerplate every time. And if you want to find all usages of an attribute across an entire class, you're looking at deeply nested loops.

We just released spatie/php-attribute-reader, a package that gives you a clean, static API for all of that. Let me walk you through what it can do.

Using attribute reader

Imagine you have a controller with a Route attribute and you want to get the attribute instance. With native PHP, that looks like this:

$reflection = new ReflectionClass(MyController::class);
$attributes = $reflection->getAttributes(Route::class, ReflectionAttribute::IS_INSTANCEOF);

$route = null;
if (count($attributes) > 0) {
    $route = $attributes[0]->newInstance();
}

Five lines, and you still need to handle the case where the attribute isn't there. With the package, it becomes:

use Spatie\Attributes\Attributes;

$route = Attributes::get(MyController::class, Route::class);

One line. Returns null if the attribute isn't present, no exception handling needed.

It gets worse with native reflection when you want to read attributes from a method. Say you want the Route attribute from a controller's index method:

$reflection = new ReflectionMethod(MyController::class, 'index');
$attributes = $reflection->getAttributes(Route::class, ReflectionAttribute::IS_INSTANCEOF);

$route = null;
if (count($attributes) > 0) {
    $route = $attributes[0]->newInstance();
}

Same boilerplate, different reflection class. The package handles all targets with dedicated methods:

Attributes::onMethod(MyController::class, 'index', Route::class);
Attributes::onProperty(User::class, 'email', Column::class);
Attributes::onConstant(Status::class, 'ACTIVE', Label::class);
Attributes::onParameter(MyController::class, 'show', 'id', FromRoute::class);

Where things really get gnarly with native reflection is when you want to find every occurrence of an attribute across an entire class. Think about a form class where multiple properties have a Validate attribute. With plain PHP, you'd need something like:

$results = [];
$class = new ReflectionClass(MyForm::class);

foreach ($class->getProperties() as $property) {
    foreach ($property->getAttributes(Validate::class, ReflectionAttribute::IS_INSTANCEOF) as $attr) {
        $results[] = ['attribute' => $attr->newInstance(), 'target' => $property];
    }
}

foreach ($class->getMethods() as $method) {
    foreach ($method->getAttributes(Validate::class, ReflectionAttribute::IS_INSTANCEOF) as $attr) {
        $results[] = ['attribute' => $attr->newInstance(), 'target' => $method];
    }
    foreach ($method->getParameters() as $parameter) {
        foreach ($parameter->getAttributes(Validate::class, ReflectionAttribute::IS_INSTANCEOF) as $attr) {
            $results[] = ['attribute' => $attr->newInstance(), 'target' => $parameter];
        }
    }
}

foreach ($class->getReflectionConstants() as $constant) {
    foreach ($constant->getAttributes(Validate::class, ReflectionAttribute::IS_INSTANCEOF) as $attr) {
        $results[] = ['attribute' => $attr->newInstance(), 'target' => $constant];
    }
}

That's a lot of code for a pretty common operation. With the package, it collapses to:

$results = Attributes::find(MyForm::class, Validate::class);

foreach ($results as $result) {
    $result->attribute; // The instantiated attribute
    $result->target;    // The Reflection object
    $result->name;      // e.g. 'email', 'handle.request'
}

All attributes come back as instantiated objects, child classes are matched automatically via IS_INSTANCEOF, and missing targets return null instead of throwing.

In closing

We're already using this package in several of our other packages, including laravel-responsecache, laravel-event-sourcing, and laravel-markdown. It cleans up a lot of the attribute-reading boilerplate that had accumulated in those codebases.

You can find the full documentation on our docs site and the source code on GitHub. This is one of the many packages we've created. If you want to support our open source work, consider picking up one of our paid products.

Join 9,500+ smart developers

Get my monthly newsletter with what I learn from running Spatie, building Oh Dear, and maintaining 300+ open source packages. Practical takes on Laravel, PHP, and AI that you can actually use.

"Always fresh, useful tips and articles. Carefully selected community content. My favorite newsletter, which I look forward to every time."

Bert De Swaef — Developer at Vulpo & Youtuber at Code with Burt

No spam. Unsubscribe anytime. You can also follow me on X.

Found something interesting to share? Submit a link to the community section.