An attribute is a class, and reading one is reflection

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.