Attributes (PHP 8.0) attach metadata to classes, methods, properties, parameters and constants. They are written between #[ and ]. Frameworks read them to learn how to treat your code.
<?php
#[Attribute]
class Route
{
public function __construct(public string $path, public string $method = "GET") {}
}
class BlogController
{
#[Route("/blog", method: "GET")]
public function list(): string
{
return "all posts";
}
#[Route("/blog/new", method: "POST")]
public function create(): string
{
return "created";
}
}
$class = new ReflectionClass(BlogController::class);
foreach ($class->getMethods() as $method) {
foreach ($method->getAttributes(Route::class) as $attribute) {
$route = $attribute->newInstance();
echo $route->method . " " . $route->path . " -> " . $method->getName() . "\n";
}
}
GET /blog -> list
POST /blog/new -> create
The attribute does nothing by itself. It is just data. The code at the bottom uses Reflection to find the attributes and decide what to do. A framework does this once to build its list of routes.
The syntax
#[Route("/blog")] // one attribute with an argument
#[Route("/blog"), Cache(60)] // several, separated by commas
#[Attribute(Attribute::TARGET_METHOD)] // limits where an attribute may be used
An attribute is a class. Its arguments go to the class constructor. Named arguments work too.
Limit where it can be used
<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Required {}
class Form
{
#[Required]
public string $name = "";
public string $note = "";
}
$props = (new ReflectionClass(Form::class))->getProperties();
foreach ($props as $prop) {
$required = count($prop->getAttributes(Required::class)) > 0;
echo $prop->getName() . ": " . ($required ? "required" : "optional") . "\n";
}
name: required
note: optional
Built-in attributes
PHP includes a few:
| Attribute | Version | Purpose |
|---|---|---|
#[\ReturnTypeWillChange] | 8.1 | Silences a return type deprecation in older code |
#[\AllowDynamicProperties] | 8.2 | Allows dynamic properties on a class |
#[\SensitiveParameter] | 8.2 | Hides a parameter’s value in stack traces |
#[\Override] | 8.3 | Checks that a method really overrides a parent method |
#[\Deprecated] | 8.4 | Marks your own code as deprecated |
#[\NoDiscard] | 8.5 | Warns when a return value is ignored |
<?php
function login(string $user, #[\SensitiveParameter] string $password): void
{
// if an exception happens here, the password is not printed in the stack trace
}
Attributes vs comments
Before PHP 8, frameworks put metadata in docblock comments (@Route("/blog")) and parsed the text. Attributes are real syntax, so PHP checks them, and tools can read them reliably.
Where you will see them
- Symfony routes and validation rules.
- Laravel has a growing number of attributes.
- Doctrine ORM mappings (
#[ORM\Entity]). - Testing tools (PHPUnit’s
#[Test]).
Older PHP and the # symbol
In older PHP, # starts a comment. #[Route()] is therefore treated as a comment on PHP 7, which is why attributes are backward compatible for simple cases.
Why this matters in Laravel and Symfony
#[Route("/x")] above a method, you now know a class is being read through Reflection.