#[Route('/orders/{id}')] right above the method, and nothing in config/routes. Symfony 5.3 came out last week, and with PHP 8 attributes configuration finally found its place. Routes, autowiring hints, some validation, native syntax next to the code:
#[Route('/orders/{id}', methods: ['GET'])]
public function show(int $id): Response
{
// ...
}
I spent years defending YAML routing. The argument was separation: code is code, wiring is wiring, one file shows the whole URL map. The argument was never wrong. It lost to practice. In every real project the first thing you do with a route is jump to the controller, and the first thing you do with a controller is wonder which route hits it. Two files, one mental join, forever. Attributes remove the join. Rename a method, the metadata moves with it. Delete the class, no orphaned YAML block stays behind to confuse the next person.
Docblock annotations did the same thing, of course. But they were comments pretending to be syntax, with a userland parser and no help from the engine. Attributes are the same idea made legal. Typos fail loudly. IDEs understand them without plugins.
The overview argument still deserves an answer, and the answer is debug:router. A generated map is always current. A hand-maintained one only tries.
One line I try to hold: attributes are for metadata, not for decisions. A route path, a service tag, a validation constraint, yes. The moment an attribute encodes a business rule, which discount, which workflow, it is logic hiding in a declaration. Logic wants to be in code you can test and step through.
We spent a decade moving configuration out of code. Now we move it back. Both directions were called progress. This time I think it is, and I remember thinking the same the last time.