Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. AttributeClassLoader.php

class AttributeClassLoader

AttributeClassLoader loads routing information from a PHP class and its methods.

You need to define an implementation for the configureRoute() method. Most of the time, this method should define some PHP callable to be called for the route (a controller in MVC speak).

The #[Route] attribute can be set on the class (for global parameters), and on each method.

The #[Route] attribute main value is the route path. The attribute also recognizes several parameters: requirements, options, defaults, schemes, methods, host, and name. The name parameter is mandatory. Here is an example of how you should be able to use it:

#[Route('/Blog')] class Blog { #[Route('/', name: 'blog_index')] public function index() { } #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])] public function show() { } }

@author Fabien Potencier <fabien@symfony.com> @author Alexander M. Turek <me@derrabus.de> @author Alexandre Daubois <alex.daubois@gmail.com>

Hierarchy

  • class \Symfony\Component\Routing\Loader\AttributeClassLoader implements \Symfony\Component\Config\Loader\LoaderInterface

Expanded class hierarchy of AttributeClassLoader

File

vendor/symfony/routing/Loader/AttributeClassLoader.php, line 54

Namespace

Symfony\Component\Routing\Loader
View source
abstract class AttributeClassLoader implements LoaderInterface {
    
    /**
     * @deprecated since Symfony 7.2, use "setRouteAttributeClass()" instead.
     */
    protected string $routeAnnotationClass = RouteAttribute::class;
    private string $routeAttributeClass = RouteAttribute::class;
    protected int $defaultRouteIndex = 0;
    public function __construct(?string $env = null) {
    }
    
    /**
     * @deprecated since Symfony 7.2, use "setRouteAttributeClass(string $class)" instead
     *
     * Sets the annotation class to read route properties from.
     */
    public function setRouteAnnotationClass(string $class) : void {
        trigger_deprecation('symfony/routing', '7.2', 'The "%s()" method is deprecated, use "%s::setRouteAttributeClass()" instead.', __METHOD__, self::class);
        $this->setRouteAttributeClass($class);
    }
    
    /**
     * Sets the attribute class to read route properties from.
     */
    public function setRouteAttributeClass(string $class) : void {
        $this->routeAnnotationClass = $class;
        $this->routeAttributeClass = $class;
    }
    
    /**
     * @throws \InvalidArgumentException When route can't be parsed
     */
    public function load(mixed $class, ?string $type = null) : RouteCollection {
        if (!class_exists($class)) {
            throw new \InvalidArgumentException(\sprintf('Class "%s" does not exist.', $class));
        }
        $class = new \ReflectionClass($class);
        if ($class->isAbstract()) {
            throw new \InvalidArgumentException(\sprintf('Attributes from class "%s" cannot be read as it is abstract.', $class->getName()));
        }
        $globals = $this->getGlobals($class);
        $collection = new RouteCollection();
        $collection->addResource(new FileResource($class->getFileName()));
        if ($globals['env'] && $this->env !== $globals['env']) {
            return $collection;
        }
        $fqcnAlias = false;
        foreach ($class->getMethods() as $method) {
            $this->defaultRouteIndex = 0;
            $routeNamesBefore = array_keys($collection->all());
            foreach ($this->getAttributes($method) as $attr) {
                $this->addRoute($collection, $attr, $globals, $class, $method);
                if ('__invoke' === $method->name) {
                    $fqcnAlias = true;
                }
            }
            if (1 === $collection->count() - \count($routeNamesBefore)) {
                $newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
                if ($newRouteName !== ($aliasName = \sprintf('%s::%s', $class->name, $method->name))) {
                    $collection->addAlias($aliasName, $newRouteName);
                }
            }
        }
        if (0 === $collection->count() && $class->hasMethod('__invoke')) {
            $globals = $this->resetGlobals();
            foreach ($this->getAttributes($class) as $attr) {
                $this->addRoute($collection, $attr, $globals, $class, $class->getMethod('__invoke'));
                $fqcnAlias = true;
            }
        }
        if ($fqcnAlias && 1 === $collection->count()) {
            $invokeRouteName = key($collection->all());
            if ($invokeRouteName !== $class->name) {
                $collection->addAlias($class->name, $invokeRouteName);
            }
            if ($invokeRouteName !== ($aliasName = \sprintf('%s::__invoke', $class->name))) {
                $collection->addAlias($aliasName, $invokeRouteName);
            }
        }
        return $collection;
    }
    
