-
Notifications
You must be signed in to change notification settings - Fork 11.6k
Adds support for Attribute-based Routing (MVP) #56424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
9552f4c
feat: Add initial routing attributes
AmirHkrg 7ae0eac
feat: Add AttributeRouteRegistrar and marker interface
AmirHkrg 31a44f5
feat: Integrate attribute routing into the application bootstrap process
AmirHkrg e41143e
test: Add initial feature tests for attribute routing
AmirHkrg 02396fc
fix: style
AmirHkrg 536a63d
fix: style
AmirHkrg 527fdea
fix: damn styleci
AmirHkrg 37c9eea
Merge branch 'laravel:master' into master
AmirHkrg 2d9e8d1
fix: change abstract class to normal class for attribute
AmirHkrg c5cf7b7
Merge branch 'laravel:master' into master
AmirHkrg 04cec2b
fix: CallableDispatcher binding in tests
AmirHkrg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
8 changes: 8 additions & 0 deletions
8
src/Illuminate/Contracts/Routing/AttributeRouteController.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?php | ||
|
||
namespace Illuminate\Contracts\Routing; | ||
|
||
interface AttributeRouteController | ||
{ | ||
// | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
<?php | ||
|
||
namespace Illuminate\Routing; | ||
|
||
use Illuminate\Contracts\Foundation\Application; | ||
use Illuminate\Contracts\Routing\AttributeRouteController; | ||
use Illuminate\Routing\Attributes\Group; | ||
use Illuminate\Routing\Attributes\RouteAttribute; | ||
use Illuminate\Support\Str; | ||
use ReflectionClass; | ||
use Symfony\Component\Finder\Finder; | ||
|
||
class AttributeRouteRegistrar | ||
{ | ||
/** | ||
* The PSR-4 autoloading map from Composer. | ||
* | ||
* @var array | ||
*/ | ||
protected $psr4Paths; | ||
|
||
/** | ||
* Create a new AttributeRouteRegistrar instance. | ||
* | ||
* @param \Illuminate\Contracts\Foundation\Application $app | ||
* @param \Illuminate\Routing\Router $router | ||
* @return void | ||
*/ | ||
public function __construct(protected Application $app, protected Router $router) | ||
{ | ||
$this->psr4Paths = $this->getPsr4Paths(); | ||
} | ||
|
||
/** | ||
* Scan the given directories and register any found attribute-based routes. | ||
* | ||
* @param string ...$controllerDirectories | ||
* @return void | ||
*/ | ||
public function register(...$controllerDirectories) | ||
{ | ||
if (empty($controllerDirectories)) { | ||
return; | ||
} | ||
|
||
$finder = (new Finder)->files()->in($controllerDirectories)->name('*.php'); | ||
|
||
foreach ($finder as $file) { | ||
$className = $this->getClassFromFile($file->getRealPath()); | ||
|
||
if ($className && class_exists($className) && is_a($className, AttributeRouteController::class, true)) { | ||
$this->registerControllerRoutes($className); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Registers all routes for a given controller class. | ||
* | ||
* @param string $controllerClassName | ||
* @return void | ||
*/ | ||
public function registerControllerRoutes($controllerClassName) | ||
{ | ||
$reflectionClass = new ReflectionClass($controllerClassName); | ||
|
||
$groupAttributes = $this->getGroupAttributes($reflectionClass) ?? []; | ||
|
||
$this->router->group($groupAttributes, function (Router $router) use ($reflectionClass) { | ||
foreach ($reflectionClass->getMethods() as $method) { | ||
$attributes = $method->getAttributes(RouteAttribute::class, \ReflectionAttribute::IS_INSTANCEOF); | ||
|
||
foreach ($attributes as $attribute) { | ||
try { | ||
$instance = $attribute->newInstance(); | ||
$route = $router->addRoute( | ||
$instance->methods, | ||
$instance->path, | ||
[$reflectionClass->getName(), $method->getName()] | ||
); | ||
$this->applyRouteOptions($route, $instance); | ||
} catch (\Throwable $e) { | ||
report($e); | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Applies all options from a RouteAttribute instance to a route. | ||
* | ||
* @param \Illuminate\Routing\Route $route | ||
* @param \Illuminate\Routing\Attributes\RouteAttribute $instance | ||
* @return void | ||
*/ | ||
protected function applyRouteOptions(Route $route, RouteAttribute $instance): void | ||
{ | ||
if ($instance->name) { | ||
$route->name($instance->name); | ||
} | ||
if ($instance->middleware) { | ||
$route->middleware($instance->middleware); | ||
} | ||
if ($instance->where) { | ||
$route->where($instance->where); | ||
} | ||
|
||
// Mark the route for the route:list command | ||
$route->setAction(array_merge($route->getAction(), ['is_attribute_route' => true])); | ||
} | ||
|
||
/** | ||
* Gets the properties from a single #[Group] attribute on a class. | ||
* | ||
* @param \ReflectionClass $reflectionClass | ||
* @return array|null | ||
*/ | ||
protected function getGroupAttributes(ReflectionClass $reflectionClass): ?array | ||
{ | ||
$attributes = $reflectionClass->getAttributes(Group::class); | ||
|
||
if (count($attributes) === 0) { | ||
return null; | ||
} | ||
|
||
try { | ||
/** @var Group $group */ | ||
$group = $attributes[0]->newInstance(); | ||
|
||
return array_filter([ | ||
'prefix' => $group->prefix, | ||
'middleware' => $group->middleware, | ||
'as' => $group->name, | ||
'where' => $group->where, | ||
]); | ||
} catch (\Throwable $e) { | ||
report($e); | ||
|
||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Derive the fully qualified class name from a file path. | ||
* | ||
* This implementation uses the project's Composer PSR-4 map to determine | ||
* the class name, making it compatible with any autoloaded directory. | ||
* | ||
* @param string $path | ||
* @return string|null | ||
*/ | ||
protected function getClassFromFile($path) | ||
{ | ||
foreach ($this->psr4Paths as $namespace => $paths) { | ||
foreach ((array) $paths as $psr4Path) { | ||
if (Str::startsWith($path, $psr4Path)) { | ||
$relativePath = Str::of($path) | ||
->after($psr4Path) | ||
->trim(DIRECTORY_SEPARATOR) | ||
->replace(['/', '.php'], ['\\', '']) | ||
->toString(); | ||
|
||
return $namespace.$relativePath; | ||
} | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Load the Composer PSR-4 autoloading map. | ||
* | ||
* This map is used to convert a file path into a fully qualified class name. | ||
* | ||
* @return array | ||
*/ | ||
protected function getPsr4Paths() | ||
{ | ||
$composerPath = $this->app->basePath('vendor/composer/autoload_psr4.php'); | ||
|
||
return file_exists($composerPath) ? require $composerPath : []; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
namespace Illuminate\Routing\Attributes; | ||
|
||
use Attribute; | ||
|
||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] | ||
class Any extends RouteAttribute | ||
{ | ||
/** | ||
* @param string $path | ||
* @param string|null $name | ||
* @param array|string $middleware | ||
* @param array $where | ||
*/ | ||
public function __construct( | ||
$path, | ||
$name = null, | ||
$middleware = [], | ||
$where = [] | ||
) { | ||
parent::__construct($path, ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $name, $middleware, $where); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
namespace Illuminate\Routing\Attributes; | ||
|
||
use Attribute; | ||
|
||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] | ||
class Delete extends RouteAttribute | ||
{ | ||
/** | ||
* @param string $path | ||
* @param string|null $name | ||
* @param array|string $middleware | ||
* @param array $where | ||
*/ | ||
public function __construct( | ||
$path, | ||
$name = null, | ||
$middleware = [], | ||
$where = [] | ||
) { | ||
parent::__construct($path, ['DELETE'], $name, $middleware, $where); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
namespace Illuminate\Routing\Attributes; | ||
|
||
use Attribute; | ||
|
||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] | ||
class Get extends RouteAttribute | ||
{ | ||
/** | ||
* @param string $path | ||
* @param string|null $name | ||
* @param array|string $middleware | ||
* @param array $where | ||
*/ | ||
public function __construct( | ||
$path, | ||
$name = null, | ||
$middleware = [], | ||
$where = [] | ||
) { | ||
parent::__construct($path, ['GET', 'HEAD'], $name, $middleware, $where); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php | ||
|
||
namespace Illuminate\Routing\Attributes; | ||
|
||
use Attribute; | ||
|
||
#[Attribute(Attribute::TARGET_CLASS)] | ||
class Group | ||
{ | ||
/** | ||
* @param string|null $prefix | ||
* @param string|null $name | ||
* @param array|string $middleware | ||
* @param array $where | ||
*/ | ||
public function __construct( | ||
public ?string $prefix = null, | ||
public ?string $name = null, | ||
public array|string $middleware = [], | ||
public array $where = [] | ||
) { | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be an attribute as well, instead of an interface? It seems that things are moving away from using empty interfaces to mark functionality and towards using attributes instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello,
Thanks for the review.
If I understood correctly, you’re referring to the
AttributeRouteController
interface. In this pull request, I initially made the system explicit by requiring that interface.If this pull request is accepted, we could also add an implicit mode so that all controllers are scanned without needing to implement the
AttributeRouteController
interface.I’d also be happy to hear if you think there’s a better approach that could be implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AmirHkrg I'm suggesting using an attribute instead of an interface since the interface is empty and appears to be used as just a marker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to avoid using an interface for marking altogether, the best approach would be the implicit one — defining routes only with attributes, without any additional requirements.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume, you talk past each other. Afaik, Will meant this:
https://www.php.net/manual/en/language.attributes.overview.php
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @shaedrich
Yes, I understand their point, but I followed the current structure and conventions of Laravel. As you can see, Laravel uses interfaces for marking, such as the
ShouldQueue
andShouldBeUnique
interfaces in queues.It is possible to use attributes for marking as well, but it would work a bit differently compared to other features.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey Amir, no offense, but this is short-sighted. Of course, Laravel has that structure. This is for historical reasons. When Laravel was created, there were no attributes. And if we would rectify everything with "we've always done it that way" and limit ourselves to only what had already been available at that time, there would be no progress.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Certainly, if I look at it from your perspective, it’s completely valid, and it could be really good to be a pioneer in using new approaches.
I’ll replace the interface with attributes, though having both together might not be a bad idea either.
Of course, it’s also important that the Laravel team agrees with us, since this pull request itself might have a low chance of being merged.