Skip to content
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

FEATURE: Add Flow\Route Attribute/Annotation #3325

Merged
merged 18 commits into from
Mar 28, 2024
Merged
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5ddd46a
FEATURE: Add `Flow\Route` Attribute/Annotation
mficzel Mar 3, 2024
6877a05
TASK: Refactor to include annotation routes via `provider` and `provi…
mficzel Mar 15, 2024
e0a8e4a
DOCS: Document `Flow\Route` annotations and the Settings `Neos.Flow.m…
mficzel Mar 18, 2024
4e635ca
TASK: Adjust tests for AnnotationRoutesProvider and ConfigurationRout…
mficzel Mar 18, 2024
adaf786
Task: Refactor RouteProviderWithOptions interface RouteProviderFactor…
mficzel Mar 18, 2024
4236241
Apply suggestions from code review
mficzel Mar 18, 2024
95917f8
Apply suggestions from code review
mficzel Mar 22, 2024
8d073a8
TASK: Remove `@format` from default route values
mficzel Mar 22, 2024
1a0e2f3
TASK: Filter out '@package', '@subpackage', '@controller', '@action' …
mficzel Mar 22, 2024
bceedfc
TASK: Adjust documentation to use uppercase HTTP-Verbs and explain At…
mficzel Mar 22, 2024
43aa1c5
TASK: Check for {@controller} or {@action} in path
mficzel Mar 22, 2024
ff5d05d
TASK: Move check for '@package', '@subpackage', '@controller', '@acti…
mficzel Mar 22, 2024
360a031
TASK: Make test green again
mficzel Mar 22, 2024
158d4dc
TASK: Add (failing) unit test for `#[Flow\Route]` constraints
mhsdesign Mar 27, 2024
794ac4a
TASK: Fix constraints for `#[Flow\Route]` annotation
mhsdesign Mar 27, 2024
f8f90df
TASK: Adjust documentation of `#[Flow\Route]`
mhsdesign Mar 27, 2024
ee8a812
TASK: Restrict `#[Flow\Route]` to `ActionController`'s
mhsdesign Mar 27, 2024
77fcaea
Clarify configuration keys
kitsunet Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
TASK: Restrict #[Flow\Route] to ActionController's
mhsdesign committed Mar 27, 2024
commit ee8a8127e1d77908b5f44786a1ba4d6f2500c583
50 changes: 33 additions & 17 deletions Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@

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;
@@ -24,33 +25,43 @@
/**
* Allows to annotate controller methods with route configurations
*
* Implementation:
* 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.
* By convention and implementation of the default ActionController inside processRequest
* {@see \Neos\Flow\Mvc\Controller\ActionController::callActionMethod()} will be used to concatenate the "Action" suffix
* to the action name and invoke it internally with prepared method arguments.
* The \@action is just another routing value while the dispatcher does not really know about "actions" from the "outside".
* 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.
* As not every FQ class name is representable via the routing configuration (e.g. the class has to end with "Controller"),
* 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.
*
* The routing annotation is placed at methods.
* It is validated that the annotated method ends with "Action" and a routing value with the suffix trimmed will be generated.
* Using the annotations on any controller makes the assumption that the controller will delegate the request to the dedicate
* action by depending "Action".
* This thesis is true for the ActionController.
* 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
*
* As discussed in https://discuss.neos.io/t/rfc-future-of-routing-mvc-in-flow/6535 we want to refactor the routing values
* to include the fully qualified controller name, so it can be easier generated without strong restrictions.
* Additionally, the action mapping should include its full name and be guaranteed to called.
* Either by invoking the action in the dispatcher or by documenting this feature as part of a implementation of a ControllerInterface
* TODO for a future scope of `Flow\Action` see {@link https://github.com/neos/flow-development-collection/issues/3335}
*/
final class AttributeRoutesProvider implements RoutesProviderInterface
{
@@ -80,6 +91,11 @@ public function getRoutes(): Routes
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);
Original file line number Diff line number Diff line change
@@ -753,6 +753,7 @@ Subroutes from Annotations
--------------------------

The ``Flow\Route`` attribute allows to define routes directly on the affected method.
(Currently only ActionController are supported https://github.com/neos/flow-development-collection/issues/3335)

.. code-block:: php

@@ -774,7 +775,7 @@ The ``Flow\Route`` attribute allows to define routes directly on the affected me
}

To find the annotation and tp specify the order of routes this has to be used together with the
`\Neos\Flow\Mvc\Routing\AttributeRoutesProviderFactory` as `providerFactory` in Setting `Neos.Flow.mvs.routes`
`\Neos\Flow\Mvc\Routing\AttributeRoutesProviderFactory` as `providerFactory` in Setting `Neos.Flow.mvc.routes`

.. code-block:: yaml

@@ -787,7 +788,7 @@ To find the annotation and tp specify the order of routes this has to be used to
providerFactory: \Neos\Flow\Mvc\Routing\AttributeRoutesProviderFactory
providerOptions:
classNames:
- Vendor\Example\Controller\ExampleController
- Vendor\Example\Controller\*

Route Loading Order and the Flow Application Context
====================================================
21 changes: 14 additions & 7 deletions Neos.Flow/Tests/Unit/Mvc/Routing/AttributeRoutesProviderTest.php
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ public function setUp(): void
$this->annotationRoutesProvider = new Routing\AttributeRoutesProvider(
$this->mockReflectionService,
$this->mockObjectManager,
['Vendor\Example\Controller\ExampleController']
['Vendor\\Example\\Controller\\*']
);
}

@@ -61,19 +61,26 @@ public function noAnnotationsYieldEmptyRoutes(): void
*/
public function routesFromAnnotationAreCreatedWhenClassNamesMatch(): void
{
$exampleFqnControllerName = 'Vendor\\Example\\Controller\\ExampleController';
eval('
namespace Vendor\Example\Controller;
class ExampleController extends \Neos\Flow\Mvc\Controller\ActionController {
}'
);

$this->mockReflectionService->expects($this->once())
->method('getClassesContainingMethodsAnnotatedWith')
->with(Flow\Route::class)
->willReturn(['Vendor\Example\Controller\ExampleController']);
->willReturn([$exampleFqnControllerName]);

$this->mockReflectionService->expects($this->once())
->method('getMethodsAnnotatedWith')
->with('Vendor\Example\Controller\ExampleController', Flow\Route::class)
->with($exampleFqnControllerName, Flow\Route::class)
->willReturn(['specialAction']);

$this->mockReflectionService->expects($this->once())
->method('getMethodAnnotations')
->with('Vendor\Example\Controller\ExampleController', 'specialAction', Flow\Route::class)
->with($exampleFqnControllerName, 'specialAction', Flow\Route::class)
->willReturn([
new Flow\Route(uriPattern: 'my/path'),
new Flow\Route(
@@ -86,12 +93,12 @@ public function routesFromAnnotationAreCreatedWhenClassNamesMatch(): void

$this->mockObjectManager->expects($this->once())
->method('getCaseSensitiveObjectName')
->with('Vendor\Example\Controller\ExampleController')
->willReturn('Vendor\Example\Controller\ExampleController');
->with($exampleFqnControllerName)
->willReturn($exampleFqnControllerName);

$this->mockObjectManager->expects($this->once())
->method('getPackageKeyByObjectName')
->with('Vendor\Example\Controller\ExampleController')
->with($exampleFqnControllerName)
->willReturn('Vendor.Example');

$expectedRoute1 = new Route();