generated from spatie/package-skeleton-laravel
-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed
#[PathParameter]
to override inferred path parameters (#724)
* refactor path parameters * implemented path parameters * Fix styling * fixed parameters handling taking in into consideration * handle both inferred and manually defined parameters --------- Co-authored-by: romalytvynenko <[email protected]>
- Loading branch information
1 parent
88314d2
commit 8865094
Showing
9 changed files
with
461 additions
and
312 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
<?php | ||
|
||
namespace Dedoc\Scramble\Reflection; | ||
|
||
use Illuminate\Contracts\Routing\UrlRoutable; | ||
use Illuminate\Routing\Route; | ||
use Illuminate\Routing\Router; | ||
use Illuminate\Support\Reflector; | ||
use Illuminate\Support\Str; | ||
use ReflectionException; | ||
use ReflectionFunction; | ||
use ReflectionNamedType; | ||
use ReflectionParameter; | ||
use WeakMap; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
class ReflectionRoute | ||
{ | ||
private static WeakMap $cache; | ||
|
||
private function __construct(private Route $route) {} | ||
|
||
public static function createFromRoute(Route $route): static | ||
{ | ||
static::$cache ??= new WeakMap; | ||
|
||
return static::$cache[$route] ??= new static($route); | ||
} | ||
|
||
/** | ||
* 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 | ||
*/ | ||
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<string, string|null> | ||
*/ | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
194 changes: 194 additions & 0 deletions
194
src/Support/OperationExtensions/ParameterExtractor/PathParametersExtractor.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
<?php | ||
|
||
namespace Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor; | ||
|
||
use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper; | ||
use Dedoc\Scramble\Reflection\ReflectionRoute; | ||
use Dedoc\Scramble\Support\Generator\Parameter; | ||
use Dedoc\Scramble\Support\Generator\Schema; | ||
use Dedoc\Scramble\Support\Generator\Types\StringType; | ||
use Dedoc\Scramble\Support\Generator\Types\Type; | ||
use Dedoc\Scramble\Support\Generator\TypeTransformer; | ||
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\ParametersExtractionResult; | ||
use Dedoc\Scramble\Support\RouteInfo; | ||
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\Database\Eloquent\Concerns\HasUuids; | ||
use Illuminate\Database\Eloquent\Model; | ||
use Illuminate\Routing\Route; | ||
use Illuminate\Support\Arr; | ||
use Illuminate\Support\Str; | ||
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; | ||
use ReflectionParameter; | ||
|
||
class PathParametersExtractor implements ParameterExtractor | ||
{ | ||
public function __construct( | ||
private TypeTransformer $openApiTransformer, | ||
) {} | ||
|
||
public function handle(RouteInfo $routeInfo, array $parameterExtractionResults): array | ||
{ | ||
$reflectionRoute = ReflectionRoute::createFromRoute($route = $routeInfo->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); | ||
} | ||
|
||
$param->setAttribute('nonBody', 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]; | ||
} | ||
} |
Oops, something went wrong.