2024-11-27 11:16:45 +07:00
< ? php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader ;
use Symfony\Component\Config\Loader\LoaderInterface ;
use Symfony\Component\Config\Loader\LoaderResolverInterface ;
use Symfony\Component\Config\Resource\FileResource ;
2024-12-02 01:18:34 +07:00
use Symfony\Component\Routing\Attribute\Route as RouteAttribute ;
2024-11-27 11:16:45 +07:00
use Symfony\Component\Routing\Exception\LogicException ;
use Symfony\Component\Routing\Route ;
use Symfony\Component\Routing\RouteCollection ;
/**
* 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>
*/
abstract class AttributeClassLoader implements LoaderInterface
{
2024-12-02 01:18:34 +07:00
/**
* @deprecated since Symfony 7.2, use "setRouteAttributeClass()" instead.
*/
protected string $routeAnnotationClass = RouteAttribute :: class ;
private string $routeAttributeClass = RouteAttribute :: class ;
2024-11-27 11:16:45 +07:00
protected int $defaultRouteIndex = 0 ;
public function __construct (
protected readonly ? string $env = null ,
) {
}
/**
2024-12-02 01:18:34 +07:00
* @deprecated since Symfony 7.2, use "setRouteAttributeClass(string $class)" instead
*
2024-11-27 11:16:45 +07:00
* Sets the annotation class to read route properties from.
*/
public function setRouteAnnotationClass ( string $class ) : void
2024-12-02 01:18:34 +07:00
{
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
2024-11-27 11:16:45 +07:00
{
$this -> routeAnnotationClass = $class ;
2024-12-02 01:18:34 +07:00
$this -> routeAttributeClass = $class ;
2024-11-27 11:16:45 +07:00
}
/**
* @throws \InvalidArgumentException When route can't be parsed
*/
public function load ( mixed $class , ? string $type = null ) : RouteCollection
{
if ( ! class_exists ( $class )) {
2024-12-02 01:18:34 +07:00
throw new \InvalidArgumentException ( \sprintf ( 'Class "%s" does not exist.' , $class ));
2024-11-27 11:16:45 +07:00
}
$class = new \ReflectionClass ( $class );
if ( $class -> isAbstract ()) {
2024-12-02 01:18:34 +07:00
throw new \InvalidArgumentException ( \sprintf ( 'Attributes from class "%s" cannot be read as it is abstract.' , $class -> getName ()));
2024-11-27 11:16:45 +07:00
}
$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 ());
2024-12-02 01:18:34 +07:00
foreach ( $this -> getAttributes ( $method ) as $attr ) {
$this -> addRoute ( $collection , $attr , $globals , $class , $method );
2024-11-27 11:16:45 +07:00
if ( '__invoke' === $method -> name ) {
$fqcnAlias = true ;
}
}
if ( 1 === $collection -> count () - \count ( $routeNamesBefore )) {
$newRouteName = current ( array_diff ( array_keys ( $collection -> all ()), $routeNamesBefore ));
2024-12-02 01:18:34 +07:00
if ( $newRouteName !== $aliasName = \sprintf ( '%s::%s' , $class -> name , $method -> name )) {
2024-11-27 11:16:45 +07:00
$collection -> addAlias ( $aliasName , $newRouteName );
}
}
}
if ( 0 === $collection -> count () && $class -> hasMethod ( '__invoke' )) {
$globals = $this -> resetGlobals ();
2024-12-02 01:18:34 +07:00
foreach ( $this -> getAttributes ( $class ) as $attr ) {
$this -> addRoute ( $collection , $attr , $globals , $class , $class -> getMethod ( '__invoke' ));
2024-11-27 11:16:45 +07:00
$fqcnAlias = true ;
}
}
if ( $fqcnAlias && 1 === $collection -> count ()) {
$invokeRouteName = key ( $collection -> all ());
if ( $invokeRouteName !== $class -> name ) {
$collection -> addAlias ( $class -> name , $invokeRouteName );
}
2024-12-02 01:18:34 +07:00
if ( $invokeRouteName !== $aliasName = \sprintf ( '%s::__invoke' , $class -> name )) {
2024-11-27 11:16:45 +07:00
$collection -> addAlias ( $aliasName , $invokeRouteName );
}
}
return $collection ;
}
/**
2024-12-02 01:18:34 +07:00
* @param RouteAttribute $attr or an object that exposes a similar interface
2024-11-27 11:16:45 +07:00
*/
2024-12-02 01:18:34 +07:00
protected function addRoute ( RouteCollection $collection , object $attr , array $globals , \ReflectionClass $class , \ReflectionMethod $method ) : void
2024-11-27 11:16:45 +07:00
{
2024-12-02 01:18:34 +07:00
if ( $attr -> getEnv () && $attr -> getEnv () !== $this -> env ) {
2024-11-27 11:16:45 +07:00
return ;
}
2024-12-02 01:18:34 +07:00
$name = $attr -> getName () ? ? $this -> getDefaultRouteName ( $class , $method );
2024-11-27 11:16:45 +07:00
$name = $globals [ 'name' ] . $name ;
2024-12-02 01:18:34 +07:00
$requirements = $attr -> getRequirements ();
2024-11-27 11:16:45 +07:00
foreach ( $requirements as $placeholder => $requirement ) {
if ( \is_int ( $placeholder )) {
2024-12-02 01:18:34 +07:00
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 ()));
2024-11-27 11:16:45 +07:00
}
}
2024-12-02 01:18:34 +07:00
$defaults = array_replace ( $globals [ 'defaults' ], $attr -> getDefaults ());
2024-11-27 11:16:45 +07:00
$requirements = array_replace ( $globals [ 'requirements' ], $requirements );
2024-12-02 01:18:34 +07:00
$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 ()));
2024-11-27 11:16:45 +07:00
2024-12-02 01:18:34 +07:00
$host = $attr -> getHost () ? ? $globals [ 'host' ];
$condition = $attr -> getCondition () ? ? $globals [ 'condition' ];
$priority = $attr -> getPriority () ? ? $globals [ 'priority' ];
2024-11-27 11:16:45 +07:00
2024-12-02 01:18:34 +07:00
$path = $attr -> getLocalizedPaths () ? : $attr -> getPath ();
2024-11-27 11:16:45 +07:00
$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 )) {
2024-12-02 01:18:34 +07:00
throw new \LogicException ( \sprintf ( 'Route to "%s" is missing paths for locale(s) "%s".' , $class -> name . '::' . $method -> name , implode ( '", "' , array_keys ( $missing ))));
2024-11-27 11:16:45 +07:00
} else {
foreach ( $path as $locale => $localePath ) {
if ( ! isset ( $prefix [ $locale ])) {
2024-12-02 01:18:34 +07:00
throw new \LogicException ( \sprintf ( 'Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".' , $method -> name , $locale , $class -> name ));
2024-11-27 11:16:45 +07:00
}
$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 ) {
2024-12-02 01:18:34 +07:00
if ( preg_match ( \sprintf ( '/\{%s(?:<.*?>)?\}/' , preg_quote ( $param -> name )), $path )) {
2024-11-27 11:16:45 +07:00
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 );
2024-12-02 01:18:34 +07:00
$this -> configureRoute ( $route , $class , $method , $attr );
2024-11-27 11:16:45 +07:00
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
{
2024-12-02 01:18:34 +07:00
throw new LogicException ( \sprintf ( 'The "%s()" method must not be called.' , __METHOD__ ));
2024-11-27 11:16:45 +07:00
}
/**
* 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 ();
2024-12-02 01:18:34 +07:00
// to be replaced in Symfony 8.0 by $this->routeAttributeClass
2024-11-27 11:16:45 +07:00
if ( $attribute = $class -> getAttributes ( $this -> routeAnnotationClass , \ReflectionAttribute :: IS_INSTANCEOF )[ 0 ] ? ? null ) {
2024-12-02 01:18:34 +07:00
$attr = $attribute -> newInstance ();
2024-11-27 11:16:45 +07:00
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getName ()) {
$globals [ 'name' ] = $attr -> getName ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getPath ()) {
$globals [ 'path' ] = $attr -> getPath ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
$globals [ 'localized_paths' ] = $attr -> getLocalizedPaths ();
2024-11-27 11:16:45 +07:00
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getRequirements ()) {
$globals [ 'requirements' ] = $attr -> getRequirements ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getOptions ()) {
$globals [ 'options' ] = $attr -> getOptions ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getDefaults ()) {
$globals [ 'defaults' ] = $attr -> getDefaults ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getSchemes ()) {
$globals [ 'schemes' ] = $attr -> getSchemes ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getMethods ()) {
$globals [ 'methods' ] = $attr -> getMethods ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getHost ()) {
$globals [ 'host' ] = $attr -> getHost ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
if ( null !== $attr -> getCondition ()) {
$globals [ 'condition' ] = $attr -> getCondition ();
2024-11-27 11:16:45 +07:00
}
2024-12-02 01:18:34 +07:00
$globals [ 'priority' ] = $attr -> getPriority () ? ? 0 ;
$globals [ 'env' ] = $attr -> getEnv ();
2024-11-27 11:16:45 +07:00
foreach ( $globals [ 'requirements' ] as $placeholder => $requirement ) {
if ( \is_int ( $placeholder )) {
2024-12-02 01:18:34 +07:00
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 ()));
2024-11-27 11:16:45 +07:00
}
}
}
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 );
}
/**
2024-12-02 01:18:34 +07:00
* @param RouteAttribute $attr or an object that exposes a similar interface
*
2024-11-27 11:16:45 +07:00
* @return void
*/
2024-12-02 01:18:34 +07:00
abstract protected function configureRoute ( Route $route , \ReflectionClass $class , \ReflectionMethod $method , object $attr );
2024-11-27 11:16:45 +07:00
/**
2024-12-02 01:18:34 +07:00
* @return iterable<int, RouteAttribute>
2024-11-27 11:16:45 +07:00
*/
2024-12-02 01:18:34 +07:00
private function getAttributes ( \ReflectionClass | \ReflectionMethod $reflection ) : iterable
2024-11-27 11:16:45 +07:00
{
2024-12-02 01:18:34 +07:00
// to be replaced in Symfony 8.0 by $this->routeAttributeClass
2024-11-27 11:16:45 +07:00
foreach ( $reflection -> getAttributes ( $this -> routeAnnotationClass , \ReflectionAttribute :: IS_INSTANCEOF ) as $attribute ) {
yield $attribute -> newInstance ();
}
}
}