PHP Attributes
What Are Attributes? In simple terms: Attributes are labels you attach to classes, methods, properties, parameters, functions, or constants to add extra information about them. The label itself does n
What Are Attributes?
In simple terms: Attributes are labels you attach to classes, methods, properties, parameters, functions, or constants to add extra information about them. The label itself does nothing; other code — PHP itself, a framework, or your own code — can read it and act on it. For example, #[Route('/products')] tells a framework which URL a controller method handles, and #[Email] on a property tells a validator what to check.
Technically: Attributes (PHP 8.0+) are structured, machine-readable metadata. They are written as #[AttributeName(arguments)] directly before a declaration. Each attribute name refers to a class marked with #[Attribute]. At runtime, the Reflection API returns the attributes of a declaration, and newInstance() creates an object of the attribute class with the given arguments.
Why Attributes Exist
Before PHP 8, frameworks put such metadata in docblock comments (/** @Route("/products") */), which had to be parsed as text, were not validated by PHP, and could contain typos that went unnoticed. Attributes are part of the language syntax: PHP parses them, they refer to real classes, and IDEs understand them.
Syntax
#[AttributeName]
#[AttributeName(argument1, named: value)]
#[First, Second(10)] // several attributes in one block
Attributes can be placed before:
| Target | Example |
|---|---|
| Classes, interfaces, traits, enums | #[Entity] final class Product |
| Methods and functions | #[Route('/products')] public function index() |
| Properties and class constants | #[Column(length: 100)] public string $name; |
| Parameters (including promoted ones) | public function __construct(#[Email] public string $email) |
Built-In Attributes
PHP itself understands several attributes:
| Attribute | Since | Effect |
|---|---|---|
#[\Override] | 8.3 | Error if the method does not override a parent or interface method (see Method Overriding) |
#[\SensitiveParameter] | 8.2 | Hides the parameter's value in stack traces (passwords, API keys) |
#[\Deprecated] | 8.4 | Calling the function, method, or class constant emits a deprecation notice |
#[\AllowDynamicProperties] | 8.2 | Allows dynamic properties on a class (see PHP Properties) |
#[\ReturnTypeWillChange] | 8.1 | Silences return-type deprecation notices when extending internal classes |
#[\NoDiscard] | 8.5 | Warns when the return value of a function is not used |
Example: Protecting Secrets and Marking Old APIs
<?php
function connectToPaymentGateway(string $merchantId, #[\SensitiveParameter] string $apiKey): void
{
throw new RuntimeException('Gateway unreachable');
}
final class PriceCalculator
{
#[\Deprecated(message: 'use priceWithTax() instead', since: '2.0')]
public function grossPrice(float $net): float
{
return $this->priceWithTax($net);
}
public function priceWithTax(float $net): float
{
return round($net * 1.18, 2);
}
}
- If
connectToPaymentGateway()throws, stack traces and error logs show the API key as a placeholder object (SensitiveParameterValue) instead of the real key — an important protection, because stack traces often end up in log files and monitoring tools. - Calling
grossPrice()triggers anE_USER_DEPRECATEDnotice containing the message and version, which helps teams migrate away from old methods.
Creating Your Own Attributes
An attribute is a normal class marked with the built-in #[Attribute] attribute. Its constructor parameters define the arguments it accepts:
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Length
{
public function __construct(public int $min, public int $max) {}
}
| Flag | Allows the Attribute On |
|---|---|
Attribute::TARGET_CLASS | Classes |
Attribute::TARGET_METHOD | Methods |
Attribute::TARGET_PROPERTY | Properties |
Attribute::TARGET_PARAMETER | Parameters |
Attribute::TARGET_FUNCTION, TARGET_CLASS_CONSTANT | Functions, class constants |
Attribute::TARGET_ALL | Everything (default) |
Attribute::IS_REPEATABLE | Using the same attribute several times on one declaration |
Flags are combined with |, for example Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE.
Practical Example: A Validator Driven by Attributes
<?php
interface Rule
{
public function check(mixed $value): ?string;
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Length implements Rule
{
public function __construct(public int $min, public int $max) {}
public function check(mixed $value): ?string
{
$length = mb_strlen((string) $value);
return $length < $this->min || $length > $this->max
? "must be {$this->min}–{$this->max} characters"
: null;
}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Email implements Rule
{
public function check(mixed $value): ?string
{
return filter_var($value, FILTER_VALIDATE_EMAIL) === false ? 'must be a valid email address' : null;
}
}
final class SignupRequest
{
public function __construct(
#[Length(min: 2, max: 30)]
public string $name,
#[Email]
public string $email,
#[Length(min: 8, max: 64)]
public string $password,
) {}
}
function validate(object $object): array
{
$errors = [];
foreach ((new ReflectionObject($object))->getProperties() as $property) {
foreach ($property->getAttributes(Rule::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$message = $attribute->newInstance()->check($property->getValue($object));
if ($message !== null) {
$errors[$property->getName()][] = $message;
}
}
}
return $errors;
}
print_r(validate(new SignupRequest('A', 'asha@', 'secret123')));
var_dump(validate(new SignupRequest('Asha', 'asha@example.com', 'correct-horse-42')) === []);
Output
Array
(
[name] => Array
(
[0] => must be 2–30 characters
)
[email] => Array
(
[0] => must be a valid email address
)
)
bool(true)
Step-by-Step Explanation
LengthandEmailare attribute classes. They also implement aRuleinterface, so the validator can treat all rules the same way.SignupRequestdeclares its rules next to the properties they apply to. Attributes on promoted constructor parameters apply to the generated properties as well.validate()uses Reflection:getAttributes(Rule::class, ReflectionAttribute::IS_INSTANCEOF)returns all attributes whose class implementsRule.newInstance()creates the attribute object with its arguments (min: 2, max: 30), which then checks the property's value.- Adding a new rule (for example,
#[Matches('/^[A-Z]/')]) only requires a new attribute class — the validator does not change.
This is exactly how Symfony's Validator (#[Assert\Email]), Doctrine's ORM mapping (#[ORM\Column]), and routing attributes (#[Route('/products')]) work.
When Are Attributes Evaluated?
- PHP parses attributes when the file is compiled, but attribute classes are not instantiated until code calls
newInstance(). - The attribute class does not even need to exist until then. Errors such as a missing class or a wrong target are reported only when
newInstance()is called. - Reflection is relatively expensive; frameworks typically read attributes once and cache the results.
Common Mistakes
Expecting Attributes to Do Something on Their Own
Effect: Adding #[Email] without a validator that reads it has no effect.
Fix: Attributes are metadata; some code must read and act on them.
Forgetting #[Attribute] on the Attribute Class
Effect: newInstance() throws an error ("Attempting to use non-attribute class ... as attribute").
Fix: Mark every attribute class with #[Attribute].
Wrong Target
Effect: newInstance() throws when an attribute is used on a declaration its flags do not allow.
Fix: Set the correct TARGET_* flags.
Putting Logic in Attributes That Belongs in Code
Effect: Behavior hidden in metadata is hard to follow and test.
Fix: Use attributes for declarative configuration; keep business logic in services.
Best Practices
- Use built-in attributes:
#[\Override],#[\SensitiveParameter], and#[\Deprecated]. - Use attributes for declarative configuration: validation rules, routes, ORM mapping, serialization.
- Restrict attribute targets and keep attribute classes small and immutable.
- Read attributes through Reflection in one place and cache the results.
- Prefer attributes over docblock annotations in modern code.
Official reference: https\://www.php.net/manual/en/language.attributes.php