Skip to content

Commit

Permalink
Add RouteArgument attribute for Yii Hydrator (#237)
Browse files Browse the repository at this point in the history
* Add `RouteArgument` attribute for Yii Hydrator (#203)

(cherry picked from commit f3f1091)

* fix
  • Loading branch information
vjik authored Feb 20, 2024
1 parent 61c1c7f commit 1a8af5c
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 3.0.1 under development

- Enh #202: Add support for `psr/http-message` version `^2.0` (@vjik)
- New #203, #237: Add `RouteArgument` attribute for Yii Hydrator (@vjik)

## 3.0.0 February 17, 2023

Expand Down
9 changes: 9 additions & 0 deletions composer-require-checker.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"symbol-whitelist" : [
"Yiisoft\\Hydrator\\Attribute\\Parameter\\ParameterAttributeInterface",
"Yiisoft\\Hydrator\\Attribute\\Parameter\\ParameterAttributeResolverInterface",
"Yiisoft\\Hydrator\\AttributeHandling\\ParameterAttributeResolveContext",
"Yiisoft\\Hydrator\\AttributeHandling\\Exception\\UnexpectedAttributeException",
"Yiisoft\\Hydrator\\Result"
]
}
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"vimeo/psalm": "^4.30|^5.22",
"yiisoft/di": "^1.0",
"yiisoft/dummy-provider": "^1.0.0",
"yiisoft/hydrator": "^1.0",
"yiisoft/test-support": "^3.0"
},
"autoload": {
Expand All @@ -52,7 +53,8 @@
}
},
"suggest": {
"yiisoft/router-fastroute": "Router implementation based on nikic/FastRoute"
"yiisoft/router-fastroute": "Router implementation based on nikic/FastRoute",
"yiisoft/hydrator": "Needed to use `RouteArgument` attribute"
},
"extra": {
"config-plugin-options": {
Expand Down
27 changes: 27 additions & 0 deletions src/HydratorAttribute/RouteArgument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Router\HydratorAttribute;

use Attribute;
use Yiisoft\Hydrator\Attribute\Parameter\ParameterAttributeInterface;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
final class RouteArgument implements ParameterAttributeInterface
{
public function __construct(
private ?string $name = null
) {
}

public function getName(): ?string
{
return $this->name;
}

public function getResolver(): string
{
return RouteArgumentResolver::class;
}
}
41 changes: 41 additions & 0 deletions src/HydratorAttribute/RouteArgumentResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Router\HydratorAttribute;

use Yiisoft\Hydrator\Attribute\Parameter\ParameterAttributeInterface;
use Yiisoft\Hydrator\Attribute\Parameter\ParameterAttributeResolverInterface;
use Yiisoft\Hydrator\AttributeHandling\Exception\UnexpectedAttributeException;
use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext;
use Yiisoft\Hydrator\Result;
use Yiisoft\Router\CurrentRoute;

use function array_key_exists;

final class RouteArgumentResolver implements ParameterAttributeResolverInterface
{
public function __construct(
private CurrentRoute $currentRoute,
) {
}

public function getParameterValue(
ParameterAttributeInterface $attribute,
ParameterAttributeResolveContext $context,
): Result {
if (!$attribute instanceof RouteArgument) {
throw new UnexpectedAttributeException(RouteArgument::class, $attribute);
}

$arguments = $this->currentRoute->getArguments();

$name = $attribute->getName() ?? $context->getParameter()->getName();

if (array_key_exists($name, $arguments)) {
return Result::success($arguments[$name]);
}

return Result::fail();
}
}
100 changes: 100 additions & 0 deletions tests/HydratorAttribute/RouteArgumentTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Router\Tests\HydratorAttribute;

use PHPUnit\Framework\TestCase;
use ReflectionFunction;
use Yiisoft\Hydrator\ArrayData;
use Yiisoft\Hydrator\Attribute\Parameter\ToString;
use Yiisoft\Hydrator\AttributeHandling\Exception\UnexpectedAttributeException;
use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext;
use Yiisoft\Hydrator\AttributeHandling\ResolverFactory\ContainerAttributeResolverFactory;
use Yiisoft\Hydrator\Hydrator;
use Yiisoft\Hydrator\Result;
use Yiisoft\Router\CurrentRoute;
use Yiisoft\Router\HydratorAttribute\RouteArgument;
use Yiisoft\Router\HydratorAttribute\RouteArgumentResolver;
use Yiisoft\Router\Route as RouterRoute;
use Yiisoft\Test\Support\Container\SimpleContainer;

final class RouteArgumentTest extends TestCase
{
public function testBase(): void
{
$hydrator = $this->createHydrator([
'a' => 'one',
'b' => 'two',
'c' => 'three',
]);

$input = new class () {
#[RouteArgument('a')]
public string $a = '';
#[RouteArgument('b')]
public string $b = '';
#[RouteArgument]
public string $c = '';
};

$hydrator->hydrate($input);

$this->assertSame('one', $input->a);
$this->assertSame('two', $input->b);
$this->assertSame('three', $input->c);
}

public function testWithoutArguments(): void
{
$hydrator = $this->createHydrator([]);

$input = new class () {
#[RouteArgument('a')]
public string $a = '';
#[RouteArgument('b')]
public string $b = '';
#[RouteArgument]
public string $c = '';
};

$hydrator->hydrate($input);

$this->assertSame('', $input->a);
$this->assertSame('', $input->b);
$this->assertSame('', $input->c);
}

public function testUnexpectedAttributeException(): void
{
$resolver = new RouteArgumentResolver(new CurrentRoute());

$attribute = new ToString();
$context = $this->createParameterAttributeResolveContext();

$this->expectException(UnexpectedAttributeException::class);
$this->expectExceptionMessage('Expected "' . RouteArgument::class . '", but "' . ToString::class . '" given.');
$resolver->getParameterValue($attribute, $context);
}

private function createHydrator(array $arguments): Hydrator
{
$currentRoute = new CurrentRoute();
$currentRoute->setRouteWithArguments(RouterRoute::get('/'), $arguments);

return new Hydrator(
attributeResolverFactory: new ContainerAttributeResolverFactory(
new SimpleContainer([
RouteArgumentResolver::class => new RouteArgumentResolver($currentRoute),
])
),
);
}

private function createParameterAttributeResolveContext(): ParameterAttributeResolveContext
{
$reflection = new ReflectionFunction(static fn (int $a) => null);

return new ParameterAttributeResolveContext($reflection->getParameters()[0], Result::fail(), new ArrayData());
}
}

0 comments on commit 1a8af5c

Please sign in to comment.