Reflection is slow, and the cache is the whole design

Scanning four hundred classes for attributes took 2.9 seconds, which is fine once at deploy and unacceptable on every request.

// the scan, at build time
$routes = [];

foreach ($classes as $class) {
    $rc = new ReflectionClass($class);

    foreach ($rc->getMethods() as $m) {
        foreach ($m->getAttributes(Route::class) as $a) {
            $route = $a->newInstance();
            $routes[] = [$route->path, $class, $m->getName()];
        }
    }
}

file_put_contents($cache, '<?php return ' . var_export($routes, true) . ';');

Compiling the reflection result to a plain PHP array that opcache can hold is the standard answer and takes the cost from three seconds to microseconds. The hazard it introduces is a stale cache: a route added without a rebuild produces a 404 with no explanation, which is exactly the complaint people had about annotation caches a decade earlier. Making the rebuild part of the deploy rather than a documented step is what prevents it.