vendor/symfony/routing/Loader/AnnotationClassLoader.php line 111

Open in your IDE?
  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\Annotation\Route as RouteAnnotation;
  16. use Symfony\Component\Routing\Route;
  17. use Symfony\Component\Routing\RouteCollection;
  18. /**
  19.  * AnnotationClassLoader 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 annotation can be set on the class (for global parameters),
  26.  * and on each method.
  27.  *
  28.  * The @Route annotation main value is the route path. The annotation 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.  *      * /
  35.  *     class Blog
  36.  *     {
  37.  *         /**
  38.  *          * @Route("/", name="blog_index")
  39.  *          * /
  40.  *         public function index()
  41.  *         {
  42.  *         }
  43.  *         /**
  44.  *          * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"})
  45.  *          * /
  46.  *         public function show()
  47.  *         {
  48.  *         }
  49.  *     }
  50.  *
  51.  * On PHP 8, the annotation class can be used as an attribute as well:
  52.  *     #[Route('/Blog')]
  53.  *     class Blog
  54.  *     {
  55.  *         #[Route('/', name: 'blog_index')]
  56.  *         public function index()
  57.  *         {
  58.  *         }
  59.  *         #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])]
  60.  *         public function show()
  61.  *         {
  62.  *         }
  63.  *     }
  64.  *
  65.  * @author Fabien Potencier <fabien@symfony.com>
  66.  * @author Alexander M. Turek <me@derrabus.de>
  67.  */
  68. abstract class AnnotationClassLoader implements LoaderInterface
  69. {
  70.     protected $reader;
  71.     protected $env;
  72.     /**
  73.      * @var string
  74.      */
  75.     protected $routeAnnotationClass RouteAnnotation::class;
  76.     /**
  77.      * @var int
  78.      */
  79.     protected $defaultRouteIndex 0;
  80.     public function __construct(Reader $reader nullstring $env null)
  81.     {
  82.         $this->reader $reader;
  83.         $this->env $env;
  84.     }
  85.     /**
  86.      * Sets the annotation class to read route properties from.
  87.      */
  88.     public function setRouteAnnotationClass(string $class)
  89.     {
  90.         $this->routeAnnotationClass $class;
  91.     }
  92.     /**
  93.      * Loads from annotations from a class.
  94.      *
  95.      * @param string $class A class name
  96.      *
  97.      * @return RouteCollection
  98.      *
  99.      * @throws \InvalidArgumentException When route can't be parsed
  100.      */
  101.     public function load($classstring $type null)
  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('Annotations from class "%s" cannot be read as it is abstract.'$class->getName()));
  109.         }
  110.         $globals $this->getGlobals($class);
  111.         $collection = new RouteCollection();
  112.         $collection->addResource(new FileResource($class->getFileName()));
  113.         if ($globals['env'] && $this->env !== $globals['env']) {
  114.             return $collection;
  115.         }
  116.         foreach ($class->getMethods() as $method) {
  117.             $this->defaultRouteIndex 0;
  118.             foreach ($this->getAnnotations($method) as $annot) {
  119.                 $this->addRoute($collection$annot$globals$class$method);
  120.             }
  121.         }
  122.         if (=== $collection->count() && $class->hasMethod('__invoke')) {
  123.             $globals $this->resetGlobals();
  124.             foreach ($this->getAnnotations($class) as $annot) {
  125.                 $this->addRoute($collection$annot$globals$class$class->getMethod('__invoke'));
  126.             }
  127.         }
  128.         return $collection;
  129.     }
  130.     /**
  131.      * @param RouteAnnotation $annot or an object that exposes a similar interface
  132.      */
  133.     protected function addRoute(RouteCollection $collectionobject $annot, array $globals\ReflectionClass $class\ReflectionMethod $method)
  134.     {
  135.         if ($annot->getEnv() && $annot->getEnv() !== $this->env) {
  136.             return;
  137.         }
  138.         $name $annot->getName();
  139.         if (null === $name) {
  140.             $name $this->getDefaultRouteName($class$method);
  141.         }
  142.         $name $globals['name'].$name;
  143.         $requirements $annot->getRequirements();
  144.         foreach ($requirements as $placeholder => $requirement) {
  145.             if (\is_int($placeholder)) {
  146.                 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()));
  147.             }
  148.         }
  149.         $defaults array_replace($globals['defaults'], $annot->getDefaults());
  150.         $requirements array_replace($globals['requirements'], $requirements);
  151.         $options array_replace($globals['options'], $annot->getOptions());
  152.         $schemes array_merge($globals['schemes'], $annot->getSchemes());
  153.         $methods array_merge($globals['methods'], $annot->getMethods());
  154.         $host $annot->getHost();
  155.         if (null === $host) {
  156.             $host $globals['host'];
  157.         }
  158.         $condition $annot->getCondition() ?? $globals['condition'];
  159.         $priority $annot->getPriority() ?? $globals['priority'];
  160.         $path $annot->getLocalizedPaths() ?: $annot->getPath();
  161.         $prefix $globals['localized_paths'] ?: $globals['path'];
  162.         $paths = [];
  163.         if (\is_array($path)) {
  164.             if (!\is_array($prefix)) {
  165.                 foreach ($path as $locale => $localePath) {
  166.                     $paths[$locale] = $prefix.$localePath;
  167.                 }
  168.             } elseif ($missing array_diff_key($prefix$path)) {
  169.                 throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".'$class->name.'::'.$method->nameimplode('", "'array_keys($missing))));
  170.             } else {
  171.                 foreach ($path as $locale => $localePath) {
  172.                     if (!isset($prefix[$locale])) {
  173.                         throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".'$method->name$locale$class->name));
  174.                     }
  175.                     $paths[$locale] = $prefix[$locale].$localePath;
  176.                 }
  177.             }
  178.         } elseif (\is_array($prefix)) {
  179.             foreach ($prefix as $locale => $localePrefix) {
  180.                 $paths[$locale] = $localePrefix.$path;
  181.             }
  182.         } else {
  183.             $paths[] = $prefix.$path;
  184.         }
  185.         foreach ($method->getParameters() as $param) {
  186.             if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) {
  187.                 continue;
  188.             }
  189.             foreach ($paths as $locale => $path) {
  190.                 if (preg_match(sprintf('/\{%s(?:<.*?>)?\}/'preg_quote($param->name)), $path)) {
  191.                     $defaults[$param->name] = $param->getDefaultValue();
  192.                     break;
  193.                 }
  194.             }
  195.         }
  196.         foreach ($paths as $locale => $path) {
  197.             $route $this->createRoute($path$defaults$requirements$options$host$schemes$methods$condition);
  198.             $this->configureRoute($route$class$method$annot);
  199.             if (!== $locale) {
  200.                 $route->setDefault('_locale'$locale);
  201.                 $route->setRequirement('_locale'preg_quote($locale));
  202.                 $route->setDefault('_canonical_route'$name);
  203.                 $collection->add($name.'.'.$locale$route$priority);
  204.             } else {
  205.                 $collection->add($name$route$priority);
  206.             }
  207.         }
  208.     }
  209.     /**
  210.      * {@inheritdoc}
  211.      */
  212.     public function supports($resourcestring $type null)
  213.     {
  214.         return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/'$resource) && (!$type || 'annotation' === $type);
  215.     }
  216.     /**
  217.      * {@inheritdoc}
  218.      */
  219.     public function setResolver(LoaderResolverInterface $resolver)
  220.     {
  221.     }
  222.     /**
  223.      * {@inheritdoc}
  224.      */
  225.     public function getResolver()
  226.     {
  227.     }
  228.     /**
  229.      * Gets the default route name for a class method.
  230.      *
  231.      * @return string
  232.      */
  233.     protected function getDefaultRouteName(\ReflectionClass $class\ReflectionMethod $method)
  234.     {
  235.         $name str_replace('\\''_'$class->name).'_'.$method->name;
  236.         $name \function_exists('mb_strtolower') && preg_match('//u'$name) ? mb_strtolower($name'UTF-8') : strtolower($name);
  237.         if ($this->defaultRouteIndex 0) {
  238.             $name .= '_'.$this->defaultRouteIndex;
  239.         }
  240.         ++$this->defaultRouteIndex;
  241.         return $name;
  242.     }
  243.     protected function getGlobals(\ReflectionClass $class)
  244.     {
  245.         $globals $this->resetGlobals();
  246.         $annot null;
  247.         if (\PHP_VERSION_ID >= 80000 && ($attribute $class->getAttributes($this->routeAnnotationClass\ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)) {
  248.             $annot $attribute->newInstance();
  249.         }
  250.         if (!$annot && $this->reader) {
  251.             $annot $this->reader->getClassAnnotation($class$this->routeAnnotationClass);
  252.         }
  253.         if ($annot) {
  254.             if (null !== $annot->getName()) {
  255.                 $globals['name'] = $annot->getName();
  256.             }
  257.             if (null !== $annot->getPath()) {
  258.                 $globals['path'] = $annot->getPath();
  259.             }
  260.             $globals['localized_paths'] = $annot->getLocalizedPaths();
  261.             if (null !== $annot->getRequirements()) {
  262.                 $globals['requirements'] = $annot->getRequirements();
  263.             }
  264.             if (null !== $annot->getOptions()) {
  265.                 $globals['options'] = $annot->getOptions();
  266.             }
  267.             if (null !== $annot->getDefaults()) {
  268.                 $globals['defaults'] = $annot->getDefaults();
  269.             }
  270.             if (null !== $annot->getSchemes()) {
  271.                 $globals['schemes'] = $annot->getSchemes();
  272.             }
  273.             if (null !== $annot->getMethods()) {
  274.                 $globals['methods'] = $annot->getMethods();
  275.             }
  276.             if (null !== $annot->getHost()) {
  277.                 $globals['host'] = $annot->getHost();
  278.             }
  279.             if (null !== $annot->getCondition()) {
  280.                 $globals['condition'] = $annot->getCondition();
  281.             }
  282.             $globals['priority'] = $annot->getPriority() ?? 0;
  283.             $globals['env'] = $annot->getEnv();
  284.             foreach ($globals['requirements'] as $placeholder => $requirement) {
  285.                 if (\is_int($placeholder)) {
  286.                     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()));
  287.                 }
  288.             }
  289.         }
  290.         return $globals;
  291.     }
  292.     private function resetGlobals(): array
  293.     {
  294.         return [
  295.             'path' => null,
  296.             'localized_paths' => [],
  297.             'requirements' => [],
  298.             'options' => [],
  299.             'defaults' => [],
  300.             'schemes' => [],
  301.             'methods' => [],
  302.             'host' => '',
  303.             'condition' => '',
  304.             'name' => '',
  305.             'priority' => 0,
  306.             'env' => null,
  307.         ];
  308.     }
  309.     protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition)
  310.     {
  311.         return new Route($path$defaults$requirements$options$host$schemes$methods$condition);
  312.     }
  313.     abstract protected function configureRoute(Route $route\ReflectionClass $class\ReflectionMethod $methodobject $annot);
  314.     /**
  315.      * @param \ReflectionClass|\ReflectionMethod $reflection
  316.      *
  317.      * @return iterable<int, RouteAnnotation>
  318.      */
  319.     private function getAnnotations(object $reflection): iterable
  320.     {
  321.         if (\PHP_VERSION_ID >= 80000) {
  322.             foreach ($reflection->getAttributes($this->routeAnnotationClass\ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
  323.                 yield $attribute->newInstance();
  324.             }
  325.         }
  326.         if (!$this->reader) {
  327.             return;
  328.         }
  329.         $anntotations $reflection instanceof \ReflectionClass
  330.             $this->reader->getClassAnnotations($reflection)
  331.             : $this->reader->getMethodAnnotations($reflection);
  332.         foreach ($anntotations as $annotation) {
  333.             if ($annotation instanceof $this->routeAnnotationClass) {
  334.                 yield $annotation;
  335.             }
  336.         }
  337.     }
  338. }