-
-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3325 from mficzel/feature/routeAnnotations
FEATURE: Add `Flow\Route` Attribute/Annotation
- Loading branch information
Showing
13 changed files
with
698 additions
and
12 deletions.
There are no files selected for viewing
This file contains 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,67 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Neos\Flow\Annotations; | ||
|
||
/* | ||
* This file is part of the Neos.Flow package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
|
||
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; | ||
|
||
/** | ||
* Adds a route configuration to a method | ||
* | ||
* This is a convenient way to add routes in project code | ||
* but should not be used in libraries/packages that shall be | ||
* configured for different use cases. | ||
* | ||
* @Annotation | ||
* @NamedArgumentConstructor | ||
* @Target({"METHOD"}) | ||
*/ | ||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] | ||
final readonly class Route | ||
{ | ||
/** | ||
* Magic route values cannot be set as default nor be contained as segments like `{\@action}` or `{\@controller}` in the uriPattern. | ||
* The magic route value `\@format` is allowed if necessary. | ||
*/ | ||
private const PRESERVED_DEFAULTS = ['@package', '@subpackage', '@controller', '@action']; | ||
|
||
/** | ||
* @param string $uriPattern The uri-pattern for the route without leading '/'. Might contain route values in the form of `path/{foo}` | ||
* @param string $name The suffix of the route name as shown in `route:list` (defaults to the action name: "My.Package :: Site :: index") | ||
* @param array $httpMethods List of uppercase http verbs like 'GET', 'POST', 'PUT', 'DELETE', if not specified any request method will be matched | ||
* @param array $defaults Values to set for this route | ||
*/ | ||
public function __construct( | ||
public string $uriPattern, | ||
public string $name = '', | ||
public array $httpMethods = [], | ||
public array $defaults = [], | ||
) { | ||
if ($uriPattern === '' || str_starts_with($uriPattern, '/')) { | ||
throw new \DomainException(sprintf('Uri pattern must not be empty or begin with a slash: "%s"', $uriPattern), 1711529592); | ||
} | ||
foreach ($httpMethods as $httpMethod) { | ||
if ($httpMethod === '' || ctype_lower($httpMethod)) { | ||
throw new \DomainException(sprintf('Http method must not be empty or be lower case: "%s"', $httpMethod), 1711530485); | ||
} | ||
} | ||
foreach (self::PRESERVED_DEFAULTS as $preservedDefaultName) { | ||
if (str_contains($uriPattern, sprintf('{%s}', $preservedDefaultName))) { | ||
throw new \DomainException(sprintf('It is not allowed to use "%s" in the uri pattern "%s"', $preservedDefaultName, $uriPattern), 1711129634); | ||
} | ||
if (array_key_exists($preservedDefaultName, $defaults)) { | ||
throw new \DomainException(sprintf('It is not allowed to override "%s" as default', $preservedDefaultName), 1711129638); | ||
} | ||
} | ||
} | ||
} |
This file contains 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
149 changes: 149 additions & 0 deletions
149
Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php
This file contains 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,149 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Neos.Flow package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Neos\Flow\Mvc\Routing; | ||
|
||
use Neos\Flow\Mvc\Controller\ActionController; | ||
use Neos\Flow\Mvc\Exception\InvalidActionNameException; | ||
use Neos\Flow\Mvc\Routing\Exception\InvalidControllerException; | ||
use Neos\Flow\ObjectManagement\ObjectManagerInterface; | ||
use Neos\Flow\Reflection\ReflectionService; | ||
use Neos\Flow\Annotations as Flow; | ||
use Neos\Utility\Arrays; | ||
|
||
/** | ||
* Allows to annotate controller methods with route configurations | ||
* | ||
* Internal implementation: | ||
* ----------------------- | ||
* | ||
* Flows routing configuration is declared via \@package, \@subpackage, \@controller and \@action | ||
* The first three options will resolve to a fully qualified class name {@see \Neos\Flow\Mvc\ActionRequest::getControllerObjectName()} | ||
* which is instantiated in the dispatcher {@see \Neos\Flow\Mvc\Dispatcher::dispatch()} | ||
* | ||
* The latter \@action option will be treated internally by each controller. From the perspective of the dispatcher \@action is just another routing value. | ||
* By convention during processRequest in the default ActionController {@see \ActionController::resolveActionMethodName()} will be used | ||
* to concatenate the "Action" suffix to the action name | ||
* and {@see ActionController::callActionMethod()} will invoke the method internally with prepared method arguments. | ||
* | ||
* Creating routes by annotation must make a few assumptions to work: | ||
* | ||
* 1. As not every FQ class name is representable via the routing configuration (e.g. the class has to end with "Controller"), | ||
* only classes can be annotated that reside in a correct location and have the correct suffix. | ||
* Otherwise, an exception will be thrown as the class is not discoverable by the dispatcher. | ||
* | ||
* 2. As the ActionController requires a little magic and is the main use case we currently only support this controller. | ||
* For that reason it is validated that the annotation is inside an ActionController and the method ends with "Action". | ||
* The routing value with the suffix trimmed will be generated: | ||
* | ||
* class MyThingController extends ActionController | ||
* { | ||
* #[Flow\Route(path: 'foo')] | ||
* public function someAction() | ||
* { | ||
* } | ||
* } | ||
* | ||
* The example will genrate the configuration: | ||
* | ||
* \@package My.Package | ||
* \@controller MyThing | ||
* \@action some | ||
* | ||
* TODO for a future scope of `Flow\Action` see {@link https://github.com/neos/flow-development-collection/issues/3335} | ||
*/ | ||
final class AttributeRoutesProvider implements RoutesProviderInterface | ||
{ | ||
/** | ||
* @param array<string> $classNames | ||
*/ | ||
public function __construct( | ||
public readonly ReflectionService $reflectionService, | ||
public readonly ObjectManagerInterface $objectManager, | ||
public readonly array $classNames, | ||
) { | ||
} | ||
|
||
public function getRoutes(): Routes | ||
{ | ||
$routes = []; | ||
$annotatedClasses = $this->reflectionService->getClassesContainingMethodsAnnotatedWith(Flow\Route::class); | ||
|
||
foreach ($annotatedClasses as $className) { | ||
$includeClassName = false; | ||
foreach ($this->classNames as $classNamePattern) { | ||
if (fnmatch($classNamePattern, $className, FNM_NOESCAPE)) { | ||
$includeClassName = true; | ||
break; | ||
} | ||
} | ||
if (!$includeClassName) { | ||
continue; | ||
} | ||
|
||
if (!in_array(ActionController::class, class_parents($className), true)) { | ||
throw new InvalidControllerException('TODO: Currently #[Flow\Route] is only supported for ActionController. See https://github.com/neos/flow-development-collection/issues/3335.'); | ||
} | ||
|
||
$controllerObjectName = $this->objectManager->getCaseSensitiveObjectName($className); | ||
$controllerPackageKey = $this->objectManager->getPackageKeyByObjectName($controllerObjectName); | ||
$controllerPackageNamespace = str_replace('.', '\\', $controllerPackageKey); | ||
if (!str_ends_with($className, 'Controller')) { | ||
throw new InvalidControllerException('Only for controller classes'); | ||
} | ||
|
||
$localClassName = substr($className, strlen($controllerPackageNamespace) + 1); | ||
|
||
if (str_starts_with($localClassName, 'Controller\\')) { | ||
$controllerName = substr($localClassName, 11); | ||
$subPackage = null; | ||
} elseif (str_contains($localClassName, '\\Controller\\')) { | ||
list($subPackage, $controllerName) = explode('\\Controller\\', $localClassName); | ||
} else { | ||
throw new InvalidControllerException('Unknown controller pattern'); | ||
} | ||
|
||
$annotatedMethods = $this->reflectionService->getMethodsAnnotatedWith($className, Flow\Route::class); | ||
foreach ($annotatedMethods as $methodName) { | ||
if (!str_ends_with($methodName, 'Action')) { | ||
throw new InvalidActionNameException('Only for action methods'); | ||
} | ||
$annotations = $this->reflectionService->getMethodAnnotations($className, $methodName, Flow\Route::class); | ||
foreach ($annotations as $annotation) { | ||
if ($annotation instanceof Flow\Route) { | ||
$controller = substr($controllerName, 0, -10); | ||
$action = substr($methodName, 0, -6); | ||
$configuration = [ | ||
'name' => $controllerPackageKey . ' :: ' . $controller . ' :: ' . ($annotation->name ?: $action), | ||
'uriPattern' => $annotation->uriPattern, | ||
'httpMethods' => $annotation->httpMethods, | ||
'defaults' => Arrays::arrayMergeRecursiveOverrule( | ||
[ | ||
'@package' => $controllerPackageKey, | ||
'@subpackage' => $subPackage, | ||
'@controller' => $controller, | ||
'@action' => $action, | ||
'@format' => 'html' | ||
], | ||
$annotation->defaults ?? [] | ||
) | ||
]; | ||
$routes[] = Route::fromConfiguration($configuration); | ||
} | ||
} | ||
} | ||
} | ||
return Routes::create(...$routes); | ||
} | ||
} |
39 changes: 39 additions & 0 deletions
39
Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProviderFactory.php
This file contains 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,39 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Neos.Flow package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Neos\Flow\Mvc\Routing; | ||
|
||
use Neos\Flow\ObjectManagement\ObjectManagerInterface; | ||
use Neos\Flow\Reflection\ReflectionService; | ||
|
||
class AttributeRoutesProviderFactory implements RoutesProviderFactoryInterface | ||
{ | ||
public function __construct( | ||
public readonly ReflectionService $reflectionService, | ||
public readonly ObjectManagerInterface $objectManager, | ||
) { | ||
} | ||
|
||
/** | ||
* @param array<string, mixed> $options | ||
*/ | ||
public function createRoutesProvider(array $options): RoutesProviderInterface | ||
{ | ||
return new AttributeRoutesProvider( | ||
$this->reflectionService, | ||
$this->objectManager, | ||
$options['classNames'] ?? [], | ||
); | ||
} | ||
} |
This file contains 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
23 changes: 23 additions & 0 deletions
23
Neos.Flow/Classes/Mvc/Routing/RoutesProviderFactoryInterface.php
This file contains 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 | ||
|
||
/* | ||
* This file is part of the Neos.Flow package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Neos\Flow\Mvc\Routing; | ||
|
||
interface RoutesProviderFactoryInterface | ||
{ | ||
/** | ||
* @param array<string, mixed> $options | ||
*/ | ||
public function createRoutesProvider(array $options): RoutesProviderInterface; | ||
} |
This file contains 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 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
Oops, something went wrong.