Attributes are stored as a name and a list of arguments and nothing constructs them until reflection asks, which makes them cheap and makes the validation later than it looks.
#[Route('/orders/{id}', methods: ['GET'])]
public function show(int $id) {}
// this class need not exist for the file to compile,
// or even for getAttributes() to return it:
$attrs = $method->getAttributes(Route::class);
$attrs[0]->getName(); // 'Route' — no autoload yet
$attrs[0]->getArguments(); // the raw arguments
$attrs[0]->newInstance(); // NOW it autoloads, type-checks
// and runs the constructor
The laziness is the feature — an application with attributes it does not read pays nothing for them. It also means the compile-time checking people expect is really instantiation-time checking, so a misspelled argument name in an attribute is silent until something reads it. Anything relying on attributes should therefore have a warm-up or lint step that instantiates every one, in CI, so the failure happens on a machine rather than in a request.