Skip to content

Commit

Permalink
FEATURE: Add Flow\Route Attribute/Annotation
Browse files Browse the repository at this point in the history
The `Flow\Route` attribute allows to define routes directly on the affected method.
This allows to avoid dealing with Routes.yaml in projects in simple cases where is sometimes is annoying to look up the exact syntax for that.

Hint: While this is a very convenient way to add routes in project code it should not be used in libraries/packages that expect to be configured for the outside.
In such cases the Routes.yaml is still preferred as it is easier to overwrite.

Usage:

```php
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\Annotations as Flow;

class ExampleController extends ActionController
{
    #[Flow\Route(uriPattern:'my/path', httpMethods: ['get'])]
    public function someAction(): void
    {
    }

    #[Flow\Route(uriPattern:'my/other/b-path', defaults: ['test' => 'b'])]
    #[Flow\Route(uriPattern:'my/other/c-path', defaults: ['test' => 'c'])]
    public function otherAction(): void
    {
    }
}
```

The package: `WebSupply.RouteAnnotation` by @sorenmalling implemented similar ideas earlier.

Resolves: #2059
  • Loading branch information
mficzel committed Mar 3, 2024
1 parent 5b8bcba commit 3f88955
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 2 deletions.
40 changes: 40 additions & 0 deletions Neos.Flow/Classes/Annotations/Route.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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;
use Neos\Flow\Security\Authorization\Privilege\PrivilegeInterface;

/**
* 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 class Route
{
public function __construct(
public readonly string $uriPattern,
public readonly ?string $name = null,
public readonly ?array $httpMethods = null,
public readonly ?array $defaults = null,
) {
}
}
2 changes: 1 addition & 1 deletion Neos.Flow/Classes/Command/RoutingCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function listCommand(): void
*/
public function showCommand(int $index): void
{
$route = $this->routesProvider->getRoutes()[$index - 1] ?? null;
$route = iterator_to_array($this->routesProvider->getRoutes()->getIterator())[$index - 1] ?? null;
if ($route === null) {
$this->outputLine('<error>Route %d was not found!</error>', [$index]);
$this->outputLine('Run <i>./flow routing:list</i> to show all registered routes');
Expand Down
85 changes: 85 additions & 0 deletions Neos.Flow/Classes/Mvc/Routing/AnnotationRoutesProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace Neos\Flow\Mvc\Routing;

use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Flow\Reflection\ReflectionService;
use Neos\Flow\Annotations as Flow;
use Neos\Utility\Arrays;

/**
* @Flow\Scope("singleton")
*/
class AnnotationRoutesProvider implements RoutesProviderInterface
{
public function __construct(
public readonly ReflectionService $reflectionService,
public readonly ObjectManagerInterface $objectManager,
) {
}

public function getRoutes(): Routes
{
$routes = [];
$annotatedClasses = $this->reflectionService->getClassesContainingMethodsAnnotatedWith(Flow\Route::class);
foreach ($annotatedClasses as $className) {
$controllerObjectName = $this->objectManager->getCaseSensitiveObjectName($className);
$controllerPackageKey = $this->objectManager->getPackageKeyByObjectName($controllerObjectName);
$controllerPackageNamespace = str_replace('.', '\\', $controllerPackageKey);
if (!str_ends_with($className, 'Controller')) {
throw new \Exception('only for controller classes');
}
if (!str_starts_with($className, $controllerPackageNamespace . '\\')) {
throw new \Exception('only for classes in package namespace');
}

$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 \Exception('unknown controller pattern');
}

$annotatedMethods = $this->reflectionService->getMethodsAnnotatedWith($className, Flow\Route::class);
// @todo remove once reflectionService handles multiple annotations properly
$annotatedMethods = array_unique($annotatedMethods);
foreach ($annotatedMethods as $methodName) {
if (!str_ends_with($methodName, 'Action')) {
throw new \Exception('only for action methods');
}
$annotations = $this->reflectionService->getMethodAnnotations($className, $methodName, Flow\Route::class);
foreach ($annotations as $annotation) {
if ($annotation instanceof Flow\Route) {
$configuration = [
'uriPattern' => $annotation->uriPattern,
'defaults' => Arrays::arrayMergeRecursiveOverrule(
[
'@package' => $controllerPackageKey,
'@subpackage' => $subPackage,
'@controller' => substr($controllerName, 0, -10),
'@action' => substr($methodName, 0, -6),
'@format' => 'html'
],
$annotation->defaults ?? []
)
];
if ($annotation->name !== null) {
$configuration['name'] = $annotation->name;
}
if ($annotation->httpMethods !== null) {
$configuration['httpMethods'] = $annotation->httpMethods;
}
$routes[] = Route::fromConfiguration($configuration);
}
}
}
}
return Routes::create(...$routes);
}
}
17 changes: 17 additions & 0 deletions Neos.Flow/Classes/Mvc/Routing/CombinedRoutesProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Neos\Flow\Mvc\Routing;

class CombinedRoutesProvider implements RoutesProviderInterface
{
public function __construct(
public readonly ConfigurationRoutesProvider $configurationRoutesProvider,
public readonly AnnotationRoutesProvider $annotationRoutesProvider,
) {
}

public function getRoutes(): Routes
{
return $this->annotationRoutesProvider->getRoutes()->merge($this->configurationRoutesProvider->getRoutes());
}
}
2 changes: 1 addition & 1 deletion Neos.Flow/Configuration/Objects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ Neos\Flow\Mvc\Routing\RouterInterface:
className: Neos\Flow\Mvc\Routing\Router

Neos\Flow\Mvc\Routing\RoutesProviderInterface:
className: Neos\Flow\Mvc\Routing\ConfigurationRoutesProvider
className: Neos\Flow\Mvc\Routing\CombinedRoutesProvider

Neos\Flow\Mvc\Routing\RouterCachingService:
properties:
Expand Down

0 comments on commit 3f88955

Please sign in to comment.