vendor/symfony/routing/Loader/AttributeClassLoader.php line 86

  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 Doctrine\Common\Annotations\Reader;
  12. use Symfony\Component\Config\Loader\LoaderInterface;
  13. use Symfony\Component\Config\Loader\LoaderResolverInterface;
  14. use Symfony\Component\Config\Resource\FileResource;
  15. use Symfony\Component\Routing\Attribute\Route as RouteAnnotation;
  16. use Symfony\Component\Routing\Route;
  17. use Symfony\Component\Routing\RouteCollection;
  18. /**
  19.  * AttributeClassLoader loads routing information from a PHP class and its methods.
  20.  *
  21.  * You need to define an implementation for the configureRoute() method. Most of the
  22.  * time, this method should define some PHP callable to be called for the route
  23.  * (a controller in MVC speak).
  24.  *
  25.  * The #[Route] attribute can be set on the class (for global parameters),
  26.  * and on each method.
  27.  *
  28.  * The #[Route] attribute main value is the route path. The attribute also
  29.  * recognizes several parameters: requirements, options, defaults, schemes,
  30.  * methods, host, and name. The name parameter is mandatory.
  31.  * Here is an example of how you should be able to use it:
  32.  *
  33.  *     #[Route('/Blog')]
  34.  *     class Blog
  35.  *     {
  36.  *         #[Route('/', name: 'blog_index')]
  37.  *         public function index()
  38.  *         {
  39.  *         }
  40.  *         #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])]
  41.  *         public function show()
  42.  *         {
  43.  *         }
  44.  *     }
  45.  *
  46.  * @author Fabien Potencier <fabien@symfony.com>
  47.  * @author Alexander M. Turek <me@derrabus.de>
  48.  * @author Alexandre Daubois <alex.daubois@gmail.com>
  49.  */
  50. abstract class AttributeClassLoader implements LoaderInterface
  51. {
  52.     /**
  53.      * @var Reader|null
  54.      *
  55.      * @deprecated in Symfony 6.4, this property will be removed in Symfony 7.
  56.      */
  57.     protected $reader;
  58.     /**
  59.      * @var string|null
  60.      */
  61.     protected $env;
  62.     /**
  63.      * @var string
  64.      */
  65.     protected $routeAnnotationClass RouteAnnotation::class;
  66.     /**
  67.      * @var int
  68.      */
  69.     protected $defaultRouteIndex 0;
  70.     private bool $hasDeprecatedAnnotations false;
  71.     /**
  72.      * @param string|null $env
  73.      */
  74.     public function __construct($env null)
  75.     {
  76.         if ($env instanceof Reader || null === $env && \func_num_args() > && null !== func_get_arg(1)) {
  77.             trigger_deprecation('symfony/routing''6.4''Passing an instance of "%s" as first and the environment as second argument to "%s" is deprecated. Pass the environment as first argument instead.'Reader::class, __METHOD__);
  78.             $this->reader $env;
  79.             $env \func_num_args() > func_get_arg(1) : null;
  80.         }
  81.         if (\is_string($env) || null === $env) {
  82.             $this->env $env;
  83.         } elseif ($env instanceof \Stringable || \is_scalar($env)) {
  84.             $this->env = (string) $env;
  85.         } else {
  86.             throw new \TypeError(__METHOD__.sprintf(': Parameter $env was expected to be a string or null, "%s" given.'get_debug_type($env)));
  87.         }
  88.     }
  89.     /**
  90.      * Sets the annotation class to read route properties from.
  91.      *
  92.      * @return void
  93.      */
  94.     public function setRouteAnnotationClass(string $class)
  95.     {
  96.         $this->routeAnnotationClass $class;
  97.     }
  98.     /**
  99.      * @throws \InvalidArgumentException When route can't be parsed
  100.      */
  101.     public function load(mixed $class, ?string $type null): RouteCollection
  102.     {
  103.         if (!class_exists($class)) {
  104.             throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.'$class));
  105.         }
  106.         $class = new \ReflectionClass($class);
  107.         if ($class->isAbstract()) {
  108.             throw new \InvalidArgumentException(sprintf('Attributes from class "%s" cannot be read as it is abstract.'$class->getName()));
  109.         }
  110.         $this->hasDeprecatedAnnotations false;
  111.         try {
  112.             $globals $this->getGlobals($class);
  113.             $collection = new RouteCollection();
  114.             $collection->addResource(new FileResource($class->getFileName()));
  115.             if ($globals['env'] && $this->env !== $globals['env']) {
  116.                 return $collection;
  117.             }
  118.             $fqcnAlias false;
  119.             foreach ($class->getMethods() as $method) {
  120.                 $this->defaultRouteIndex 0;
  121.                 $routeNamesBefore array_keys($collection->all());
  122.                 foreach ($this->getAnnotations($method) as $annot) {
  123.                     $this->addRoute($collection$annot$globals$class$method);
  124.                     if ('__invoke' === $method->name) {
  125.                         $fqcnAlias true;
  126.                     }
  127.                 }
  128.                 if (=== $collection->count() - \count($routeNamesBefore)) {
  129.                     $newRouteName current(array_diff(array_keys($collection->all()), $routeNamesBefore));
  130.                     if ($newRouteName !== $aliasName sprintf('%s::%s'$class->name$method->name)) {
  131.                         $collection->addAlias($aliasName$newRouteName);
  132.                     }
  133.                 }
  134.             }
  135.             if (=== $collection->count() && $class->hasMethod('__invoke')) {
  136.                 $globals $this->resetGlobals();
  137.                 foreach ($this->getAnnotations($class) as $annot) {
  138.                     $this->addRoute($collection$annot$globals$class$class->getMethod('__invoke'));
  139.                     $fqcnAlias true;
  140.                 }
  141.             }
  142.             if ($fqcnAlias && === $collection->count()) {
  143.                 $invokeRouteName key($collection->all());
  144.                 if ($invokeRouteName !== $class->name) {
  145.                     $collection->addAlias($class->name$invokeRouteName);
  146.                 }
  147.                 if ($invokeRouteName !== $aliasName sprintf('%s::__invoke'$class->name)) {
  148.                     $collection->addAlias($aliasName$invokeRouteName);
  149.                 }
  150.             }
  151.             if ($this->hasDeprecatedAnnotations) {
  152.                 trigger_deprecation('symfony/routing''6.4''Class "%s" uses Doctrine Annotations to configure routes, which is deprecated. Use PHP attributes instead.'$class->getName());
  153.             }
  154.         } finally {
  155.             $this->hasDeprecatedAnnotations false;
  156.         }
  157.         return $collection;
  158.     }
  159.     /**
  160.      * @param RouteAnnotation $annot or an object that exposes a similar interface
  161.      *
  162.      * @return void
  163.      */
  164.     protected function addRoute(RouteCollection $collectionobject $annot, array $globals\ReflectionClass $class\ReflectionMethod $method)
  165.     {
  166.         if ($annot->getEnv() && $annot->getEnv() !== $this->env) {
  167.             return;
  168.         }
  169.         $name $annot->getName() ?? $this->getDefaultRouteName($class$method);
  170.         $name $globals['name'].$name;
  171.         $requirements $annot->getRequirements();
  172.         foreach ($requirements as $placeholder => $requirement) {
  173.             if (\is_int($placeholder)) {
  174.                 throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s::%s()"?'$placeholder$requirement$name$class->getName(), $method->getName()));
  175.             }
  176.         }
  177.         $defaults array_replace($globals['defaults'], $annot->getDefaults());
  178.         $requirements array_replace($globals['requirements'], $requirements);
  179.         $options array_replace($globals['options'], $annot->getOptions());
  180.         $schemes array_unique(array_merge($globals['schemes'], $annot->getSchemes()));
  181.         $methods array_unique(array_merge($globals['methods'], $annot->getMethods()));
  182.         $host $annot->getHost() ?? $globals['host'];
  183.         $condition $annot->getCondition() ?? $globals['condition'];
  184.         $priority $annot->getPriority() ?? $globals['priority'];
  185.         $path $annot->getLocalizedPaths() ?: $annot->getPath();
  186.         $prefix $globals['localized_paths'] ?: $globals['path'];
  187.         $paths = [];
  188.         if (\is_array($path)) {
  189.             if (!\is_array($prefix)) {
  190.                 foreach ($path as $locale => $localePath) {
  191.                     $paths[$locale] = $prefix.$localePath;
  192.                 }
  193.             } elseif ($missing array_diff_key($prefix$path)) {
  194.                 throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".'$class->name.'::'.$method->nameimplode('", "'array_keys($missing))));
  195.             } else {
  196.                 foreach ($path as $locale => $localePath) {
  197.                     if (!isset($prefix[$locale])) {
  198.                         throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".'$method->name$locale$class->name));
  199.                     }
  200.                     $paths[$locale] = $prefix[$locale].$localePath;
  201.                 }
  202.             }
  203.         } elseif (\is_array($prefix)) {
  204.             foreach ($prefix as $locale => $localePrefix) {
  205.                 $paths[$locale] = $localePrefix.$path;
  206.             }
  207.         } else {
  208.             $paths[] = $prefix.$path;
  209.         }
  210.         foreach ($method->getParameters() as $param) {
  211.             if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) {
  212.                 continue;
  213.             }
  214.             foreach ($paths as $locale => $path) {
  215.                 if (preg_match(sprintf('/\{%s(?:<.*?>)?\}/'preg_quote($param->name)), $path)) {
  216.                     if (\is_scalar($defaultValue $param->getDefaultValue()) || null === $defaultValue) {
  217.                         $defaults[$param->name] = $defaultValue;
  218.                     } elseif ($defaultValue instanceof \BackedEnum) {
  219.                         $defaults[$param->name] = $defaultValue->value;
  220.                     }
  221.                     break;
  222.                 }
  223.             }
  224.         }
  225.         foreach ($paths as $locale => $path) {
  226.             $route $this->createRoute($path$defaults$requirements$options$host$schemes$methods$condition);
  227.             $this->configureRoute($route$class$method$annot);
  228.             if (!== $locale) {
  229.                 $route->setDefault('_locale'$locale);
  230.                 $route->setRequirement('_locale'preg_quote($locale));
  231.                 $route->setDefault('_canonical_route'$name);
  232.                 $collection->add($name.'.'.$locale$route$priority);
  233.             } else {
  234.                 $collection->add($name$route$priority);
  235.             }
  236.         }
  237.     }
  238.     public function supports(mixed $resource, ?string $type null): bool
  239.     {
  240.         if ('annotation' === $type) {
  241.             trigger_deprecation('symfony/routing''6.4''The "annotation" route type is deprecated, use the "attribute" route type instead.');
  242.         }
  243.         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));
  244.     }
  245.     public function setResolver(LoaderResolverInterface $resolver): void
  246.     {
  247.     }
  248.     public function getResolver(): LoaderResolverInterface
  249.     {
  250.     }
  251.     /**
  252.      * Gets the default route name for a class method.
  253.      *
  254.      * @return string
  255.      */
  256.     protected function getDefaultRouteName(\ReflectionClass $class\ReflectionMethod $method)
  257.     {
  258.         $name str_replace('\\''_'$class->name).'_'.$method->name;
  259.         $name \function_exists('mb_strtolower') && preg_match('//u'$name) ? mb_strtolower($name'UTF-8') : strtolower($name);
  260.         if ($this->defaultRouteIndex 0) {
  261.             $name .= '_'.$this->defaultRouteIndex;
  262.         }
  263.         ++$this->defaultRouteIndex;
  264.         return $name;
  265.     }
  266.     /**
  267.      * @return array<string, mixed>
  268.      */
  269.     protected function getGlobals(\ReflectionClass $class)
  270.     {
  271.         $globals $this->resetGlobals();
  272.         $annot null;
  273.         if ($attribute $class->getAttributes($this->routeAnnotationClass\ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
  274.             $annot $attribute->newInstance();
  275.         }
  276.         if (!$annot && $annot $this->reader?->getClassAnnotation($class$this->routeAnnotationClass)) {
  277.             $this->hasDeprecatedAnnotations true;
  278.         }
  279.         if ($annot) {
  280.             if (null !== $annot->getName()) {
  281.                 $globals['name'] = $annot->getName();
  282.             }
  283.             if (null !== $annot->getPath()) {
  284.                 $globals['path'] = $annot->getPath();
  285.             }
  286.             $globals['localized_paths'] = $annot->getLocalizedPaths();
  287.             if (null !== $annot->getRequirements()) {
  288.                 $globals['requirements'] = $annot->getRequirements();
  289.             }
  290.             if (null !== $annot->getOptions()) {
  291.                 $globals['options'] = $annot->getOptions();
  292.             }
  293.             if (null !== $annot->getDefaults()) {
  294.                 $globals['defaults'] = $annot->getDefaults();
  295.             }
  296.             if (null !== $annot->getSchemes()) {
  297.                 $globals['schemes'] = $annot->getSchemes();
  298.             }
  299.             if (null !== $annot->getMethods()) {
  300.                 $globals['methods'] = $annot->getMethods();
  301.             }
  302.             if (null !== $annot->getHost()) {
  303.                 $globals['host'] = $annot->getHost();
  304.             }
  305.             if (null !== $annot->getCondition()) {
  306.                 $globals['condition'] = $annot->getCondition();
  307.             }
  308.             $globals['priority'] = $annot->getPriority() ?? 0;
  309.             $globals['env'] = $annot->getEnv();
  310.             foreach ($globals['requirements'] as $placeholder => $requirement) {
  311.                 if (\is_int($placeholder)) {
  312.                     throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" in "%s"?'$placeholder$requirement$class->getName()));
  313.                 }
  314.             }
  315.         }
  316.         return $globals;
  317.     }
  318.     private function resetGlobals(): array
  319.     {
  320.         return [
  321.             'path' => null,
  322.             'localized_paths' => [],
  323.             'requirements' => [],
  324.             'options' => [],
  325.             'defaults' => [],
  326.             'schemes' => [],
  327.             'methods' => [],
  328.             'host' => '',
  329.             'condition' => '',
  330.             'name' => '',
  331.             'priority' => 0,
  332.             'env' => null,
  333.         ];
  334.     }
  335.     /**
  336.      * @return Route
  337.      */
  338.     protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition)
  339.     {
  340.         return new Route($path$defaults$requirements$options$host$schemes$methods$condition);
  341.     }
  342.     /**
  343.      * @return void
  344.      */
  345.     abstract protected function configureRoute(Route $route\ReflectionClass $class\ReflectionMethod $methodobject $annot);
  346.     /**
  347.      * @return iterable<int, RouteAnnotation>
  348.      */
  349.     private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection): iterable
  350.     {
  351.         foreach ($reflection->getAttributes($this->routeAnnotationClass\ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
  352.             yield $attribute->newInstance();
  353.         }
  354.         if (!$this->reader) {
  355.             return;
  356.         }
  357.         $annotations $reflection instanceof \ReflectionClass
  358.             $this->reader->getClassAnnotations($reflection)
  359.             : $this->reader->getMethodAnnotations($reflection);
  360.         foreach ($annotations as $annotation) {
  361.             if ($annotation instanceof $this->routeAnnotationClass) {
  362.                 $this->hasDeprecatedAnnotations true;
  363.                 yield $annotation;
  364.             }
  365.         }
  366.     }
  367. }
  368. if (!class_exists(AnnotationClassLoader::class, false)) {
  369.     class_alias(AttributeClassLoader::class, AnnotationClassLoader::class);
  370. }