Skip to content

Commit

Permalink
Add RouteArgument attribute for Yii Hydrator (#203)
Browse files Browse the repository at this point in the history
  • Loading branch information
vjik authored Jan 29, 2024
1 parent 5bc9a72 commit f3f1091
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Enh #202: Add support for `psr/http-message` version `^2.0` (@vjik)
- Chg #222: Make `Route`, `Group` and `MatchingResult` dispatcher-independent (@rustamwin, @vjik)
- Enh #229: Add URL arguments' psalm type in `UrlGeneratorInterface` (@vjik)
- New #203: Added `RouteArgument` attribute for Yii Hydrator (@vjik)

## 3.0.0 February 17, 2023

Expand Down
5 changes: 5 additions & 0 deletions composer-require-checker.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"symbol-whitelist" : [
"Psr\\Container\\ContainerInterface",
"Yiisoft\\Hydrator\\Attribute\\Parameter\\ParameterAttributeInterface",
"Yiisoft\\Hydrator\\Attribute\\Parameter\\ParameterAttributeResolverInterface",
"Yiisoft\\Hydrator\\AttributeHandling\\ParameterAttributeResolveContext",
"Yiisoft\\Hydrator\\AttributeHandling\\Exception\\UnexpectedAttributeException",
"Yiisoft\\Hydrator\\Result",
"Yiisoft\\VarDumper\\VarDumper",
"Yiisoft\\Yii\\Debug\\Debugger",
"Yiisoft\\Yii\\Debug\\Collector\\CollectorTrait",
Expand Down
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.20",
"yiisoft/di": "^1.0",
"yiisoft/dummy-provider": "^1.0.0",
"yiisoft/hydrator": "^1.0",
"yiisoft/test-support": "^3.0",
"yiisoft/yii-debug": "dev-master|dev-php80"
},
Expand All @@ -53,7 +54,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 f3f1091

Please sign in to comment.