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.