Skip to content

Commit

Permalink
Add Route attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
olvlvl committed Mar 23, 2023
1 parent c2eb077 commit 1b80903
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 36 deletions.
73 changes: 73 additions & 0 deletions lib/ActionResolver.php
@@ -0,0 +1,73 @@
<?php

namespace ICanBoogie\Binding\Routing;

use ICanBoogie\HTTP\RequestMethod;

Check failure on line 5 in lib/ActionResolver.php

View workflow job for this annotation

GitHub Actions / phpcs

Header blocks must not contain blank lines

use ICanBoogie\Routing\RouteMaker;
use RuntimeException;

use function ICanBoogie\hyphenate;
use function ICanBoogie\pluralize;
use function in_array;
use function strlen;
use function strrpos;
use function strtolower;
use function substr;

final class ActionResolver
{
private const CONTROLLER_SUFFIX = 'Controller';
private const ACTIONS = [

RouteMaker::ACTION_LIST,
RouteMaker::ACTION_NEW,
RouteMaker::ACTION_CREATE,
RouteMaker::ACTION_SHOW,
RouteMaker::ACTION_EDIT,
RouteMaker::ACTION_UPDATE,
RouteMaker::ACTION_DELETE,

];

/**
* @param class-string $class
*/
public static function resolve_action(string $class, string $method): string
{
$name = self::extract_action_name($method);

if (!$name) {
throw new RuntimeException("Unable to resolve action name from $class::$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 static function extract_action_name(string $method): string
{
if (in_array($method, self::ACTIONS)) {
return $method;
}

foreach (RequestMethod::cases() as $case) {
$try = strtolower($case->value) . '_';

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

break;
}
}

return $method;
}
}
18 changes: 18 additions & 0 deletions lib/Attribute/Delete.php
@@ -0,0 +1,18 @@
<?php

namespace ICanBoogie\Binding\Routing\Attribute;

use Attribute;
use ICanBoogie\HTTP\RequestMethod;

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Delete extends Route
{
public function __construct(
string $pattern,
string|null $action = null,
string|null $id = null,
) {
parent::__construct($pattern, $action, RequestMethod::METHOD_DELETE, $id);
}
}
18 changes: 18 additions & 0 deletions lib/Attribute/Get.php
@@ -0,0 +1,18 @@
<?php

namespace ICanBoogie\Binding\Routing\Attribute;

use Attribute;
use ICanBoogie\HTTP\RequestMethod;

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Get extends Route
{
public function __construct(
string $pattern,
string|null $action = null,
string|null $id = null,
) {
parent::__construct($pattern, $action, RequestMethod::METHOD_GET, $id);
}
}
18 changes: 18 additions & 0 deletions lib/Attribute/Post.php
@@ -0,0 +1,18 @@
<?php

namespace ICanBoogie\Binding\Routing\Attribute;

use Attribute;
use ICanBoogie\HTTP\RequestMethod;

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Post extends Route
{
public function __construct(
string $pattern,
string|null $action = null,
string|null $id = null,
) {
parent::__construct($pattern, $action, RequestMethod::METHOD_POST, $id);
}
}
18 changes: 18 additions & 0 deletions lib/Attribute/Put.php
@@ -0,0 +1,18 @@
<?php

namespace ICanBoogie\Binding\Routing\Attribute;

use Attribute;
use ICanBoogie\HTTP\RequestMethod;

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Put extends Route
{
public function __construct(
string $pattern,
string|null $action = null,
string|null $id = null,
) {
parent::__construct($pattern, $action, RequestMethod::METHOD_PUT, $id);
}
}
28 changes: 28 additions & 0 deletions lib/Attribute/Route.php
@@ -0,0 +1,28 @@
<?php

namespace ICanBoogie\Binding\Routing\Attribute;

use Attribute;
use ICanBoogie\HTTP\RequestMethod;

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Route
{
/**
* @param string $pattern
* Pattern of the route.
* @param string|null $action
* Identifier of a qualified action. e.g. 'articles:show'.
* If it is not defined, the action might be resolved from the controller and the method.
* @param RequestMethod|RequestMethod[] $methods
* Request method(s) accepted by the respond.
* @param string|null $id
*/
public function __construct(
public readonly string $pattern,
public readonly string|null $action = null,
public readonly RequestMethod|array $methods = RequestMethod::METHOD_ANY,
public readonly string|null $id = null,
) {
}
}
51 changes: 24 additions & 27 deletions lib/AttributeCompilerPass.php
Expand Up @@ -2,6 +2,11 @@

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\HTTP\RequestMethod;
use olvlvl\ComposerAttributeCollector\Attributes;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
Expand Down Expand Up @@ -29,6 +34,7 @@ public function process(ContainerBuilder $container): void
}

$this->process_action_responders($container);
$this->process_routes($container);
$this->process_actions($container);
}

Expand All @@ -48,49 +54,40 @@ 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 ]);
}
}

/**
* @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;
}
}
60 changes: 51 additions & 9 deletions lib/ConfigBuilder.php
@@ -1,19 +1,20 @@
<?php

/*
* This file is part of the ICanBoogie package.
*
* (c) Olivier Laviale <olivier.laviale@gmail.com>
*
* 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.
Expand All @@ -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;
}
}
2 changes: 2 additions & 0 deletions tests/app/all/config/services.yml
Expand Up @@ -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 }
Expand Down
42 changes: 42 additions & 0 deletions tests/lib/Acme/ImageController.php
@@ -0,0 +1,42 @@
<?php

namespace Test\ICanBoogie\Binding\Routing\Acme;

use ICanBoogie\Binding\Routing\ActionResponder;
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\Routing\Controller\ActionTrait;
use ICanBoogie\Routing\ControllerAbstract;

#[ActionResponder]
final class ImageController extends ControllerAbstract
{
use ActionTrait;

#[Get('/images.html')]
public function list(): void
{
}

#[Get('/images/:id.html')]
public function show(): void
{
}

#[Post('/images')]
public function create(): void
{
}

#[Put('/images/:id')]
public function update(): void
{
}

#[Delete('/images/:id')]
public function delete(): void
{
}
}

0 comments on commit 1b80903

Please sign in to comment.