Laravel
Attributes in Laravel 13
I've always liked the direction PHP has taken over the last few years. Features like enums, readonly properties, constructor property promotion, sorted and named arguments, match expression, and much more have made the language significantly more expressive. And how (and where) Laravel moves, I like even more. I've prepared an article that explores one of my favorite PHP features - Attributes - which I believe have become one of the fundamental puzzles of modern Laravel (especially in the latest releases).
What is good about PHP nowadays is that it develops new features, functions, and patterns quite quickly. And Laravel is the framework that adopts new things even faster. In this article I want to cover one particular topic - Attributes. They were introduced in PHP 8 and have been fully embraced by Laravel in the latest versions, especially 13.x. I think they are such a beautiful feature to use widely across project codebases. Attributes pattern is a descriptive and very distinct way to attach the necessary metadata to a particular class. And, what is most important, they are highly readable, both for humans and for AI agents.
Before we cover some good use cases of attributes in Laravel, let's first define what they are in the general PHP context. Introduced in PHP 8, attributes provide a native way to attach structured metadata to classes, methods, properties, parameters, and constants. Unlike regular object properties, attributes are not part of an object's state, but rather metadata associated with the code itself. Dynamically this metadata can be read by using PHP's Reflection API to configure or modify behavior. It's a good alternative for PHPDoc annotations, additional configuration files, properties, static methods, and so on.
In Laravel, there are many use cases where attributes become an essential part of this beautiful framework. So let's cover some best use cases.
1. Controllers
Laravel 13 introduces three first-party controller attributes that allow you to declare middleware and authorization directly on your controllers and actions.
#[Middleware]
Use the #[Middleware] attribute to apply one or more middleware to an entire controller or specific method.
#[Middleware('auth')]
#[Middleware('verified')]
class PostShowController extends Controller
{
public function __invoke(Post $post): RedirectResponse
{
//
}
}
A good example of a use case is to rate-limit an endpoint like forgot password etc.
#[Middleware('throttle:5,1')]
class ForgotPasswordController extends Controller
{
//
}
#[Authorize]
The #[Authorize] attribute declaratively applies authorization to a controller action. Instead of using the can middleware or calling $this->authorize() inside the controller method, you simply specify the policy ability and the route parameter that should be resolved into the model.
#[Middleware('auth')]
class PostShowController extends Controller
{
#[Authorize('view', 'post')]
public function __invoke(Post $post): RedirectResponse
{
//
}
}
2. Models
Another great bucket of use cases is usage of attributes in Eloquent Model classes. I'm a big fan of tiny Model classes with as little noise as possible so introducing attributes there was a real gift for me. So we actually could use attributes to define a lot of the static-wired or meta-related things around our model which define the class relations and rules applied to each object instance of this class.
The model attributes in Laravel 13 are things like #[Table], #[Connection], #[Fillable], #[Guarded], #[Hidden], #[Visible], #[Appends], #[Touches], #[WithoutTimestamps], #[WithoutIncrementing], #[ObservedBy], #[ScopedBy], #[UsePolicy], #[UseFactory], #[UseResource], #[UseResourceCollection], #[UseEloquentBuilder], and #[Scope]. But as I'm the adept of strict separation of concern and avoiding the direct exposing of object data and mass mutability of the object attributes, I would completely avoid such things as #[Fillable], #[Hidden], #[Appends] etc - both in attributes or in the old-school way of using the class properties for that. But this is more relevant for another topic. What I mostly use in each model is: #[Table], #[ObservedBy], #[CollectedBy], #[UsePolicy], #[UseFactory]. Below you can see the example of how good looking the Model is now. Table is deterministic, collection class bound to the model, policy is wired up, factory is defined, observer class is there.
/**
* @mixin IdeHelperMonitor
*/
#[Table('monitors')]
#[ObservedBy(MonitorObserver::class)]
#[CollectedBy(MonitorCollection::class)]
#[UsePolicy(MonitorPolicy::class)]
#[UseFactory(MonitorFactory::class)]
final class Monitor extends BaseModel
{
/**
* @return BelongsTo<Team, $this>
*/
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
/**
* @return BelongsTo<Project, $this>
*/
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
/**
* @return HasMany<Incident, $this>
*/
public function incidents(): HasMany
{
return $this->hasMany(Incident::class);
}
/**
* @return HasMany<CheckResult, $this>
*/
public function checkResults(): HasMany
{
return $this->hasMany(CheckResult::class);
}
}
Of course it all depends on what is your own code conventions and approaches and this could define the attributes usage, but the idea is clear.
3. Commands
This is where attributes are definitely placed in the right spot. In Artisan Commands. This allows you to configure command metadata directly on the command class instead of defining protected properties. The available command attributes are:
#[Signature] // defines the Artisan command signature.
#[Description] // provides the command description displayed by php artisan list.
#[Help] // supplies additional help text shown when running php artisan help.
#[Aliases] // registers one or more alternative command names.
#[Hidden] // hides the command from the Artisan command list while keeping it executable.
#[Usage] // provides one or more usage examples displayed in the help output.
Using these attributes keeps all command metadata in one place, making the command class self-contained and eliminating the need for protected $signature and $description properties.
4. Queue jobs
Another great use case for attributes in Laravel. Most of these attributes replace the public properties that were traditionally defined on the job class:
#[Connection('redis')]
#[Queue('emails')]
#[Delay(30)]
#[Tries(3)]
#[Timeout(120)]
#[Backoff([10, 30, 60])]
#[MaxExceptions(3)]
#[UniqueFor(3600)]
#[FailOnTimeout]
#[DeleteWhenMissingModels]
class SendConfirmationEmailJob implements ShouldQueue
{
//
}
5. Form requests
The freshest Laravel extended attributes for Form Requests, allowing you to declaratively configure validation behavior and the response generated when validation fails. This keeps the request's configuration close to the class itself, eliminating the need to define additional properties or override methods for common scenarios. Some of them not only enhance already existing behavior but introduce something totally new, which is worth checking - like #[FailOnUnknownFields]. This attribute could be useful if we want to prevent in body request anything suspicious that is not in a bag of our validate attributes. It could be especially useful in public post requests. The available Form Request attributes are:
#[StopOnFirstFailure] // stops validation as soon as the first validation rule fails.
#[FailOnUnknownFields] // rejects requests containing fields that are not defined by the validation rules.
#[RedirectTo] // redirects the user to a specific URI when validation fails.
#[RedirectToRoute] // redirects the user to a named route when validation fails.
#[ErrorBag] // stores validation errors in a named error bag.
6. Dependency injection
It's an absolutely new thing in the latest Laravel 13 versions. Instead of manually resolving dependencies or injecting configuration values inside constructors, you can now declaratively describe what should be injected. Some of the most useful container attributes include:
#[Config] — injects a configuration value.
#[CurrentUser] — injects the currently authenticated user.
#[RouteParameter] — injects a route parameter.
#[Storage] — injects a filesystem disk.
#[Cache] — injects a cache store.
#[DB] — injects a database connection.
#[Log] — injects a log channel.
#[Bind] — binds an interface to its implementation.
#[Singleton] — registers a class as a singleton.
#[Scoped] — registers a class with a scoped lifetime.
In the example below, notice how the container automatically resolves each dependency based on the applied attributes. Instead of manually reaching for the Config facade or calling config('app.timezone') somewhere inside the class, the timezone is injected directly into the constructor. The same applies to the current authenticated user, storage disks, cache stores, and other framework services.
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
class ExportService
{
public function __construct(
#[Config('app.timezone')]
protected string $timezone,
#[Storage('s3')]
protected Filesystem $disk,
#[CurrentUser]
protected User $user,
) {
}
}
This approach reduces boilerplate, makes dependencies explicit, and keeps the class focused on its actual responsibility rather than on retrieving services from the framework. In other words, instead of telling Laravel how to obtain a dependency, you simply declare what you need, and the service container takes care of the rest.
7. Resources
Laravel 13 also introduces attributes for API Resources, allowing you to declaratively configure how resource collections behave. Currently, two attributes are available:
#[Collects] // specifies the resource class that should be used for each item in a resource collection.
#[PreserveKeys] // preserves the original collection keys when transforming resources.
Conclusion
It's obviously not all the attributes covered in this article but the general idea is clear - attributes pattern is a great way to make things more distinct, descriptive, readable, reduce the noise and turn the codebase on the next level. Attributes don't make Laravel magically better. What they do is move important metadata next to the code it describes. Instead of gathering configuration across service providers, protected properties, middleware definitions, and helper methods, the class becomes a self-describing unit. That's beneficial for developers reading the code today, and increasingly valuable for AI coding assistants that need to quickly understand the structure and intent of a codebase. This is worth including in the next refactoring of old codebase and this is by default - the playbook for freshly installed Laravel applications.