Using PHP Attributes in Laravel
PHP 8.0 introduced attributes as a native way to add structured metadata to classes, methods, properties, and more. Attributes require PHP 8.0 or higher.
Laravel itself already relies heavily on metadata (routes, middleware, validation rules, policies, etc.), so attributes are a natural fit when you want to attach configuration directly to your code instead of scattering it in arrays or config files.
What are PHP attributes?
Attributes are special metadata that you attach using the #[...] syntax. Under the hood they are just classes marked with the built-in #[Attribute] attribute.
use Attribute;
#[Attribute]
class MyAttribute
{
public function __construct(public string $value)
{
}
}
#[MyAttribute('example')]
class SomeService
{
}
At runtime, you can use PHP's reflection API (e.g. ReflectionClass, ReflectionMethod, ReflectionProperty) to read and act on those attributes.
This is the same mechanism described in the official PHP attributes overview, but here we'll focus specifically on how to use it inside a Laravel app.
Declaring a simple attribute
Let's say we want to mark some controller actions as "internal only", meaning they should only be accessible from within our network or by certain roles.
First, we define a custom attribute class in app/Attributes:
// app/Attributes/InternalOnly.php
namespace App\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class InternalOnly
{
public function __construct(
public ?array $allowedIps = null,
) {
}
}
Attribute::TARGET_METHODmeans this attribute can only be used on methods.- The
$allowedIpsargument lets you configure per-action IP whitelists if needed.
Using attributes on Laravel controllers
Now we can use this attribute on a controller method:
// app/Http/Controllers/ReportController.php
namespace App\Http\Controllers;
use App\Attributes\InternalOnly;
use Illuminate\Http\Request;
class ReportController extends Controller
{
#[InternalOnly(["10.0.0.1", "10.0.0.2"])]
public function index(Request $request)
{
// Only internal IPs should reach here
return view('reports.index');
}
}
This keeps the configuration (InternalOnly, allowed IPs) right next to the method it affects.
Now we need Laravel to respect this attribute, which is where middleware comes in.
Reading attributes in middleware
We'll create a middleware that uses reflection to check for our InternalOnly attribute on the current controller action.
// app/Http/Middleware/CheckInternalOnly.php
namespace App\Http\Middleware;
use App\Attributes\InternalOnly;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class CheckInternalOnly
{
public function handle(Request $request, Closure $next)
{
$route = $request->route();
if (! $route) {
return $next($request);
}
$controllerClass = $route->getControllerClass();
$method = $route->getActionMethod();
if (! $controllerClass || ! $method) {
return $next($request);
}
$reflection = new \ReflectionMethod($controllerClass, $method);
$attributes = $reflection->getAttributes(InternalOnly::class);
if (empty($attributes)) {
// No InternalOnly attribute, continue as normal
return $next($request);
}
/** @var InternalOnly $config */
$config = $attributes[0]->newInstance();
$clientIp = $request->ip();
if ($config->allowedIps !== null && ! in_array($clientIp, $config->allowedIps, true)) {
throw new AccessDeniedHttpException('This endpoint is internal only.');
}
return $next($request);
}
}
Then register the middleware. The location differs between Laravel versions.
Laravel 10 — app/Http/Kernel.php:
protected $routeMiddleware = [
// ...
'internal_only' => \App\Http\Middleware\CheckInternalOnly::class,
];
Laravel 11+ — bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'internal_only' => \App\Http\Middleware\CheckInternalOnly::class,
]);
})
Finally, apply it to routes that should respect the attribute:
Route::middleware(['internal_only'])
->get('/reports', [\App\Http\Controllers\ReportController::class, 'index']);
Now the InternalOnly attribute is actually enforced.
- If an action has the attribute, the middleware checks the IP.
- If not, the request continues normally.
Using attributes for console commands and services
Laravel uses Symfony Console under the hood, and modern Symfony allows defining commands using attributes like #[AsCommand]. You can adopt a similar pattern for your own internal tooling.
Example: tagging services with attributes
Let's say you have certain classes that should be auto-registered for a specific process, like scheduled tasks or background workers.
Define a Task attribute:
// app/Attributes/Task.php
namespace App\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Task
{
public function __construct(
public string $name,
public ?string $schedule = null,
) {
}
}
Use it on a class:
// app/Tasks/CleanOldReports.php
namespace App\Tasks;
use App\Attributes\Task;
#[Task('clean-old-reports', schedule: '0 3 * * *')]
class CleanOldReports
{
public function __invoke(): void
{
// Clean up old records...
}
}
In a service provider, you could scan a namespace for classes with the Task attribute and register them dynamically (e.g. into the scheduler or a custom registry).
// app/Providers/TaskServiceProvider.php
namespace App\Providers;
use App\Attributes\Task;
use Illuminate\Support\ServiceProvider;
use ReflectionClass;
class TaskServiceProvider extends ServiceProvider
{
public function boot(): void
{
$taskClasses = [
\App\Tasks\CleanOldReports::class,
// Add more or discover them dynamically
];
foreach ($taskClasses as $taskClass) {
$reflection = new ReflectionClass($taskClass);
$attributes = $reflection->getAttributes(Task::class);
if (empty($attributes)) {
continue;
}
/** @var Task $task */
$task = $attributes[0]->newInstance();
// Here you could register the task somewhere, e.g.:
// app(Schedule::class)->call(new $taskClass())->cron($task->schedule);
// (The line above is intentionally simplified pseudocode — you'd
// need to resolve the Schedule instance and handle any dependencies.)
}
}
}
This gives you a declarative way to describe scheduled tasks or background jobs without maintaining big configuration arrays.
When to use attributes in Laravel
Attributes are useful in Laravel when you want to:
- Attach metadata directly to controllers, models, or services.
- Build declarative APIs for things like permissions, throttling, logging, or caching.
- Reduce duplication between configuration arrays and the code they configure.
- Build your own framework-like features on top of Laravel.
They may not be a good fit when:
- The configuration is highly dynamic and depends on runtime data (per tenant, per user, etc.).
- A simple config file or
.envvariable is clearer. - You're adding complexity without a clear benefit.
Final thoughts
PHP attributes give you a powerful, native way to attach metadata directly to your Laravel code. By combining them with Laravel's middleware, service providers, and scheduling, you can build expressive, declarative APIs tailored to your application.
Start small: create a single attribute (like InternalOnly) and wire it into middleware. Once you're comfortable with the pattern, you'll find many opportunities to use attributes to replace scattered configuration arrays and make your Laravel codebase clearer and easier to maintain.