An attribute (PHP 8.0) is metadata written #[Name(args)] before a declaration. Any class marked #[Attribute] can be one, and its flags limit where it may go. The engine only records attributes; code reads them through Reflection (Subsection 4.9.13), and newInstance() runs the attribute's constructor. PHP 8.5 lets attributes target global const constants, read through ReflectionConstant.
<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Column { public function __construct(public string $name, public bool $key = false) {} }
#[Attribute(Attribute::TARGET_CONSTANT)]
class Since { public function __construct(public string $version) {} }
final class Product {
#[Column('id', key: true)] public int $id = 0;
#[Column('sku')] public string $sku = '';
}
#[Since('1.4')]
const SHOP_API = 'v2'; // PHP 8.5: attributes on constants
foreach (new ReflectionClass(Product::class)->getProperties() as $prop) {
foreach ($prop->getAttributes(Column::class) as $attr) {
$col = $attr->newInstance(); // the constructor runs only now
echo $prop->name, ' -> ', $col->name, $col->key ? ' (primary key)' : '', "\n";
}
}
$since = new ReflectionConstant('SHOP_API')->getAttributes(Since::class)[0]->newInstance();
echo 'SHOP_API since ', $since->version, "\n";id -> id (primary key) sku -> sku SHOP_API since 1.4
Your own attribute's target is checked only at newInstance(): #[Column] on a class compiles, then fails with "Attribute "Column" cannot target class (allowed targets: property)". PHP's own attributes are checked at compile time, and 8.5's #[\DelayedTargetValidation] defers that for libraries that must also load on versions where a target is not yet valid.