diff --git a/psalm.xml.dist b/psalm.xml.dist index c32cf45c..f928c781 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -25,6 +25,8 @@ + + diff --git a/resources/definitions/middlewares/base64_decoding_middleware.php b/resources/definitions/middlewares/base64_decoding_middleware.php new file mode 100644 index 00000000..27c7853c --- /dev/null +++ b/resources/definitions/middlewares/base64_decoding_middleware.php @@ -0,0 +1,19 @@ + add([ + create(Base64DecodingMiddleware::class) + ->constructor( + streamFactory: get(StreamFactoryInterface::class), + ) + ]), +]; diff --git a/src/Middleware/Base64DecodingMiddleware.php b/src/Middleware/Base64DecodingMiddleware.php new file mode 100644 index 00000000..8b1b1ffa --- /dev/null +++ b/src/Middleware/Base64DecodingMiddleware.php @@ -0,0 +1,50 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * @since 3.2.0 + */ +final class Base64DecodingMiddleware implements MiddlewareInterface +{ + public function __construct( + private readonly StreamFactoryInterface $streamFactory, + ) { + } + + /** + * @inheritDoc + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding + if ($request->getHeaderLine('Content-Encoding') === 'base64') { + /** @var resource $resource */ + $resource = $request->getBody()->detach(); + // https://www.php.net/manual/en/filters.convert.php + \stream_filter_append($resource, 'convert.base64-decode'); + + $body = $this->streamFactory->createStreamFromResource($resource); + $request = $request->withBody($body); + } + + return $handler->handle($request); + } +} diff --git a/src/OpenApi/Annotation/SchemaName.php b/src/OpenApi/Annotation/SchemaName.php new file mode 100644 index 00000000..819d9022 --- /dev/null +++ b/src/OpenApi/Annotation/SchemaName.php @@ -0,0 +1,28 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\OpenApi\Annotation; + +use Attribute; + +/** + * @since 3.2.0 + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class SchemaName +{ + public function __construct( + public readonly string $value, + ) { + } +} diff --git a/src/OpenApi/PhpTypeSchemaResolver/ArrayAccessPhpTypeSchemaResolver.php b/src/OpenApi/PhpTypeSchemaResolver/ArrayAccessPhpTypeSchemaResolver.php index 0c4aad8f..46457a7c 100644 --- a/src/OpenApi/PhpTypeSchemaResolver/ArrayAccessPhpTypeSchemaResolver.php +++ b/src/OpenApi/PhpTypeSchemaResolver/ArrayAccessPhpTypeSchemaResolver.php @@ -63,7 +63,6 @@ public function resolvePhpTypeSchema(Type $phpType, Reflector $phpTypeHolder): a $phpTypeName = $phpType->name; $arrayPhpType = new Type(Type::PHP_TYPE_NAME_ARRAY, $phpType->allowsNull); - /** @var array{oneOf: array{0: array{type: 'array'}, 1: array{type: 'object'}}} $phpTypeSchema */ $phpTypeSchema = $this->openApiPhpTypeSchemaResolverManager ->resolvePhpTypeSchema($arrayPhpType, $phpTypeHolder); @@ -76,8 +75,7 @@ public function resolvePhpTypeSchema(Type $phpType, Reflector $phpTypeHolder): a $collectionElementPhpTypeSchema = $this->openApiPhpTypeSchemaResolverManager ->resolvePhpTypeSchema($collectionElementPhpType, $phpTypeHolder); - $phpTypeSchema['oneOf'][0]['items'] = $collectionElementPhpTypeSchema; - $phpTypeSchema['oneOf'][1]['additionalProperties'] = $collectionElementPhpTypeSchema; + $phpTypeSchema['items'] = $collectionElementPhpTypeSchema; } return $phpTypeSchema; diff --git a/src/OpenApi/PhpTypeSchemaResolver/ArrayPhpTypeSchemaResolver.php b/src/OpenApi/PhpTypeSchemaResolver/ArrayPhpTypeSchemaResolver.php index d07412d4..a775ee70 100644 --- a/src/OpenApi/PhpTypeSchemaResolver/ArrayPhpTypeSchemaResolver.php +++ b/src/OpenApi/PhpTypeSchemaResolver/ArrayPhpTypeSchemaResolver.php @@ -52,14 +52,7 @@ public function resolvePhpTypeSchema(Type $phpType, Reflector $phpTypeHolder): a $this->supportsPhpType($phpType, $phpTypeHolder) or throw new UnsupportedPhpTypeException(); $phpTypeSchema = [ - 'oneOf' => [ - [ - 'type' => Type::OAS_TYPE_NAME_ARRAY, - ], - [ - 'type' => Type::OAS_TYPE_NAME_OBJECT, - ], - ], + 'type' => Type::OAS_TYPE_NAME_ARRAY, ]; if ( @@ -75,12 +68,9 @@ public function resolvePhpTypeSchema(Type $phpType, Reflector $phpTypeHolder): a $arrayElementPhpTypeSchema = $this->openApiPhpTypeSchemaResolverManager ->resolvePhpTypeSchema($arrayElementPhpType, $phpTypeHolder); - $phpTypeSchema['oneOf'][0]['items'] = $arrayElementPhpTypeSchema; - $phpTypeSchema['oneOf'][1]['additionalProperties'] = $arrayElementPhpTypeSchema; - + $phpTypeSchema['items'] = $arrayElementPhpTypeSchema; if ($annotation->limit !== null) { - $phpTypeSchema['oneOf'][0]['maxItems'] = $annotation->limit; - $phpTypeSchema['oneOf'][1]['maxProperties'] = $annotation->limit; + $phpTypeSchema['maxItems'] = $annotation->limit; } } } diff --git a/src/OpenApi/PhpTypeSchemaResolver/BackedEnumPhpTypeSchemaResolver.php b/src/OpenApi/PhpTypeSchemaResolver/BackedEnumPhpTypeSchemaResolver.php index 6409869d..b01e619c 100644 --- a/src/OpenApi/PhpTypeSchemaResolver/BackedEnumPhpTypeSchemaResolver.php +++ b/src/OpenApi/PhpTypeSchemaResolver/BackedEnumPhpTypeSchemaResolver.php @@ -14,9 +14,12 @@ namespace Sunrise\Http\Router\OpenApi\PhpTypeSchemaResolver; use BackedEnum; +use ReflectionAttribute; +use ReflectionClass; use ReflectionEnum; use ReflectionException; use Reflector; +use Sunrise\Http\Router\OpenApi\Annotation\SchemaName; use Sunrise\Http\Router\OpenApi\Exception\UnsupportedPhpTypeException; use Sunrise\Http\Router\OpenApi\OpenApiPhpTypeSchemaNameResolverInterface; use Sunrise\Http\Router\OpenApi\OpenApiPhpTypeSchemaResolverInterface; @@ -26,7 +29,6 @@ use Sunrise\Http\Router\OpenApi\TypeFactory; use function is_subclass_of; -use function strtr; /** * @since 3.0.0 @@ -79,6 +81,17 @@ public function getWeight(): int public function resolvePhpTypeSchemaName(Type $phpType, Reflector $phpTypeHolder): string { - return strtr($phpType->name, '\\', '.'); + /** @var class-string $className */ + $className = $phpType->name; + $classReflection = new ReflectionClass($className); + + /** @var list> $annotations */ + $annotations = $classReflection->getAttributes(SchemaName::class); + if (isset($annotations[0])) { + $annotation = $annotations[0]->newInstance(); + return $annotation->value; + } + + return $classReflection->getShortName(); } } diff --git a/src/OpenApi/PhpTypeSchemaResolver/FloatPhpTypeSchemaResolver.php b/src/OpenApi/PhpTypeSchemaResolver/FloatPhpTypeSchemaResolver.php index 99bd2492..9b1340ea 100644 --- a/src/OpenApi/PhpTypeSchemaResolver/FloatPhpTypeSchemaResolver.php +++ b/src/OpenApi/PhpTypeSchemaResolver/FloatPhpTypeSchemaResolver.php @@ -38,6 +38,7 @@ public function resolvePhpTypeSchema(Type $phpType, Reflector $phpTypeHolder): a return [ 'type' => Type::OAS_TYPE_NAME_NUMBER, 'format' => 'double', + 'example' => 0.0, ]; } diff --git a/src/OpenApi/PhpTypeSchemaResolver/IntPhpTypeSchemaResolver.php b/src/OpenApi/PhpTypeSchemaResolver/IntPhpTypeSchemaResolver.php index e5903c45..5dd032a0 100644 --- a/src/OpenApi/PhpTypeSchemaResolver/IntPhpTypeSchemaResolver.php +++ b/src/OpenApi/PhpTypeSchemaResolver/IntPhpTypeSchemaResolver.php @@ -40,6 +40,7 @@ public function resolvePhpTypeSchema(Type $phpType, Reflector $phpTypeHolder): a return [ 'type' => Type::OAS_TYPE_NAME_INTEGER, 'format' => PHP_INT_SIZE === 4 ? 'int32' : 'int64', + 'example' => 0, ]; } diff --git a/src/OpenApi/PhpTypeSchemaResolver/ObjectPhpTypeSchemaResolver.php b/src/OpenApi/PhpTypeSchemaResolver/ObjectPhpTypeSchemaResolver.php index b2a24654..6599bd5b 100644 --- a/src/OpenApi/PhpTypeSchemaResolver/ObjectPhpTypeSchemaResolver.php +++ b/src/OpenApi/PhpTypeSchemaResolver/ObjectPhpTypeSchemaResolver.php @@ -19,6 +19,7 @@ use ReflectionException; use ReflectionProperty; use Reflector; +use Sunrise\Http\Router\OpenApi\Annotation\SchemaName; use Sunrise\Http\Router\OpenApi\Exception\UnsupportedPhpTypeException; use Sunrise\Http\Router\OpenApi\OpenApiPhpTypeSchemaNameResolverInterface; use Sunrise\Http\Router\OpenApi\OpenApiPhpTypeSchemaResolverInterface; @@ -33,7 +34,6 @@ use function class_exists; use function is_scalar; -use function strtr; /** * @since 3.0.0 @@ -126,7 +126,18 @@ public function getWeight(): int public function resolvePhpTypeSchemaName(Type $phpType, Reflector $phpTypeHolder): string { - return strtr($phpType->name, '\\', '.'); + /** @var class-string $className */ + $className = $phpType->name; + $classReflection = new ReflectionClass($className); + + /** @var list> $annotations */ + $annotations = $classReflection->getAttributes(SchemaName::class); + if (isset($annotations[0])) { + $annotation = $annotations[0]->newInstance(); + return $annotation->value; + } + + return $classReflection->getShortName(); } private static function isIgnoredProperty(ReflectionProperty $property): bool diff --git a/src/Router.php b/src/Router.php index 0ac93062..650924a0 100644 --- a/src/Router.php +++ b/src/Router.php @@ -173,8 +173,12 @@ public function match(ServerRequestInterface $request): RouteInterface * * @throws InvalidArgumentException */ - public function runRoute(RouteInterface $route, ServerRequestInterface $request): ResponseInterface + public function runRoute(RouteInterface|string $route, ServerRequestInterface $request): ResponseInterface { + if (! $route instanceof RouteInterface) { + $route = $this->getRoute($route); + } + foreach ($route->getAttributes() as $name => $value) { $request = $request->withAttribute($name, $value); } @@ -203,8 +207,12 @@ public function runRoute(RouteInterface $route, ServerRequestInterface $request) * * @throws InvalidArgumentException */ - public function buildRoute(RouteInterface $route, array $values = [], bool $strictly = false): string + public function buildRoute(RouteInterface|string $route, array $values = [], bool $strictly = false): string { + if (! $route instanceof RouteInterface) { + $route = $this->getRoute($route); + } + $result = RouteBuilder::buildRoute($route->getPath(), $values + $route->getAttributes()); if ($strictly && !RouteMatcher::matchRoute($this->compileRoute($route), $result)) { diff --git a/src/RouterInterface.php b/src/RouterInterface.php index 62ea59b7..3b225d2c 100644 --- a/src/RouterInterface.php +++ b/src/RouterInterface.php @@ -48,7 +48,7 @@ public function hasRoute(string $name): bool; * * @since 3.0.0 */ - public function runRoute(RouteInterface $route, ServerRequestInterface $request): ResponseInterface; + public function runRoute(RouteInterface|string $route, ServerRequestInterface $request): ResponseInterface; /** * @param array $values @@ -57,7 +57,7 @@ public function runRoute(RouteInterface $route, ServerRequestInterface $request) * * @since 3.0.0 */ - public function buildRoute(RouteInterface $route, array $values = [], bool $strictly = false): string; + public function buildRoute(RouteInterface|string $route, array $values = [], bool $strictly = false): string; /** * @throws HttpException diff --git a/tests/Fixture/App/View/PageView.php b/tests/Fixture/App/View/PageView.php index b6b8854b..8fcbd9dd 100644 --- a/tests/Fixture/App/View/PageView.php +++ b/tests/Fixture/App/View/PageView.php @@ -4,6 +4,9 @@ namespace Sunrise\Http\Router\Tests\Fixture\App\View; +use Sunrise\Http\Router\OpenApi\Annotation\SchemaName; + +#[SchemaName('Page')] final class PageView { public function __construct( diff --git a/tests/Fixture/App/openapi.json b/tests/Fixture/App/openapi.json index b4e3f57d..5d0cbaa0 100644 --- a/tests/Fixture/App/openapi.json +++ b/tests/Fixture/App/openapi.json @@ -50,7 +50,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Auth.SignInRequest" + "$ref": "#/components/schemas/SignInRequest" } } }, @@ -86,7 +86,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Product.ProductCreateRequest" + "$ref": "#/components/schemas/ProductCreateRequest" } } }, @@ -206,7 +206,7 @@ "in": "query", "name": "Query", "schema": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Page.PageListRequest" + "$ref": "#/components/schemas/PageListRequest" }, "required": true } @@ -217,20 +217,10 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "array", - "items": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.View.PageView" - } - }, - { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.View.PageView" - } - } - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/Page" + } } } } @@ -263,7 +253,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.View.PageView" + "$ref": "#/components/schemas/Page" } } } @@ -273,7 +263,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Page.PageCreateRequest" + "$ref": "#/components/schemas/PageCreateRequest" } } }, @@ -317,7 +307,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.View.PageView" + "$ref": "#/components/schemas/Page" } } } @@ -327,7 +317,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Page.PageUpdateRequest" + "$ref": "#/components/schemas/PageUpdateRequest" } } }, @@ -344,7 +334,7 @@ }, "components": { "schemas": { - "Sunrise.Http.Router.Tests.Fixture.App.Dto.Auth.SignInRequest": { + "SignInRequest": { "type": "object", "additionalProperties": false, "properties": { @@ -362,7 +352,7 @@ "password" ] }, - "Sunrise.Http.Router.Tests.Fixture.App.Dto.Product.ProductTagDto": { + "ProductTagDto": { "type": "object", "additionalProperties": false, "properties": { @@ -375,7 +365,7 @@ "id" ] }, - "Sunrise.Http.Router.Tests.Fixture.App.Dictionary.ProductFeature": { + "ProductFeature": { "type": "string", "enum": [ "fast-delivery", @@ -805,9 +795,10 @@ "UTC" ] }, - "Sunrise.Http.Router.Tests.Fixture.App.Dictionary.ProductStatus": { + "ProductStatus": { "type": "integer", "format": "int64", + "example": 0, "enum": [ 0, 1, @@ -815,7 +806,7 @@ 3 ] }, - "Sunrise.Http.Router.Tests.Fixture.App.Dto.Product.ProductCreateRequest": { + "ProductCreateRequest": { "type": "object", "additionalProperties": false, "properties": { @@ -826,52 +817,33 @@ "dKQLn8yyMsYG": [], "publicId": { "type": "integer", - "format": "int64" + "format": "int64", + "example": 0 }, "name": { "type": "string" }, "price": { "type": "number", - "format": "double" + "format": "double", + "example": 0 }, "categoryId": { "type": "string", "format": "uuid" }, "tags": { - "oneOf": [ - { - "type": "array", - "items": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Product.ProductTagDto" - } - }, - { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Product.ProductTagDto" - } - } - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductTagDto" + } }, "features": { - "oneOf": [ - { - "type": "array", - "items": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dictionary.ProductFeature" - }, - "maxItems": 2 - }, - { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dictionary.ProductFeature" - }, - "maxProperties": 2 - } - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductFeature" + }, + "maxItems": 2 }, "isModerated": { "type": "boolean" @@ -887,7 +859,7 @@ "status": { "allOf": [ { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dictionary.ProductStatus" + "$ref": "#/components/schemas/ProductStatus" } ], "default": 0 @@ -908,7 +880,7 @@ "timezone" ] }, - "Sunrise.Http.Router.Tests.Fixture.App.Dto.Page.PageFilterRequest": { + "PageFilterRequest": { "type": "object", "additionalProperties": false, "properties": { @@ -924,7 +896,7 @@ } } }, - "Sunrise.Http.Router.Tests.Fixture.App.Dto.Common.PaginationDto": { + "PaginationDto": { "type": "object", "additionalProperties": false, "properties": { @@ -932,7 +904,8 @@ "allOf": [ { "type": "integer", - "format": "int64" + "format": "int64", + "example": 0 } ], "default": 20 @@ -941,26 +914,27 @@ "allOf": [ { "type": "integer", - "format": "int64" + "format": "int64", + "example": 0 } ], "default": 0 } } }, - "Sunrise.Http.Router.Tests.Fixture.App.Dto.Page.PageListRequest": { + "PageListRequest": { "type": "object", "additionalProperties": false, "properties": { "filter": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Page.PageFilterRequest" + "$ref": "#/components/schemas/PageFilterRequest" }, "pagination": { - "$ref": "#/components/schemas/Sunrise.Http.Router.Tests.Fixture.App.Dto.Common.PaginationDto" + "$ref": "#/components/schemas/PaginationDto" } } }, - "Sunrise.Http.Router.Tests.Fixture.App.View.PageView": { + "Page": { "type": "object", "additionalProperties": false, "properties": { @@ -972,7 +946,7 @@ "name" ] }, - "Sunrise.Http.Router.Tests.Fixture.App.Dto.Page.PageCreateRequest": { + "PageCreateRequest": { "type": "object", "additionalProperties": false, "properties": { @@ -984,7 +958,7 @@ "name" ] }, - "Sunrise.Http.Router.Tests.Fixture.App.Dto.Page.PageUpdateRequest": { + "PageUpdateRequest": { "type": "object", "additionalProperties": false, "properties": { diff --git a/tests/Middleware/Base64DecodingMiddlewareTest.php b/tests/Middleware/Base64DecodingMiddlewareTest.php new file mode 100644 index 00000000..d3d06ed5 --- /dev/null +++ b/tests/Middleware/Base64DecodingMiddlewareTest.php @@ -0,0 +1,58 @@ +mockedRequest = $this->createMock(ServerRequestInterface::class); + $this->mockedRequestHandler = $this->createMock(RequestHandlerInterface::class); + $this->mockedResponse = $this->createMock(ResponseInterface::class); + $this->mockedStream = $this->createMock(StreamInterface::class); + $this->mockedStreamFactory = $this->createMock(StreamFactoryInterface::class); + $this->resource = \fopen('php://temp', 'r+b'); + } + + protected function tearDown(): void + { + if (\is_resource($this->resource)) { + \fclose($this->resource); + } + } + + public function testProcess(): void + { + $middleware = new Base64DecodingMiddleware($this->mockedStreamFactory); + $this->mockedRequest->expects(self::once())->method('getHeaderLine')->with('Content-Encoding')->willReturn('base64'); + $this->mockedRequest->expects(self::once())->method('getBody')->willReturn($this->mockedStream); + $this->mockedStream->expects(self::once())->method('detach')->willReturn($this->resource); + $this->mockedStreamFactory->expects(self::once())->method('createStreamFromResource')->with($this->resource)->willReturn($this->mockedStream); + $this->mockedRequest->expects(self::once())->method('withBody')->with($this->mockedStream)->willReturnSelf(); + $this->mockedRequestHandler->expects(self::once())->method('handle')->with($this->mockedRequest)->willReturn($this->mockedResponse); + \fwrite($this->resource, 'Zm9v'); + \rewind($this->resource); + $actualResponse = $middleware->process($this->mockedRequest, $this->mockedRequestHandler); + self::assertSame($this->mockedResponse, $actualResponse); + self::assertSame('foo', \fread($this->resource, 4)); + } +} diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 7518d3d6..086d254a 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -236,6 +236,28 @@ public function testRunRoute(): void self::assertSame($this->mockedResponse, $this->createRouter([])->runRoute($route, $request)); } + public function testRunRouteByName(): void + { + $request = $this->mockServerRequest(path: '/test'); + $request->expects(self::any())->method('withAttribute')->withAnyParameters()->willReturnSelf(); + $this->mockedReferenceResolver->expects(self::once())->method('resolveRequestHandler')->with('@test')->willReturn($this->mockedRequestHandler); + $this->mockedRequestHandler->expects(self::once())->method('handle')->with($request)->willReturn($this->mockedResponse); + self::assertSame($this->mockedResponse, $this->createRouter([ + $this->mockLoader([ + $this->mockRoute('test', path: '/test', requestHandler: '@test'), + ], calls: 1), + ])->runRoute('test', $request)); + } + + public function testRunRouteByUnknownName(): void + { + $router = $this->createRouter([]); + $request = $this->mockServerRequest(path: '/test'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The route "test" does not exist.'); + $router->runRoute('test', $request); + } + public function testRunRouteAndPassAttributesToRequest(): void { $route = $this->mockRoute('test', path: '/test', requestHandler: $this->mockedRequestHandler); @@ -245,7 +267,7 @@ public function testRunRouteAndPassAttributesToRequest(): void static function ($name, $value) use ($route, $request) { self::assertContains([$name, $value], [[RouteInterface::class, $route], ['foo', 'bar']]); return $request; - } + }, ); $this->mockedReferenceResolver->expects(self::once())->method('resolveRequestHandler')->withAnyParameters()->willReturnArgument(0); $this->mockedRequestHandler->expects(self::once())->method('handle')->with($request)->willReturn($this->mockedResponse); @@ -264,7 +286,7 @@ static function (object $event) use ($request, $overriddenRequest) { self::assertSame($request, $event->request); $event->request = $overriddenRequest; } - } + }, ); $this->mockedReferenceResolver->expects(self::once())->method('resolveRequestHandler')->withAnyParameters()->willReturnArgument(0); $this->mockedRequestHandler->expects(self::once())->method('handle')->with($overriddenRequest)->willReturn($this->mockedResponse); @@ -285,7 +307,7 @@ function (object $event) use ($overriddenResponse) { self::assertSame($this->mockedResponse, $event->response); $event->response = $overriddenResponse; } - } + }, ); self::assertSame($overriddenResponse, $this->createRouter([])->runRoute($route, $request)); } @@ -311,6 +333,24 @@ public function testBuildRoute(string $routePath, string $expectedPath, array $v self::assertSame($expectedPath, $this->createRouter([])->buildRoute($route, $variables, $strictly)); } + #[DataProvider('buildRouteDataProvider')] + public function testBuildRouteByName(string $routePath, string $expectedPath, array $variables = [], bool $strictly = false): void + { + self::assertSame($expectedPath, $this->createRouter([ + $this->mockLoader([ + $this->mockRoute('test', path: $routePath), + ], calls: 1), + ])->buildRoute('test', $variables, $strictly)); + } + + public function testBuildRouteByUnknownName(): void + { + $router = $this->createRouter([]); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The route "test" does not exist.'); + $router->buildRoute('test'); + } + public static function buildRouteDataProvider(): Generator { yield ['/test', '/test']; @@ -374,7 +414,7 @@ public function testHandle(): void static function ($name, $value) use ($route, $request) { self::assertContains([$name, $value], [['foo', 'bar'], ['bar', 'baz'], [RouteInterface::class, $route]]); return $request; - } + }, ); $this->mockedRequestHandler->expects(self::once())->method('handle')->with($request)->willReturn($this->mockedResponse); @@ -392,7 +432,7 @@ static function (RoutePreRunEvent|RoutePostRunEvent $event) use ($eventOverridde if ($event instanceof RoutePostRunEvent) { $event->response = $eventOverriddenResponse; } - } + }, ); $router = $this->createRouter(