Most applications start with a few simple decisions living in ordinary code. A controller calls a service. A command has a signature. A model has a scope. A webhook event maps to a handler.

Now we need to describe rules that belong to a class, method, property, or parameter, but are not the implementation of that thing. We reach for arrays in service providers, naming conventions, PHPDoc tags, switch statements, or comments that slowly become part of the application's contract.

For example, a webhook dispatcher might need to know which handler accepts invoice.paid. We could keep that mapping in a central array:

This works, but the information is separated from the handler it describes. When someone creates a new handler, they must remember a second place that needs to change. That is a small form of coupling, and it becomes expensive when the mapping gets more detailed.

PHP attributes give us another option. They let us attach structured, machine-readable metadata directly to code. A framework or application can then read that metadata and turn it into behavior.

Since PHP 8, attributes have become a first-class language feature. Laravel uses them for things like Eloquent scopes and contextual dependency injection, but the idea is much bigger than any one framework.

In this article, let's do a deep dive into PHP attributes: what they are, why they are useful, how to build and consume them, and just as importantly, when they are the wrong tool.

The most important mental model is this:

An attribute describes code. It does not execute code by itself.

An attribute is metadata attached to a PHP declaration. PHP can attach that metadata to classes, methods, functions, properties, class constants, and parameters. The metadata is structured because it is represented by a real PHP class with a constructor and typed properties.

OrderPlaced still behaves exactly as it did before. PHP does not automatically create an audit record, publish an event, or call a logger just because it finds #[Audit('orders')].

This class has audit metadata and its stream is orders.

Something else must read that declaration and decide what it means. That something might be your application, a framework, a package, a test runner, or a static analysis tool.