Psr4DirectoryLoader.php 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Routing\Loader;
  11. use Symfony\Component\Config\FileLocatorInterface;
  12. use Symfony\Component\Config\Loader\DirectoryAwareLoaderInterface;
  13. use Symfony\Component\Config\Loader\Loader;
  14. use Symfony\Component\Config\Resource\DirectoryResource;
  15. use Symfony\Component\Routing\Exception\InvalidArgumentException;
  16. use Symfony\Component\Routing\RouteCollection;
  17. /**
  18. * A loader that discovers controller classes in a directory that follows PSR-4.
  19. *
  20. * @author Alexander M. Turek <me@derrabus.de>
  21. */
  22. final class Psr4DirectoryLoader extends Loader implements DirectoryAwareLoaderInterface
  23. {
  24. private ?string $currentDirectory = null;
  25. public function __construct(
  26. private readonly FileLocatorInterface $locator,
  27. ) {
  28. // PSR-4 directory loader has no env-aware logic, so we drop the $env constructor parameter.
  29. parent::__construct();
  30. }
  31. /**
  32. * @param array{path: string, namespace: string} $resource
  33. */
  34. public function load(mixed $resource, ?string $type = null): ?RouteCollection
  35. {
  36. $path = $this->locator->locate($resource['path'], $this->currentDirectory);
  37. if (!is_dir($path)) {
  38. return new RouteCollection();
  39. }
  40. if (!preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\)++$/', trim($resource['namespace'], '\\').'\\')) {
  41. throw new InvalidArgumentException(\sprintf('Namespace "%s" is not a valid PSR-4 prefix.', $resource['namespace']));
  42. }
  43. return $this->loadFromDirectory($path, trim($resource['namespace'], '\\'));
  44. }
  45. public function supports(mixed $resource, ?string $type = null): bool
  46. {
  47. return 'attribute' === $type && \is_array($resource) && isset($resource['path'], $resource['namespace']);
  48. }
  49. public function forDirectory(string $currentDirectory): static
  50. {
  51. $loader = clone $this;
  52. $loader->currentDirectory = $currentDirectory;
  53. return $loader;
  54. }
  55. private function loadFromDirectory(string $directory, string $psr4Prefix): RouteCollection
  56. {
  57. $collection = new RouteCollection();
  58. $collection->addResource(new DirectoryResource($directory, '/\.php$/'));
  59. $files = iterator_to_array(new \RecursiveIteratorIterator(
  60. new \RecursiveCallbackFilterIterator(
  61. new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
  62. fn (\SplFileInfo $current) => !str_starts_with($current->getBasename(), '.')
  63. ),
  64. \RecursiveIteratorIterator::SELF_FIRST
  65. ));
  66. usort($files, fn (\SplFileInfo $a, \SplFileInfo $b) => (string) $a > (string) $b ? 1 : -1);
  67. /** @var \SplFileInfo $file */
  68. foreach ($files as $file) {
  69. if ($file->isDir()) {
  70. $collection->addCollection($this->loadFromDirectory($file->getPathname(), $psr4Prefix.'\\'.$file->getFilename()));
  71. continue;
  72. }
  73. if ('php' !== $file->getExtension() || !class_exists($className = $psr4Prefix.'\\'.$file->getBasename('.php')) || (new \ReflectionClass($className))->isAbstract()) {
  74. continue;
  75. }
  76. $collection->addCollection($this->import($className, 'attribute'));
  77. }
  78. return $collection;
  79. }
  80. }