#[Route('/orders/{id}', methods: ['GET'])]
public function show(int $id): Response

Symfony 5.2 accepts this on PHP 8, and the annotation era quietly starts to end. Same shape as the annotation, but now it is language, not a comment. The engine parses it, static analysis sees it, a typo is a compile-time complaint instead of a route that silently does not exist. Doctrine and the validator are heading the same way. Everything that lived in docblocks will move over the next year or two.

I am for it, with one old scar. We had this debate before, annotations versus YAML config, and the annotation side won on convenience. The cost showed up later: behavior scattered across a hundred class files, no way to see the whole picture. Where is the full route table? Everywhere. Which entities cascade deletes? Open each one and read the comments. Attributes inherit this problem exactly. They just make the scattered metadata syntactically respectable.

So the question is what belongs next to the class at all. My current line: metadata that describes the class itself lives on the class. Validation constraints on a DTO, serialization groups, the mapping of a property to a column. This information has no other natural home, and pulling it into config only separates it from the thing it describes.

Decisions about the application boundary stay out of the class. Which routes are public and which sit behind auth, rate limits, firewall rules, anything security shaped. I want one file that answers “what can the outside world reach”. An archaeology session across controllers does not answer it. A pentester reads one file. So should you.

Convenient syntax makes it tempting to hang everything on the class. I already caught myself wanting the firewall rule next to the action this week. Resist a little.