Skip to content

Commit

Permalink
Add the 'Action' attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
olvlvl committed Mar 21, 2023
1 parent 9c32d86 commit 95d61c3
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 4 deletions.
48 changes: 48 additions & 0 deletions README.md
Expand Up @@ -55,6 +55,54 @@ namespace ICanBoogie;
$routes = $app->config_for_class(Routing\RouteProvider::class);
```

### Matching routes with controllers

Routes have no idea of the controller to use, to match a route with a controller, you need to tag
the controller with the actions that it supports.

The following example demonstrates how `ArticleControler` is configured to handle the actions
`articles:show` and `articles:list`.

```yaml
services:
_defaults:
autowire: true

App\Presentation\HTTP\Controller\ArticleController:
shared: false
tags:
- { name: action_responder }
- { name: action_alias, action: 'articles:list' }
- { name: action_alias, action: 'articles:show' }
```

Alternatively, you can add the `Action` attribute to your action methods:

```php
<?php

namespace App\Presentation\HTTP\Controller;

use ICanBoogie\Binding\Routing\Action;

final class ArticleController
{
// ...

#[Action]
private function list(): void
{
// ...
}

#[Action]
private function show(): void
{
// ...
}
}
```



----------
Expand Down
11 changes: 9 additions & 2 deletions composer.json
Expand Up @@ -22,7 +22,10 @@
"source": "https://github.com/ICanBoogie/bind-routing"
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"olvlvl/composer-attribute-collector": true
}
},
"minimum-stability": "dev",
"prefer-stable": true,
Expand All @@ -34,6 +37,7 @@
},
"require-dev": {
"icanboogie/console": "^6.0",
"olvlvl/composer-attribute-collector": "^1.1",
"phpstan/phpstan": "^1.5",
"phpunit/phpunit": "^9.5"
},
Expand All @@ -45,7 +49,10 @@
"autoload-dev": {
"psr-4": {
"Test\\ICanBoogie\\Binding\\Routing\\": "tests/lib"
}
},
"files": [
"vendor/attributes.php"
]
},
"scripts": {
"post-autoload-dump": "ICanBoogie\\Autoconfig\\Hooks::on_autoload_dump"
Expand Down
2 changes: 2 additions & 0 deletions config/container.php
Expand Up @@ -10,7 +10,9 @@
*/

use ICanBoogie\Binding\Routing\ActionAliasCompilerPass;
use ICanBoogie\Binding\Routing\AttributeCompilerPass;
use ICanBoogie\Binding\SymfonyDependencyInjection\ConfigBuilder;

return fn(ConfigBuilder $config) => $config
->add_compiler_pass(AttributeCompilerPass::class)
->add_compiler_pass(ActionAliasCompilerPass::class);
17 changes: 17 additions & 0 deletions lib/Action.php
@@ -0,0 +1,17 @@
<?php

namespace ICanBoogie\Binding\Routing;

use Attribute;

/**
* Used by {@link AttributeCompilerPass} to tag services with `action_alias`.
*/
#[Attribute(Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
final class Action
{
public function __construct(
public readonly ?string $action = null,
) {
}
}
81 changes: 81 additions & 0 deletions lib/AttributeCompilerPass.php
@@ -0,0 +1,81 @@
<?php

namespace ICanBoogie\Binding\Routing;

use ICanBoogie\HTTP\RequestMethod;
use olvlvl\ComposerAttributeCollector\Attributes;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

use function class_exists;
use function ICanBoogie\hyphenate;
use function ICanBoogie\pluralize;
use function str_ends_with;
use function str_starts_with;
use function strlen;
use function strrpos;
use function strtolower;
use function substr;

final class AttributeCompilerPass implements CompilerPassInterface
{
private const CONTROLLER_SUFFIX = 'Controller';

public function process(ContainerBuilder $container)
{
if (!class_exists(Attributes::class)) {
return;
}

$this->process_actions($container);
}

/**
* Configures tag `{ name: action_alias, action: X }` from methods with the attribute {@link Action}.
*/
private function process_actions(ContainerBuilder $container): void
{
$target_methods = Attributes::findTargetMethods(Action::class);

foreach ($target_methods as $method) {
$class = $method->class;
$attribute = $method->attribute;
$action = $attribute->action ?? $this->resolve_action($class, $method->name);

$definition = $container->findDefinition($class);
$definition->addTag(ActionAliasCompilerPass::TAG, [ ActionAliasCompilerPass::TAG_KEY => $action ]);
}
}

/**
* @param class-string $class
*/
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
{
foreach (RequestMethod::cases() as $case) {
$try = strtolower($case->value) . '_';

if (str_starts_with($try, $method)) {
$method = substr($method, strlen($try));

break;
}
}

return $method;
}
}
5 changes: 3 additions & 2 deletions tests/app/all/config/services.yml
Expand Up @@ -6,8 +6,9 @@ services:
Test\ICanBoogie\Binding\Routing\Acme\ArticleController:
tags:
- { name: action_responder }
- { name: action_alias, action: 'articles:home' }
- { name: action_alias, action: 'articles:show' }
# We use attributes for these
# - { name: action_alias, action: 'articles:home' }
# - { name: action_alias, action: 'articles:show' }

Test\ICanBoogie\Binding\Routing\Acme\PageController:
tags:
Expand Down
8 changes: 8 additions & 0 deletions tests/lib/Acme/ArticleController.php
Expand Up @@ -11,14 +11,22 @@

namespace Test\ICanBoogie\Binding\Routing\Acme;

use ICanBoogie\Binding\Routing\Action;
use ICanBoogie\HTTP\Request;
use ICanBoogie\Routing\ControllerAbstract;
use ICanBoogie\Routing\Route;

final class ArticleController extends ControllerAbstract
{
#[Action('articles:show')]
#[Action('articles:create')]
protected function action(Request $request): string
{
return $request->context->get(Route::class)->action;
}

#[Action]
protected function home(): void
{
}
}
1 change: 1 addition & 0 deletions tests/lib/ContainerTest.php
Expand Up @@ -61,6 +61,7 @@ public function test_parameter(): void
$this->assertEquals([
'articles:home' => ArticleController::class,
'articles:show' => ArticleController::class,
'articles:create' => ArticleController::class,
'page:about' => PageController::class,
'api:ping' => PingController::class,
], $actual);
Expand Down

0 comments on commit 95d61c3

Please sign in to comment.