diff --git a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php index b63dc5c85e665..fbf7b0105cd51 100644 --- a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php +++ b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php @@ -106,11 +106,14 @@ public function setRouteAnnotationClass(string $class) */ public function load(mixed $class, string $type = null): RouteCollection { - if (!class_exists($class)) { - throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + if (!$class instanceof \ReflectionClass) { + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + } + + $class = new \ReflectionClass($class); } - $class = new \ReflectionClass($class); if ($class->isAbstract()) { throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class->getName())); } @@ -227,7 +230,7 @@ protected function addRoute(RouteCollection $collection, object $annot, array $g public function supports(mixed $resource, string $type = null): bool { - return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || \in_array($type, ['annotation', 'attribute'], true)); + return ($resource instanceof \ReflectionClass || \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource)) && (!$type || \in_array($type, ['annotation', 'attribute'], true)); } public function setResolver(LoaderResolverInterface $resolver) diff --git a/src/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php index 258673593625b..a27f744de348d 100644 --- a/src/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php +++ b/src/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php @@ -51,9 +51,9 @@ function (\SplFileInfo $current) { continue; } - if ($class = $this->findClass($file)) { - $refl = new \ReflectionClass($class); - if ($refl->isAbstract()) { + if ($className = $this->findClass($file)) { + $class = new \ReflectionClass($className); + if ($class->isAbstract()) { continue; } diff --git a/src/Symfony/Component/Routing/Loader/Psr4DirectoryLoader.php b/src/Symfony/Component/Routing/Loader/Psr4DirectoryLoader.php index 9f262c006ffc2..433ce702afa8c 100644 --- a/src/Symfony/Component/Routing/Loader/Psr4DirectoryLoader.php +++ b/src/Symfony/Component/Routing/Loader/Psr4DirectoryLoader.php @@ -43,7 +43,7 @@ public function load(mixed $resource, string $type = null): ?RouteCollection return new RouteCollection(); } - return $this->loadFromDirectory($path, trim($resource['namespace'], '\\')); + return $this->loadFromDirectory($path, trim($resource['namespace'], '\\'), $type); } public function supports(mixed $resource, string $type = null): bool @@ -59,7 +59,7 @@ public function forDirectory(string $currentDirectory): static return $loader; } - private function loadFromDirectory(string $directory, string $psr4Prefix): RouteCollection + private function loadFromDirectory(string $directory, string $psr4Prefix, string $type): RouteCollection { $collection = new RouteCollection(); $collection->addResource(new DirectoryResource($directory, '/\.php$/')); @@ -79,7 +79,7 @@ function (\SplFileInfo $current) { /** @var \SplFileInfo $file */ foreach ($files as $file) { if ($file->isDir()) { - $collection->addCollection($this->loadFromDirectory($file->getPathname(), $psr4Prefix.'\\'.$file->getFilename())); + $collection->addCollection($this->loadFromDirectory($file->getPathname(), $psr4Prefix.'\\'.$file->getFilename(), $type)); continue; } @@ -87,7 +87,12 @@ function (\SplFileInfo $current) { continue; } - $collection->addCollection($this->import($className, 'attribute')); + $class = new \ReflectionClass($className); + if ($class->isAbstract()) { + continue; + } + + $collection->addCollection($this->import($class, $type)); } return $collection; diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/IrrelevantClass.php b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/IrrelevantClass.php new file mode 100644 index 0000000000000..ca3c1bcbddcaf --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/IrrelevantClass.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace; + +use Symfony\Component\HttpFoundation\Response; + +/** + * An irrelevant class. + * + * This fixture is not referenced anywhere. Its presence makes sure, classes without attributes are silently ignored + * when loading routes from a directory. + */ +final class IrrelevantClass +{ + public function irrelevantAction(): Response + { + return new Response(status: Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/MyAbstractController.php b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/MyAbstractController.php new file mode 100644 index 0000000000000..30d8bbdb65bcc --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/MyAbstractController.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +abstract class MyAbstractController +{ + #[Route('/a/route/from/an/abstract/controller', name: 'from_abstract')] + public function someAction(): Response + { + return new Response(status: Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/MyChildController.php b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/MyChildController.php new file mode 100644 index 0000000000000..a6d0333577079 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Psr4Controllers/SubNamespace/MyChildController.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace; + +use Symfony\Component\Routing\Annotation\Route; + +#[Route('/my/child/controller', name: 'my_child_controller_')] +final class MyChildController extends MyAbstractController +{ +} diff --git a/src/Symfony/Component/Routing/Tests/Loader/Psr4DirectoryLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/Psr4DirectoryLoaderTest.php index 541d352746c90..2bae59005fa60 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/Psr4DirectoryLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/Psr4DirectoryLoaderTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController; use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace\EvenDeeperNamespace\MyOtherController; +use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace\MyChildController; use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace\MyControllerWithATrait; class Psr4DirectoryLoaderTest extends TestCase @@ -56,6 +57,14 @@ public function testTraitController() $this->assertSame(MyControllerWithATrait::class.'::someAction', $route->getDefault('_controller')); } + public function testAbstractController() + { + $route = $this->loadPsr4Controllers()->get('my_child_controller_from_abstract'); + + $this->assertSame('/my/child/controller/a/route/from/an/abstract/controller', $route->getPath()); + $this->assertSame(MyChildController::class.'::someAction', $route->getDefault('_controller')); + } + /** * @dataProvider provideNamespacesThatNeedTrimming */