From 8a0036e204dce43b6919f79a96a7fa83540f3b3d Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Tue, 11 Feb 2025 16:02:18 +0200 Subject: [PATCH] refactor path parameters --- src/Configuration/ParametersExtractors.php | 2 + src/Reflection/ReflectionRoute.php | 155 +++++++++ .../PathParametersExtractor.php | 192 +++++++++++ .../RequestEssentialsExtension.php | 306 +----------------- tests/Reflection/ReflectionRouteTest.php | 45 +++ 5 files changed, 397 insertions(+), 303 deletions(-) create mode 100644 src/Reflection/ReflectionRoute.php create mode 100644 src/Support/OperationExtensions/ParameterExtractor/PathParametersExtractor.php create mode 100644 tests/Reflection/ReflectionRouteTest.php diff --git a/src/Configuration/ParametersExtractors.php b/src/Configuration/ParametersExtractors.php index 22d87230..b63ffec7 100644 --- a/src/Configuration/ParametersExtractors.php +++ b/src/Configuration/ParametersExtractors.php @@ -3,6 +3,7 @@ namespace Dedoc\Scramble\Configuration; use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\FormRequestParametersExtractor; +use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\PathParametersExtractor; use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\ValidateCallParametersExtractor; use Illuminate\Support\Arr; @@ -44,6 +45,7 @@ public function use(array $extractors) public function all(): array { $base = $this->extractors ?: [ + PathParametersExtractor::class, FormRequestParametersExtractor::class, ValidateCallParametersExtractor::class, ]; diff --git a/src/Reflection/ReflectionRoute.php b/src/Reflection/ReflectionRoute.php new file mode 100644 index 00000000..8c25a229 --- /dev/null +++ b/src/Reflection/ReflectionRoute.php @@ -0,0 +1,155 @@ + 'emailId', 'recipient_id' => 'recipientId']`. + * + * The trick is to avoid mapping parameters like `Request $request`, but to correctly map the model bindings + * (and other potential kind of bindings). + * + * During this method implementation, Laravel implicit binding checks against snake cased parameters. + * + * @see ImplicitRouteBinding::getParameterName + */ + public function getSignatureParametersMap(): array + { + $paramNames = $this->route->parameterNames(); + + $paramBoundTypes = $this->getBoundParametersTypes(); + + $checkingRouteSignatureParameters = $this->route->signatureParameters(); + $paramsToSignatureParametersNameMap = collect($paramNames) + ->mapWithKeys(function ($name) use ($paramBoundTypes, &$checkingRouteSignatureParameters) { + $boundParamType = $paramBoundTypes[$name]; + $mappedParameterReflection = collect($checkingRouteSignatureParameters) + ->first(function (ReflectionParameter $rp) use ($boundParamType) { + $type = $rp->getType(); + + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { + return true; + } + + $className = Reflector::getParameterClassName($rp); + + return is_a($boundParamType, $className, true); + }); + + if ($mappedParameterReflection) { + $checkingRouteSignatureParameters = array_filter($checkingRouteSignatureParameters, fn ($v) => $v !== $mappedParameterReflection); + } + + return [ + $name => $mappedParameterReflection, + ]; + }); + + $paramsWithRealNames = $paramsToSignatureParametersNameMap + ->mapWithKeys(fn (?ReflectionParameter $reflectionParameter, $name) => [$name => $reflectionParameter?->name ?: $name]) + ->values(); + + return collect($paramNames)->mapWithKeys(fn ($name, $i) => [$name => $paramsWithRealNames[$i]])->all(); + } + + /** + * Get bound parameters types – these are the name of classes that can be bound to the parameters. + * This includes implicitly bound types (UrlRoutable, backedEnum) and explicitly bound parameters. + * + * @return array + */ + public function getBoundParametersTypes(): array + { + $paramNames = $this->route->parameterNames(); + + $implicitlyBoundReflectionParams = collect() + ->union($this->route->signatureParameters(UrlRoutable::class)) + ->union($this->route->signatureParameters(['backedEnum' => true])) + ->keyBy('name'); + + return collect($paramNames) + ->mapWithKeys(function ($name) use ($implicitlyBoundReflectionParams) { + if ($explicitlyBoundParamType = $this->getExplicitlyBoundParamType($name)) { + return [$name => $explicitlyBoundParamType]; + } + + /** @var ReflectionParameter $implicitlyBoundParam */ + $implicitlyBoundParam = $implicitlyBoundReflectionParams->first( + fn (ReflectionParameter $p) => $p->name === $name || Str::snake($p->name) === $name, + ); + + if ($implicitlyBoundParam) { + return [$name => Reflector::getParameterClassName($implicitlyBoundParam)]; + } + + return [ + $name => null, + ]; + }) + ->all(); + } + + private function getExplicitlyBoundParamType(string $name): ?string + { + if (! $binder = app(Router::class)->getBindingCallback($name)) { + return null; + } + + try { + $reflection = new ReflectionFunction($binder); + } catch (ReflectionException) { + return null; + } + + if ($returnType = $reflection->getReturnType()) { + return $returnType instanceof ReflectionNamedType && ! $returnType->isBuiltin() + ? $returnType->getName() + : null; + } + + // in case this is a model binder + if ( + ($modelClass = $reflection->getClosureUsedVariables()['class'] ?? null) + && is_string($modelClass) + ) { + return $modelClass; + } + + return null; + } +} diff --git a/src/Support/OperationExtensions/ParameterExtractor/PathParametersExtractor.php b/src/Support/OperationExtensions/ParameterExtractor/PathParametersExtractor.php new file mode 100644 index 00000000..85997adb --- /dev/null +++ b/src/Support/OperationExtensions/ParameterExtractor/PathParametersExtractor.php @@ -0,0 +1,192 @@ +route); + + $methodPhpDocNode = $routeInfo->phpDoc(); + $aliases = $reflectionRoute->getSignatureParametersMap(); + $routeParams = collect($route->signatureParameters()); + $reflectionParamsByKeys = $routeParams->keyBy->name; + $paramsValuesClasses = $reflectionRoute->getBoundParametersTypes(); + $phpDocTypehintParam = $methodPhpDocNode + ? collect($methodPhpDocNode->getParamTagValues())->keyBy(fn (ParamTagValueNode $n) => Str::replace('$', '', $n->parameterName)) + : collect(); + + /* + * Figure out param type based on importance priority: + * 1. Typehint (reflection) + * 2. PhpDoc Typehint + * 3. String (?) + */ + $parameters = array_map(function (string $paramName) use ($routeInfo, $route, $aliases, $reflectionParamsByKeys, $phpDocTypehintParam, $paramsValuesClasses) { + $originalParamName = $paramName; + $paramName = $aliases[$paramName]; + + $description = $phpDocTypehintParam[$paramName]?->description ?? ''; + [$schemaType, $description, $isOptional] = $this->getParameterType( + $paramName, + $description, + $routeInfo, + $route, + $phpDocTypehintParam[$paramName] ?? null, + $reflectionParamsByKeys[$paramName] ?? null, + $paramsValuesClasses[$originalParamName] ?? null, + ); + + $param = Parameter::make($paramName, 'path') + ->description($description) + ->setSchema(Schema::fromType($schemaType)); + + if ($isOptional) { + $param->setExtensionProperty('optional', true); + } + + return $param; + }, array_values(array_diff($route->parameterNames(), $this->getParametersFromString($route->getDomain())))); + + return [...$parameterExtractionResults, new ParametersExtractionResult($parameters)]; + } + + private function getParametersFromString(?string $str) + { + return Str::of($str)->matchAll('/\{(.*?)\}/')->values()->toArray(); + } + + private function getParameterType( + string $paramName, + string $description, + RouteInfo $routeInfo, + Route $route, + ?ParamTagValueNode $phpDocParam, + ?ReflectionParameter $reflectionParam, + ?string $boundClass, + ) { + $type = $boundClass ? new ObjectType($boundClass) : new UnknownType; + if ($routeInfo->reflectionMethod()) { + $type->setAttribute('file', $routeInfo->reflectionMethod()->getFileName()); + $type->setAttribute('line', $routeInfo->reflectionMethod()->getStartLine()); + } + + if ($phpDocParam?->type) { + $type = PhpDocTypeHelper::toType($phpDocParam->type); + } + + if ($reflectionParam?->hasType()) { + $type = TypeHelper::createTypeFromReflectionType($reflectionParam->getType()); + } + + $simplifiedType = Union::wrap(array_map( + fn (InferType $t) => $t instanceof ObjectType + ? (enum_exists($t->name) ? $t : new \Dedoc\Scramble\Support\Type\StringType) + : $t, + $type instanceof Union ? $type->types : [$type], + )); + + $schemaType = $this->openApiTransformer->transform($simplifiedType); + + if ($isModelId = $type instanceof ObjectType) { + [$schemaType, $description] = $this->getModelIdTypeAndDescription($schemaType, $type, $paramName, $description, $route->bindingFields()[$paramName] ?? null); + + $schemaType->setAttribute('isModelId', true); + } + + if ($schemaType instanceof \Dedoc\Scramble\Support\Generator\Types\UnknownType) { + $schemaType = (new StringType)->mergeAttributes($schemaType->attributes()); + } + + if ($reflectionParam?->isDefaultValueAvailable()) { + $schemaType->default($reflectionParam->getDefaultValue()); + } + + $description ??= ''; + + if ($isOptional = Str::contains($route->uri(), ['{'.$paramName.'?}', '{'.Str::snake($paramName).'?}'], ignoreCase: true)) { + $description = implode('. ', array_filter(['**Optional**', $description])); + } + + return [$schemaType, $description, $isOptional]; + } + + private function getModelIdTypeAndDescription( + Type $baseType, + InferType $type, + string $paramName, + string $description, + ?string $bindingField, + ): array { + $defaults = [ + $baseType, + $description ?: 'The '.Str::of($paramName)->kebab()->replace(['-', '_'], ' ').' ID', + ]; + + if (! $type->isInstanceOf(Model::class)) { + return $defaults; + } + + /** @var ObjectType $type */ + $defaults[0] = $this->openApiTransformer->transform(new IntegerType); + + try { + /** @var Model $modelInstance */ + $modelInstance = resolve($type->name); + } catch (BindingResolutionException) { + return $defaults; + } + + $modelKeyName = $modelInstance->getKeyName(); + $routeKeyName = $bindingField ?: $modelInstance->getRouteKeyName(); + + if ($description === '') { + $keyDescriptionName = in_array($routeKeyName, ['id', 'uuid']) + ? Str::upper($routeKeyName) + : (string) Str::of($routeKeyName)->lower()->kebab()->replace(['-', '_'], ' '); + + $description = 'The '.Str::of($paramName)->kebab()->replace(['-', '_'], ' ').' '.$keyDescriptionName; + } + + $modelTraits = class_uses($type->name); + if ($routeKeyName === $modelKeyName && Arr::has($modelTraits, HasUuids::class)) { + return [(new StringType)->format('uuid'), $description]; + } + + $propertyType = $type->getPropertyType($routeKeyName); + if ($propertyType instanceof UnknownType) { + $propertyType = new IntegerType; + } + + return [$this->openApiTransformer->transform($propertyType), $description]; + } +} diff --git a/src/Support/OperationExtensions/RequestEssentialsExtension.php b/src/Support/OperationExtensions/RequestEssentialsExtension.php index bf7d164e..9c577fe8 100644 --- a/src/Support/OperationExtensions/RequestEssentialsExtension.php +++ b/src/Support/OperationExtensions/RequestEssentialsExtension.php @@ -6,43 +6,20 @@ use Dedoc\Scramble\Extensions\OperationExtension; use Dedoc\Scramble\GeneratorConfig; use Dedoc\Scramble\Infer; -use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper; +use Dedoc\Scramble\Reflection\ReflectionRoute; use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\Operation; -use Dedoc\Scramble\Support\Generator\Parameter; -use Dedoc\Scramble\Support\Generator\Schema; use Dedoc\Scramble\Support\Generator\Server; -use Dedoc\Scramble\Support\Generator\ServerVariable; -use Dedoc\Scramble\Support\Generator\Types\StringType; -use Dedoc\Scramble\Support\Generator\Types\Type; use Dedoc\Scramble\Support\Generator\TypeTransformer; use Dedoc\Scramble\Support\Generator\UniqueNameOptions; use Dedoc\Scramble\Support\PhpDoc; use Dedoc\Scramble\Support\RouteInfo; use Dedoc\Scramble\Support\ServerFactory; -use Dedoc\Scramble\Support\Type\IntegerType; -use Dedoc\Scramble\Support\Type\ObjectType; -use Dedoc\Scramble\Support\Type\Type as InferType; -use Dedoc\Scramble\Support\Type\TypeHelper; -use Dedoc\Scramble\Support\Type\Union; -use Dedoc\Scramble\Support\Type\UnknownType; -use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Contracts\Routing\UrlRoutable; -use Illuminate\Database\Eloquent\Concerns\HasUuids; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Routing\ImplicitRouteBinding; use Illuminate\Routing\Route; -use Illuminate\Routing\Router; use Illuminate\Support\Arr; -use Illuminate\Support\Reflector; use Illuminate\Support\Str; -use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; -use ReflectionException; -use ReflectionFunction; -use ReflectionNamedType; -use ReflectionParameter; class RequestEssentialsExtension extends OperationExtension { @@ -78,7 +55,7 @@ private function getDefaultTags(Operation $operation, RouteInfo $routeInfo) public function handle(Operation $operation, RouteInfo $routeInfo) { - [$pathParams, $pathAliases] = $this->getRoutePathParameters($routeInfo); + $pathAliases = ReflectionRoute::createFromRoute($routeInfo->route)->getSignatureParametersMap(); $tagResolver = Scramble::$tagResolver ?? fn () => $this->getDefaultTags($operation, $routeInfo); @@ -92,8 +69,7 @@ public function handle(Operation $operation, RouteInfo $routeInfo) $uriWithoutOptionalParams, )) ->setTags($tagResolver($routeInfo, $operation)) - ->servers($this->getAlternativeServers($routeInfo->route)) - ->addParameters($pathParams); + ->servers($this->getAlternativeServers($routeInfo->route)); if (count($routeInfo->phpDoc()->getTagsByName('@unauthenticated'))) { $operation->security = []; @@ -164,268 +140,6 @@ private function extractTagsForMethod(RouteInfo $routeInfo) return explode(',', array_values($tagNodes)[0]->value->value); } - private function getParametersFromString(?string $str) - { - return Str::of($str)->matchAll('/\{(.*?)\}/')->values()->toArray(); - } - - /** - * The goal here is to get the mapping of route names specified in route path to the parameters - * used in a route definition. The mapping then is used to get more information about the parameters for - * the documentation. For example, the description from PHPDoc will be used for a route path parameter - * description. - * - * So given the route path `/emails/{email_id}/recipients/{recipient_id}` and the route's method: - * `public function show(Request $request, string $emailId, string $recipientId)`, we get the mapping: - * `['email_id' => 'emailId', 'recipient_id' => 'recipientId']`. - * - * The trick is to avoid mapping parameters like `Request $request`, but to correctly map the model bindings - * (and other potential kind of bindings). - * - * During this method implementation, Laravel implicit binding checks against snake cased parameters. - * - * @see ImplicitRouteBinding::getParameterName - */ - private function getRoutePathParameters(RouteInfo $routeInfo) - { - [$route, $methodPhpDocNode] = [$routeInfo->route, $routeInfo->phpDoc()]; - - $paramNames = $route->parameterNames(); - - $implicitlyBoundReflectionParams = collect() - ->union($route->signatureParameters(UrlRoutable::class)) - ->union($route->signatureParameters(['backedEnum' => true])) - ->keyBy('name'); - - $paramsValuesClasses = collect($paramNames) - ->mapWithKeys(function ($name) use ($implicitlyBoundReflectionParams) { - if ($explicitlyBoundParamType = $this->getExplicitlyBoundParamType($name)) { - return [$name => $explicitlyBoundParamType]; - } - - /** @var ReflectionParameter $implicitlyBoundParam */ - $implicitlyBoundParam = $implicitlyBoundReflectionParams->first( - fn (ReflectionParameter $p) => $p->name === $name || Str::snake($p->name) === $name, - ); - - if ($implicitlyBoundParam) { - return [$name => Reflector::getParameterClassName($implicitlyBoundParam)]; - } - - return [ - $name => null, - ]; - }); - - $routeParams = collect($route->signatureParameters()); - - $checkingRouteSignatureParameters = $route->signatureParameters(); - $paramsToSignatureParametersNameMap = collect($paramNames) - ->mapWithKeys(function ($name) use ($paramsValuesClasses, &$checkingRouteSignatureParameters) { - $boundParamType = $paramsValuesClasses[$name]; - $mappedParameterReflection = collect($checkingRouteSignatureParameters) - ->first(function (ReflectionParameter $rp) use ($boundParamType) { - $type = $rp->getType(); - - if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { - return true; - } - - $className = Reflector::getParameterClassName($rp); - - return is_a($boundParamType, $className, true); - }); - - if ($mappedParameterReflection) { - $checkingRouteSignatureParameters = array_filter($checkingRouteSignatureParameters, fn ($v) => $v !== $mappedParameterReflection); - } - - return [ - $name => $mappedParameterReflection, - ]; - }); - - $paramsWithRealNames = $paramsToSignatureParametersNameMap - ->mapWithKeys(fn (?ReflectionParameter $reflectionParameter, $name) => [$name => $reflectionParameter?->name ?: $name]) - ->values(); - - $aliases = collect($paramNames)->mapWithKeys(fn ($name, $i) => [$name => $paramsWithRealNames[$i]])->all(); - - $reflectionParamsByKeys = $routeParams->keyBy->name; - $phpDocTypehintParam = $methodPhpDocNode - ? collect($methodPhpDocNode->getParamTagValues())->keyBy(fn (ParamTagValueNode $n) => Str::replace('$', '', $n->parameterName)) - : collect(); - - /* - * Figure out param type based on importance priority: - * 1. Typehint (reflection) - * 2. PhpDoc Typehint - * 3. String (?) - */ - $params = array_map(function (string $paramName) use ($routeInfo, $route, $aliases, $reflectionParamsByKeys, $phpDocTypehintParam, $paramsValuesClasses) { - $originalParamName = $paramName; - $paramName = $aliases[$paramName]; - - $description = $phpDocTypehintParam[$paramName]?->description ?? ''; - [$schemaType, $description, $isOptional] = $this->getParameterType( - $paramName, - $description, - $routeInfo, - $route, - $phpDocTypehintParam[$paramName] ?? null, - $reflectionParamsByKeys[$paramName] ?? null, - $paramsValuesClasses[$originalParamName] ?? null, - ); - - $param = Parameter::make($paramName, 'path') - ->description($description) - ->setSchema(Schema::fromType($schemaType)); - - if ($isOptional) { - $param->setExtensionProperty('optional', true); - } - - return $param; - }, array_values(array_diff($route->parameterNames(), $this->getParametersFromString($route->getDomain())))); - - return [$params, $aliases]; - } - - private function getExplicitlyBoundParamType(string $name): ?string - { - if (! $binder = app(Router::class)->getBindingCallback($name)) { - return null; - } - - try { - $reflection = new ReflectionFunction($binder); - } catch (ReflectionException) { - return null; - } - - if ($returnType = $reflection->getReturnType()) { - return $returnType instanceof ReflectionNamedType && ! $returnType->isBuiltin() - ? $returnType->getName() - : null; - } - - // in case this is a model binder - if ( - ($modelClass = $reflection->getClosureUsedVariables()['class'] ?? null) - && is_string($modelClass) - ) { - return $modelClass; - } - - return null; - } - - private function getParameterType( - string $paramName, - string $description, - RouteInfo $routeInfo, - Route $route, - ?ParamTagValueNode $phpDocParam, - ?ReflectionParameter $reflectionParam, - ?string $boundClass, - ) { - $type = $boundClass ? new ObjectType($boundClass) : new UnknownType; - if ($routeInfo->reflectionMethod()) { - $type->setAttribute('file', $routeInfo->reflectionMethod()->getFileName()); - $type->setAttribute('line', $routeInfo->reflectionMethod()->getStartLine()); - } - - if ($phpDocParam?->type) { - $type = PhpDocTypeHelper::toType($phpDocParam->type); - } - - if ($reflectionParam?->hasType()) { - $type = TypeHelper::createTypeFromReflectionType($reflectionParam->getType()); - } - - $simplifiedType = Union::wrap(array_map( - fn (InferType $t) => $t instanceof ObjectType - ? (enum_exists($t->name) ? $t : new \Dedoc\Scramble\Support\Type\StringType) - : $t, - $type instanceof Union ? $type->types : [$type], - )); - - $schemaType = $this->openApiTransformer->transform($simplifiedType); - - if ($isModelId = $type instanceof ObjectType) { - [$schemaType, $description] = $this->getModelIdTypeAndDescription($schemaType, $type, $paramName, $description, $route->bindingFields()[$paramName] ?? null); - - $schemaType->setAttribute('isModelId', true); - } - - if ($schemaType instanceof \Dedoc\Scramble\Support\Generator\Types\UnknownType) { - $schemaType = (new StringType)->mergeAttributes($schemaType->attributes()); - } - - if ($reflectionParam?->isDefaultValueAvailable()) { - $schemaType->default($reflectionParam->getDefaultValue()); - } - - $description ??= ''; - - $isOptional = false; - if ($isOptional = Str::contains($route->uri(), ['{'.$paramName.'?}', '{'.Str::snake($paramName).'?}'], ignoreCase: true)) { - $description = implode('. ', array_filter(['**Optional**', $description])); - } - - return [$schemaType, $description, $isOptional]; - } - - private function getModelIdTypeAndDescription( - Type $baseType, - InferType $type, - string $paramName, - string $description, - ?string $bindingField, - ): array { - $defaults = [ - $baseType, - $description ?: 'The '.Str::of($paramName)->kebab()->replace(['-', '_'], ' ').' ID', - ]; - - if (! $type->isInstanceOf(Model::class)) { - return $defaults; - } - - /** @var ObjectType $type */ - $defaults[0] = $this->openApiTransformer->transform(new IntegerType); - - try { - /** @var Model $modelInstance */ - $modelInstance = resolve($type->name); - } catch (BindingResolutionException) { - return $defaults; - } - - $modelKeyName = $modelInstance->getKeyName(); - $routeKeyName = $bindingField ?: $modelInstance->getRouteKeyName(); - - if ($description === '') { - $keyDescriptionName = in_array($routeKeyName, ['id', 'uuid']) - ? Str::upper($routeKeyName) - : (string) Str::of($routeKeyName)->lower()->kebab()->replace(['-', '_'], ' '); - - $description = 'The '.Str::of($paramName)->kebab()->replace(['-', '_'], ' ').' '.$keyDescriptionName; - } - - $modelTraits = class_uses($type->name); - if ($routeKeyName === $modelKeyName && Arr::has($modelTraits, HasUuids::class)) { - return [(new StringType)->format('uuid'), $description]; - } - - $propertyType = $type->getPropertyType($routeKeyName); - if ($propertyType instanceof UnknownType) { - $propertyType = new IntegerType; - } - - return [$this->openApiTransformer->transform($propertyType), $description]; - } - private function getOperationId(RouteInfo $routeInfo) { $routeClassName = $routeInfo->className() ?: ''; @@ -464,18 +178,4 @@ private function getOperationId(RouteInfo $routeInfo) ->toArray(), ); } - - private function getServerVariablesInUrl(string $url): array - { - $variables = $this->config->serverVariables->all(); - - $params = Str::of($url)->matchAll('/\{(.*?)\}/'); - - return collect($variables) - ->only($params) - ->merge($params->reject(fn ($p) => array_key_exists($p, $variables))->mapWithKeys(fn ($p) => [ - $p => ServerVariable::make('example'), - ])) - ->toArray(); - } } diff --git a/tests/Reflection/ReflectionRouteTest.php b/tests/Reflection/ReflectionRouteTest.php new file mode 100644 index 00000000..b9af9cc2 --- /dev/null +++ b/tests/Reflection/ReflectionRouteTest.php @@ -0,0 +1,45 @@ + null); + + $ra = ReflectionRoute::createFromRoute($route); + $rb = ReflectionRoute::createFromRoute($route); + + expect($ra === $rb)->toBeTrue(); +}); + +test('gets params aliases single instance from route', function () { + $route = RouteFacade::get('{test_id}', fn (string $testId) => null); + + expect(ReflectionRoute::createFromRoute($route)->getSignatureParametersMap())->toBe([ + 'test_id' => 'testId', + ]); +}); + +test('gets params aliases without request from route', function () { + $route = RouteFacade::get('{test_id}', fn (Request $request, string $testId) => null); + + expect(ReflectionRoute::createFromRoute($route)->getSignatureParametersMap())->toBe([ + 'test_id' => 'testId', + ]); +}); + +test('gets bound params types', function () { + $r = ReflectionRoute::createFromRoute( + RouteFacade::get('{test_id}', fn (Request $request, User_ReflectionRouteTest $testId) => null) + ); + + expect($r->getBoundParametersTypes())->toBe([ + 'test_id' => User_ReflectionRouteTest::class, + ]); +}); +class User_ReflectionRouteTest extends Model +{}