Docblock annotations were parsed from a comment by a library, which meant a typo was silent and an IDE could not help. An attribute is a real class the engine parses.
#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
public function __construct(
public string $path,
public string $method = 'GET',
) {}
}
#[Route('/orders/{id}', method: 'GET')]
public function show(int $id): Response {}
// reading it, with no library at all
$attrs = (new ReflectionMethod($c, 'show'))->getAttributes(Route::class);
$route = $attrs[0]->newInstance();
newInstance() is what actually constructs it, and until that is called the arguments are unvalidated — so a wrong type is an error at read time rather than at parse time. The syntax was chosen so that older PHP treats it as a comment, which means a library can ship attributes and docblocks together during a transition. Arguments must be constant expressions, so an attribute cannot contain a computed value.