    /**
     * @param RouteAttribute $attr or an object that exposes a similar interface
     */
    protected function addRoute(RouteCollection $collection, object $attr, array $globals, \ReflectionClass $class, \ReflectionMethod $method) : void {
        if ($attr->getEnv() && $attr->getEnv() !== $this->env) {
            return;
        }
        $name = $attr->getName() ?? $this->getDefaultRouteName($class, $method);
        $name = $globals['name'] . $name;
        $requirements = $attr->getRequirements();
        foreach ($requirements as $placeholder => $requirement) {
            if (\is_int($placeholder)) {
                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()));
            }
        }
        $defaults = array_replace($globals['defaults'], $attr->getDefaults());
        $requirements = array_replace($globals['requirements'], $requirements);
        $options = array_replace($globals['options'], $attr->getOptions());
        $schemes = array_unique(array_merge($globals['schemes'], $attr->getSchemes()));
        $methods = array_unique(array_merge($globals['methods'], $attr->getMethods()));
        $host = $attr->getHost() ?? $globals['host'];
        $condition = $attr->getCondition() ?? $globals['condition'];
        $priority = $attr->getPriority() ?? $globals['priority'];
        $path = $attr->getLocalizedPaths() ?: $attr->getPath();
        $prefix = $globals['localized_paths'] ?: $globals['path'];
        $paths = [];
        if (\is_array($path)) {
            if (!\is_array($prefix)) {
                foreach ($path as $locale => $localePath) {
                    $paths[$locale] = $prefix . $localePath;
                }
            }
            elseif ($missing = array_diff_key($prefix, $path)) {
                throw new \LogicException(\sprintf('Route to "%s" is missing paths for locale(s) "%s".', $class->name . '::' . $method->name, implode('", "', array_keys($missing))));
            }
            else {
                foreach ($path as $locale => $localePath) {
                    if (!isset($prefix[$locale])) {
                        throw new \LogicException(\sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".', $method->name, $locale, $class->name));
                    }
                    $paths[$locale] = $prefix[$locale] . $localePath;
                }
            }
        }
        elseif (\is_array($prefix)) {
            foreach ($prefix as $locale => $localePrefix) {
                $paths[$locale] = $localePrefix . $path;
            }
        }
        else {
            $paths[] = $prefix . $path;
        }
        foreach ($method->getParameters() as $param) {
            if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) {
                continue;
            }
            foreach ($paths as $locale => $path) {
                if (preg_match(\sprintf('/\\{%s(?:<.*?>)?\\}/', preg_quote($param->name)), $path)) {
                    if (\is_scalar($defaultValue = $param->getDefaultValue()) || null === $defaultValue) {
                        $defaults[$param->name] = $defaultValue;
                    }
                    elseif ($defaultValue instanceof \BackedEnum) {
                        $defaults[$param->name] = $defaultValue->value;
                    }
                    break;
                }
            }
        }
        foreach ($paths as $locale => $path) {
            $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
            $this->configureRoute($route, $class, $method, $attr);
            if (0 !== $locale) {
                $route->setDefault('_locale', $locale);
                $route->setRequirement('_locale', preg_quote($locale));
                $route->setDefault('_canonical_route', $name);
                $collection->add($name . '.' . $locale, $route, $priority);
            }
            else {
                $collection->add($name, $route, $priority);
            }
        }
    }
    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 || 'attribute' === $type);
    }
    public function setResolver(LoaderResolverInterface $resolver) : void {
    }
    public function getResolver() : LoaderResolverInterface {
        throw new LogicException(\sprintf('The "%s()" method must not be called.', __METHOD__));
    }
    
    /**
     * Gets the default route name for a class method.
     *
     * @return string
     */
    protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) {
        $name = str_replace('\\', '_', $class->name) . '_' . $method->name;
        $name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name);
        if ($this->defaultRouteIndex > 0) {
            $name .= '_' . $this->defaultRouteIndex;
        }
        ++$this->defaultRouteIndex;
        return $name;
    }
    
    /**
     * @return array<string, mixed>
     */
    protected function getGlobals(\ReflectionClass $class) : array {
        $globals = $this->resetGlobals();
        // to be replaced in Symfony 8.0 by $this->routeAttributeClass
        if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
            $attr = $attribute->newInstance();
            if (null !== $attr->getName()) {
                $globals['name'] = $attr->getName();
            }
            if (null !== $attr->getPath()) {
                $globals['path'] = $attr->getPath();
            }
            $globals['localized_paths'] = $attr->getLocalizedPaths();
            if (null !== $attr->getRequirements()) {
                $globals['requirements'] = $attr->getRequirements();
            }
            if (null !== $attr->getOptions()) {
                $globals['options'] = $attr->getOptions();
            }
            if (null !== $attr->getDefaults()) {
                $globals['defaults'] = $attr->getDefaults();
            }
            if (null !== $attr->getSchemes()) {
                $globals['schemes'] = $attr->getSchemes();
            }
            if (null !== $attr->getMethods()) {
                $globals['methods'] = $attr->getMethods();
            }
            if (null !== $attr->getHost()) {
                $globals['host'] = $attr->getHost();
            }
            if (null !== $attr->getCondition()) {
                $globals['condition'] = $attr->getCondition();
            }
            $globals['priority'] = $attr->getPriority() ?? 0;
            $globals['env'] = $attr->getEnv();
            foreach ($globals['requirements'] as $placeholder => $requirement) {
                if (\is_int($placeholder)) {
                    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()));
                }
            }
        }
        return $globals;
    }
    private function resetGlobals() : array {
        return [
            'path' => null,
            'localized_paths' => [],
            'requirements' => [],
            'options' => [],
            'defaults' => [],
            'schemes' => [],
            'methods' => [],
            'host' => '',
            'condition' => '',
            'name' => '',
            'priority' => 0,
            'env' => null,
        ];
    }
    protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition) : Route {
        return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
    }
    
    /**
     * @param RouteAttribute $attr or an object that exposes a similar interface
     *
     * @return void
     */
    protected abstract function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $attr);
    
    /**
     * @return iterable<int, RouteAttribute>
     */
    private function getAttributes(\ReflectionClass|\ReflectionMethod $reflection) : iterable {
        // to be replaced in Symfony 8.0 by $this->routeAttributeClass
        foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
            (yield $attribute->newInstance());
        }
    }

}

Members

Title Sort descending Deprecated Modifiers Object type Summary
AttributeClassLoader::$defaultRouteIndex protected property
AttributeClassLoader::$routeAnnotationClass Deprecated protected property
AttributeClassLoader::$routeAttributeClass private property
AttributeClassLoader::addRoute protected function
AttributeClassLoader::configureRoute abstract protected function
AttributeClassLoader::createRoute protected function
AttributeClassLoader::getAttributes private function
AttributeClassLoader::getDefaultRouteName protected function Gets the default route name for a class method.
AttributeClassLoader::getGlobals protected function
AttributeClassLoader::getResolver public function
AttributeClassLoader::load public function
AttributeClassLoader::resetGlobals private function
AttributeClassLoader::setResolver public function
AttributeClassLoader::setRouteAnnotationClass Deprecated public function
AttributeClassLoader::setRouteAttributeClass public function Sets the attribute class to read route properties from.
AttributeClassLoader::supports public function
AttributeClassLoader::__construct public function

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal