Skip to content

Commit

Permalink
Automatically register controllers as services
Browse files Browse the repository at this point in the history
  • Loading branch information
olvlvl committed Mar 25, 2023
1 parent fd1b96e commit 53f4703
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 107 deletions.
21 changes: 6 additions & 15 deletions README.md
Expand Up @@ -19,9 +19,9 @@ composer require icanboogie/bind-routing

## Defining routes using attributes

The easiest way to define routes is to use routing attributes such as [Route][] or [Get][] to tag your controller actions. Using any of these tags on a controller triggers the tagging of the controller as an action responder, so you don't have to use the [ActionResponder][] attribute.
The easiest way to define routes is to use routing attributes such as [Route][] or [Get][] to tag your controller and actions. Using any of these tags triggers the registration of the controller as a service (if it is not already registered), and the tagging with `action_responder` and `action_alias`.

The following example demonstrates how the [Route][] attribute can be used on a class to specify a prefix for all the actions of a controller. The [Get][] and [Post][] attributes are used to tag actions. If left undefined, the action is inferred from the controller class and the method name.
The following example demonstrates how the [Route][] attribute can be used at the class level to specify a prefix for all the actions of a controller. The [Get][] and [Post][] attributes are used to tag actions. If left undefined, the action is inferred from the controller class and the method name.

```php
<?php
Expand Down Expand Up @@ -58,18 +58,7 @@ final SkillController extends ControllerAbstract
}
```

This would be the container configuration:

```yaml
services:
_defaults:
autowire: true

App\Presentation\HTTP\SkillController:
shared: false
```

Using the `from_attributes()` method, the config builder can collect all these attributes to configure itself.
Using the `from_attributes()` method, the config builder can collect these attributes to configure itself.

```php
<?php
Expand All @@ -82,9 +71,11 @@ use ICanBoogie\Binding\Routing\ConfigBuilder;
return fn(ConfigBuilder $config) => $config->from_attributes();
```



## Defining routes using configuration fragments

Alternatively, you can configure routes manually using `routes` configuration fragments.
Alternatively, you can configure routes manually using `routes` configuration fragments, but you will have to register the service and tag it with `action_responder` and `action_alias`.

The following example demonstrates how to define routes, resource routes. The pattern of the `articles:show` route is overridden to use _year_, _month_ and _slug_.

Expand Down
4 changes: 2 additions & 2 deletions config/container.php
Expand Up @@ -10,9 +10,9 @@
*/

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

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

namespace ICanBoogie\Binding\Routing;

use ICanBoogie\Binding\Routing\Attribute\ActionResponder;
use ICanBoogie\Binding\Routing\Attribute\Route;
use olvlvl\ComposerAttributeCollector\Attributes;
use olvlvl\ComposerAttributeCollector\TargetMethod;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

use function array_fill_keys;
use function array_keys;
use function class_exists;
use function ICanBoogie\iterable_to_groups;
use function in_array;

/**
* Registers controller services, and set the tags 'action_responder' and 'action_alias' according to attributes.
*/
final class ActionResponderCompilerPass implements CompilerPassInterface
{
public const TAG = 'action_responder';

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

$target_methods_by_class = $this->find_target_methods_by_class();
$this->process_controllers($container, $target_methods_by_class);
}

/**
* @return array<class-string, iterable<TargetMethod<Route>>>
*/
private function find_target_methods_by_class(): array
{
/** @phpstan-ignore-next-line */
return iterable_to_groups(
Attributes::filterTargetMethods(
Attributes::predicateForAttributeInstanceOf(Route::class)
),
fn(TargetMethod $t) => $t->class
);
}

/**
* Registers controllers with the class attributes {@link Route} or {@link ActionResponder}.
*
* @param array<class-string, iterable<TargetMethod<Route>>> $target_methods_by_class
*/
private function process_controllers(ContainerBuilder $container, array $target_methods_by_class): void
{
foreach ($this->find_controllers_classes(array_keys($target_methods_by_class)) as $class) {
$definition = $this->ensure_definition($class, $container);

if (!$definition->hasTag(self::TAG)) {
$definition->addTag(self::TAG);
}

$this->tag_aliases($definition, $target_methods_by_class[$class] ?? []);
}
}

/**
* @param iterable<TargetMethod<Route>> $target_methods
*/
private function tag_aliases(Definition $definition, iterable $target_methods): void
{
foreach ($target_methods as $tm) {
$action = $tm->attribute->action
?? ActionResolver::resolve_action($tm->class, $tm->name);

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

/**
* Ensures the controller service is defined.
*
* @param class-string $class
*/
private function ensure_definition(string $class, ContainerBuilder $container): Definition
{
if ($container->hasDefinition($class)) {
return $container->getDefinition($class);
}

$definition = new Definition($class);
$definition->setAutowired(true);
$definition->setShared(false);

$container->setDefinition($class, $definition);

return $definition;
}

/**
* @param array<class-string> $classes
* Classes already found from target methods.
*
* @return array<class-string>
*/
private function find_controllers_classes(array $classes): array
{
$classes = array_fill_keys($classes, true);

$target_classes = Attributes::filterTargetClasses(
fn(string $attribute): bool => in_array($attribute, [ Route::class, ActionResponder::class ])
);

foreach ($target_classes as $target_class) {
$classes[$target_class->name] = true;
}

return array_keys($classes);
}
}
2 changes: 1 addition & 1 deletion lib/Attribute/ActionResponder.php
Expand Up @@ -5,7 +5,7 @@
use Attribute;

/**
* Used by {@link AttributeCompilerPass} to tag services with `action_responder`.
* Used by {@link ActionResponderCompilerPass} to tag services with `action_responder`.
*/
#[Attribute(Attribute::TARGET_CLASS)]
final class ActionResponder
Expand Down
84 changes: 0 additions & 84 deletions lib/AttributeCompilerPass.php

This file was deleted.

12 changes: 7 additions & 5 deletions tests/app/all/config/services.yml
Expand Up @@ -3,15 +3,17 @@ services:
public: true
autowire: true

Test\ICanBoogie\Binding\Routing\Acme\ArticleController:
# We use attributes for these
# Because they use route attributes, these controllers are registered automatically and tagged with 'action_responder'
# and 'action_alias'.
#
# Test\ICanBoogie\Binding\Routing\Acme\ArticleController:
# tags:
# - { name: action_responder }
# - { name: action_alias, action: 'articles:home' }
# - { name: action_alias, action: 'articles:show' }

Test\ICanBoogie\Binding\Routing\Acme\ImageController: ~
Test\ICanBoogie\Binding\Routing\Acme\SkillController: ~
#
# Test\ICanBoogie\Binding\Routing\Acme\ImageController: ~
# Test\ICanBoogie\Binding\Routing\Acme\SkillController: ~

Test\ICanBoogie\Binding\Routing\Acme\PageController:
tags:
Expand Down

0 comments on commit 53f4703

Please sign in to comment.