From 1b8090338db2f563ce7b368a87b53d41243b88a3 Mon Sep 17 00:00:00 2001 From: Olivier Laviale Date: Thu, 23 Mar 2023 01:16:06 +0100 Subject: [PATCH] Add Route attributes --- lib/ActionResolver.php | 73 ++++++++++++++++++++++++++++++ lib/Attribute/Delete.php | 18 ++++++++ lib/Attribute/Get.php | 18 ++++++++ lib/Attribute/Post.php | 18 ++++++++ lib/Attribute/Put.php | 18 ++++++++ lib/Attribute/Route.php | 28 ++++++++++++ lib/AttributeCompilerPass.php | 51 ++++++++++----------- lib/ConfigBuilder.php | 60 ++++++++++++++++++++---- tests/app/all/config/services.yml | 2 + tests/lib/Acme/ImageController.php | 42 +++++++++++++++++ tests/lib/ConfigBuilderTest.php | 36 +++++++++++++++ tests/lib/ContainerTest.php | 8 ++++ 12 files changed, 336 insertions(+), 36 deletions(-) create mode 100644 lib/ActionResolver.php create mode 100644 lib/Attribute/Delete.php create mode 100644 lib/Attribute/Get.php create mode 100644 lib/Attribute/Post.php create mode 100644 lib/Attribute/Put.php create mode 100644 lib/Attribute/Route.php create mode 100644 tests/lib/Acme/ImageController.php create mode 100644 tests/lib/ConfigBuilderTest.php diff --git a/lib/ActionResolver.php b/lib/ActionResolver.php new file mode 100644 index 0000000..47987a4 --- /dev/null +++ b/lib/ActionResolver.php @@ -0,0 +1,73 @@ +value) . '_'; + + if (str_starts_with($try, $method)) { + $method = substr($method, strlen($try)); + + break; + } + } + + return $method; + } +} diff --git a/lib/Attribute/Delete.php b/lib/Attribute/Delete.php new file mode 100644 index 0000000..0b02332 --- /dev/null +++ b/lib/Attribute/Delete.php @@ -0,0 +1,18 @@ +process_action_responders($container); + $this->process_routes($container); $this->process_actions($container); } @@ -48,14 +54,20 @@ private function process_action_responders(ContainerBuilder $container): void /** * Configures tag `{ name: action_alias, action: X }` from methods with the attribute {@link Action}. */ - private function process_actions(ContainerBuilder $container): void + private function process_routes(ContainerBuilder $container): void { - $target_methods = Attributes::findTargetMethods(Action::class); + $target_methods = [ + ...Attributes::findTargetMethods(Get::class), + ...Attributes::findTargetMethods(Post::class), + ...Attributes::findTargetMethods(Put::class), + ...Attributes::findTargetMethods(Delete::class), + ...Attributes::findTargetMethods(Route::class), + ]; foreach ($target_methods as $method) { $class = $method->class; $attribute = $method->attribute; - $action = $attribute->action ?? $this->resolve_action($class, $method->name); + $action = $attribute->action ?? ActionResolver::resolve_action($class, $method->name); $definition = $container->findDefinition($class); $definition->addTag(ActionAliasCompilerPass::TAG, [ ActionAliasCompilerPass::TAG_KEY => $action ]); @@ -63,34 +75,19 @@ private function process_actions(ContainerBuilder $container): void } /** - * @param class-string $class + * Configures tag `{ name: action_alias, action: X }` from methods with the attribute {@link Action}. */ - private function resolve_action(string $class, string $method): string - { - $name = $this->extract_action_name($method); - $unqualified_class = substr($class, strrpos($class, '\\') + 1); - - if (str_ends_with($unqualified_class, self::CONTROLLER_SUFFIX)) { - $unqualified_class = substr($unqualified_class, 0, -strlen(self::CONTROLLER_SUFFIX)); - } - - $base = pluralize(hyphenate($unqualified_class)); - - return "$base:$name"; - } - - private function extract_action_name(string $method): string + private function process_actions(ContainerBuilder $container): void { - foreach (RequestMethod::cases() as $case) { - $try = strtolower($case->value) . '_'; + $target_methods = Attributes::findTargetMethods(Action::class); - if (str_starts_with($try, $method)) { - $method = substr($method, strlen($try)); + foreach ($target_methods as $method) { + $class = $method->class; + $attribute = $method->attribute; + $action = $attribute->action ?? ActionResolver::resolve_action($class, $method->name); - break; - } + $definition = $container->findDefinition($class); + $definition->addTag(ActionAliasCompilerPass::TAG, [ ActionAliasCompilerPass::TAG_KEY => $action ]); } - - return $method; } } diff --git a/lib/ConfigBuilder.php b/lib/ConfigBuilder.php index 12db9ff..245f789 100644 --- a/lib/ConfigBuilder.php +++ b/lib/ConfigBuilder.php @@ -1,19 +1,20 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace ICanBoogie\Binding\Routing; +use ICanBoogie\Binding\Routing\Attribute\Delete; +use ICanBoogie\Binding\Routing\Attribute\Get; +use ICanBoogie\Binding\Routing\Attribute\Post; +use ICanBoogie\Binding\Routing\Attribute\Put; +use ICanBoogie\Binding\Routing\Attribute\Route; use ICanBoogie\Config\Builder; use ICanBoogie\Routing\RouteCollector; use ICanBoogie\Routing\RouteProvider; +use LogicException; +use olvlvl\ComposerAttributeCollector\Attributes; + +use function class_exists; +use function sprintf; /** * A config builder for 'routes' fragments. @@ -31,4 +32,45 @@ public function build(): RouteProvider { return $this->collect(); } + + /** + * Builds configuration from the {@link Route} attribute. + * + * @return $this + */ + public function from_attributes(): self + { + if (!class_exists(Attributes::class)) { + throw new LogicException( + sprintf( + "Unable to build from attributes, the class %s is not available", + Attributes::class + ) + ); + } + + $target_methods = [ + ...Attributes::findTargetMethods(Get::class), + ...Attributes::findTargetMethods(Post::class), + ...Attributes::findTargetMethods(Put::class), + ...Attributes::findTargetMethods(Delete::class), + ...Attributes::findTargetMethods(Route::class), + ]; + + foreach ($target_methods as $method) { + /** @var Route $attribute */ + $attribute = $method->attribute; + $action = $attribute->action + ?? ActionResolver::resolve_action($method->class, $method->name); + + $this->route( + pattern: $attribute->pattern, + action: $action, + methods: $attribute->methods, + id: $attribute->id + ); + } + + return $this; + } } diff --git a/tests/app/all/config/services.yml b/tests/app/all/config/services.yml index bbafd70..d14addf 100644 --- a/tests/app/all/config/services.yml +++ b/tests/app/all/config/services.yml @@ -10,6 +10,8 @@ services: # - { name: action_alias, action: 'articles:home' } # - { name: action_alias, action: 'articles:show' } + Test\ICanBoogie\Binding\Routing\Acme\ImageController: ~ + Test\ICanBoogie\Binding\Routing\Acme\PageController: tags: - { name: action_responder } diff --git a/tests/lib/Acme/ImageController.php b/tests/lib/Acme/ImageController.php new file mode 100644 index 0000000..ee5249c --- /dev/null +++ b/tests/lib/Acme/ImageController.php @@ -0,0 +1,42 @@ +from_attributes() + ->build(); + + $expected = [ + + 'images:list' => new Route('/images.html', 'images:list', methods: RequestMethod::METHOD_GET), + 'images:show' => new Route('/images/:id.html', 'images:show', methods: RequestMethod::METHOD_GET), + 'images:create' => new Route('/images', 'images:create', methods: RequestMethod::METHOD_POST), + 'images:update' => new Route('/images/:id', 'images:update', methods: RequestMethod::METHOD_PUT), + 'images:delete' => new Route('/images/:id', 'images:delete', methods: RequestMethod::METHOD_DELETE), + + ]; + + foreach ($expected as $action => $exp) { + $route = $config->route_for_predicate(new ByAction($action)); + + $this->assertNotNull($route, "No match for action $action"); + $this->assertEquals($exp, $route); + } + } +} diff --git a/tests/lib/ContainerTest.php b/tests/lib/ContainerTest.php index e4ae422..bb5c9e3 100644 --- a/tests/lib/ContainerTest.php +++ b/tests/lib/ContainerTest.php @@ -18,6 +18,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerInterface; use Test\ICanBoogie\Binding\Routing\Acme\ArticleController; +use Test\ICanBoogie\Binding\Routing\Acme\ImageController; use Test\ICanBoogie\Binding\Routing\Acme\PageController; use function ICanBoogie\app; @@ -60,6 +61,11 @@ public function test_parameter(): void 'articles:home' => ArticleController::class, 'articles:show' => ArticleController::class, 'articles:create' => ArticleController::class, + 'images:list' => ImageController::class, + 'images:show' => ImageController::class, + 'images:create' => ImageController::class, + 'images:update' => ImageController::class, + 'images:delete' => ImageController::class, 'pages:about' => PageController::class, 'api:ping' => PingController::class, ], $actual); @@ -89,6 +95,8 @@ public static function provide_responder_provider(): array [ 'articles:show', ArticleController::class ], [ 'articles:create', ArticleController::class ], [ 'pages:about', PageController::class ], + [ 'images:list', ImageController::class ], + [ 'images:show', ImageController::class ], ]; }