From f3f1091369f3c452d9a735135e072b19716e8f16 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 29 Jan 2024 20:10:54 +0300 Subject: [PATCH] Add `RouteArgument` attribute for Yii Hydrator (#203) --- CHANGELOG.md | 1 + composer-require-checker.json | 5 + composer.json | 4 +- src/HydratorAttribute/RouteArgument.php | 27 +++++ .../RouteArgumentResolver.php | 41 +++++++ tests/HydratorAttribute/RouteArgumentTest.php | 100 ++++++++++++++++++ 6 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/HydratorAttribute/RouteArgument.php create mode 100644 src/HydratorAttribute/RouteArgumentResolver.php create mode 100644 tests/HydratorAttribute/RouteArgumentTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af9672..ffe3a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/composer-require-checker.json b/composer-require-checker.json index 34286ea..5605f5f 100644 --- a/composer-require-checker.json +++ b/composer-require-checker.json @@ -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", diff --git a/composer.json b/composer.json index 359976e..2af2cb3 100644 --- a/composer.json +++ b/composer.json @@ -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" }, @@ -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": { diff --git a/src/HydratorAttribute/RouteArgument.php b/src/HydratorAttribute/RouteArgument.php new file mode 100644 index 0000000..aca5947 --- /dev/null +++ b/src/HydratorAttribute/RouteArgument.php @@ -0,0 +1,27 @@ +name; + } + + public function getResolver(): string + { + return RouteArgumentResolver::class; + } +} diff --git a/src/HydratorAttribute/RouteArgumentResolver.php b/src/HydratorAttribute/RouteArgumentResolver.php new file mode 100644 index 0000000..2c8d0c0 --- /dev/null +++ b/src/HydratorAttribute/RouteArgumentResolver.php @@ -0,0 +1,41 @@ +currentRoute->getArguments(); + + $name = $attribute->getName() ?? $context->getParameter()->getName(); + + if (array_key_exists($name, $arguments)) { + return Result::success($arguments[$name]); + } + + return Result::fail(); + } +} diff --git a/tests/HydratorAttribute/RouteArgumentTest.php b/tests/HydratorAttribute/RouteArgumentTest.php new file mode 100644 index 0000000..eedf943 --- /dev/null +++ b/tests/HydratorAttribute/RouteArgumentTest.php @@ -0,0 +1,100 @@ +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()); + } +}