Skip to content

Commit 4f1eb60

Browse files
committed
Get path parameters and type from route
1 parent 1a9b8ea commit 4f1eb60

File tree

9 files changed

+443
-4
lines changed

9 files changed

+443
-4
lines changed

src/Doc.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ class Doc implements Arrayable
8484
*/
8585
private array $responses;
8686

87+
/**
88+
* A list of route path parameters, such as `/users/{id}`.
89+
*
90+
* @var string[]
91+
*/
92+
private array $pathParameters;
93+
8794
/**
8895
* The group name of the route.
8996
*
@@ -117,6 +124,7 @@ public function __construct(
117124
string $controllerFullPath,
118125
string $method,
119126
string $httpMethod,
127+
array $pathParameters,
120128
array $rules,
121129
string $docBlock
122130
) {
@@ -127,6 +135,7 @@ public function __construct(
127135
$this->controllerFullPath = $controllerFullPath;
128136
$this->method = $method;
129137
$this->httpMethod = $httpMethod;
138+
$this->pathParameters = $pathParameters;
130139
$this->rules = $rules;
131140
$this->docBlock = $docBlock;
132141
$this->responses = [];
@@ -330,6 +339,7 @@ public function toArray(): array
330339
'controller_full_path' => $this->controllerFullPath,
331340
'method' => $this->method,
332341
'http_method' => $this->httpMethod,
342+
'path_parameters' => $this->pathParameters,
333343
'rules' => $this->rules,
334344
'doc_block' => $this->docBlock,
335345
'responses' => $this->responses,

src/LaravelRequestDocs.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313

1414
class LaravelRequestDocs
1515
{
16+
private RoutePath $routePath;
17+
18+
public function __construct(RoutePath $routePath)
19+
{
20+
$this->routePath = $routePath;
21+
}
22+
1623
/**
1724
* Get a collection of {@see \Rakutentech\LaravelRequestDocs\Doc} with route and rules information.
1825
*
@@ -182,6 +189,11 @@ public function getControllersInfo(array $onlyMethods): Collection
182189
$controllerName = (new ReflectionClass($controllerFullPath))->getShortName();
183190
}
184191

192+
$paths = [];
193+
if (Str::startsWith($route->uri, 'user')) {
194+
$paths = $this->routePath->getPaths($route);
195+
}
196+
185197
$doc = new Doc(
186198
$route->uri,
187199
$routeMethods,
@@ -190,6 +202,7 @@ public function getControllersInfo(array $onlyMethods): Collection
190202
config('request-docs.hide_meta_data') ? '' : $controllerFullPath,
191203
config('request-docs.hide_meta_data') ? '' : $method,
192204
'',
205+
$paths,
193206
[],
194207
'',
195208
);

src/RoutePath.php

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<?php
2+
3+
namespace Rakutentech\LaravelRequestDocs;
4+
5+
use Illuminate\Contracts\Routing\UrlRoutable;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Routing\Route;
8+
use Illuminate\Support\Reflector;
9+
use ReflectionClass;
10+
use ReflectionNamedType;
11+
use ReflectionParameter;
12+
13+
class RoutePath
14+
{
15+
private const TYPE_MAP = [
16+
'bool' => 'boolean',
17+
'int' => 'integer',
18+
];
19+
20+
/**
21+
* @throws \ReflectionException
22+
*/
23+
public function getPaths(Route $route): array
24+
{
25+
$paths = $this->initAllParametersWithStringType($route);
26+
27+
$paths = $this->setParameterType($route, $paths);
28+
29+
$paths = $this->setOptional($route, $paths);
30+
31+
$paths = $this->mutateKeyNameWithBindingField($route, $paths);
32+
33+
return $this->setRegex($route, $paths);
34+
}
35+
36+
/**
37+
* Set route path parameter type.
38+
* This method will overwrite `$paths` type with the real types found from route declaration.
39+
*
40+
* @param \Illuminate\Routing\Route $route
41+
* @param array<string, string> $paths
42+
* @return array<string, string>
43+
* @throws \ReflectionException
44+
*/
45+
private function setParameterType(Route $route, array $paths): array
46+
{
47+
$bindableParameters = $this->getBindableParameters($route);
48+
49+
foreach ($route->parameterNames() as $position => $parameterName) {
50+
// Check `$bindableParameters` existence by comparing the position of route parameters.
51+
if (!isset($bindableParameters[$position])) {
52+
continue;
53+
}
54+
55+
$bindableParameter = $bindableParameters[$position];
56+
57+
// For builtin type, always get the type from reflection parameter.
58+
if ($bindableParameter['class'] === null) {
59+
$paths[$parameterName] = $this->getParameterType($bindableParameter['parameter']);
60+
continue;
61+
}
62+
63+
$resolved = $bindableParameter['class'];
64+
65+
// Check if is model parameter?
66+
if (!$resolved->isSubclassOf(Model::class)) {
67+
continue;
68+
}
69+
70+
// Model and path parameter name must be the same.
71+
if ($bindableParameter['parameter']->getName() !== $parameterName) {
72+
continue;
73+
}
74+
75+
$model = $resolved->newInstance();
76+
77+
// Check if model binding using another column.
78+
// Skip if user defined column except than default key.
79+
// Since we do not have the binding column type information, we set to string type.
80+
$bindingField = $route->bindingFieldFor($parameterName);
81+
if ($bindingField !== null && $bindingField !== $model->getKeyName()) {
82+
continue;
83+
}
84+
85+
// Try set type from model key type.
86+
if ($model->getKeyName() === $model->getRouteKeyName()) {
87+
$paths[$parameterName] = self::TYPE_MAP[$model->getKeyType()] ?? $model->getKeyType();
88+
}
89+
}
90+
return $paths;
91+
}
92+
93+
private function getOptionalParameterNames(string $uri): array
94+
{
95+
preg_match_all('/\{(\w+?)\?\}/', $uri, $matches);
96+
97+
return $matches[1] ?? [];
98+
}
99+
100+
/**
101+
* Get bindable parameters in ordered position that are listed in the route / controller signature.
102+
* This method will filter {@see \Illuminate\Http\Request}.
103+
* The ordering of returned parameter should be maintained to match with route path parameter.
104+
*
105+
* @param \Illuminate\Routing\Route $route
106+
* @return array<int, array{parameter: \ReflectionParameter, class: \ReflectionClass|null}>
107+
* @throws \ReflectionException
108+
*/
109+
private function getBindableParameters(Route $route): array
110+
{
111+
/** @var array<int, array{parameter: \ReflectionParameter, class: \ReflectionClass|null}> $parameters */
112+
$parameters = [];
113+
114+
foreach ($route->signatureParameters() as $reflectionParameter) {
115+
$className = Reflector::getParameterClassName($reflectionParameter);
116+
117+
// Is native type.
118+
if ($className === null) {
119+
$parameters[] = [
120+
'parameter' => $reflectionParameter,
121+
'class' => null,
122+
];
123+
continue;
124+
}
125+
126+
// Check if the class name is a bindable objects, such as model. Skip if not.
127+
$reflectionClass = new ReflectionClass($className);
128+
if (!$reflectionClass->implementsInterface(UrlRoutable::class)) {
129+
continue;
130+
}
131+
132+
$parameters[] = [
133+
'parameter' => $reflectionParameter,
134+
'class' => $reflectionClass,
135+
];
136+
}
137+
return $parameters;
138+
}
139+
140+
/**
141+
* @param \Illuminate\Routing\Route $route
142+
* @param array<string, string> $paths
143+
* @return array<string, string>
144+
*/
145+
private function setOptional(Route $route, array $paths): array
146+
{
147+
$optionalParameters = $this->getOptionalParameterNames($route->uri);
148+
149+
foreach ($paths as $parameter => $rule) {
150+
if (in_array($parameter, $optionalParameters)) {
151+
$paths[$parameter] .= '|nullable';
152+
continue;
153+
}
154+
155+
$paths[$parameter] .= '|required';
156+
}
157+
return $paths;
158+
}
159+
160+
/**
161+
* @param \Illuminate\Routing\Route $route
162+
* @param array<string, string> $paths
163+
* @return array<string, string>
164+
*/
165+
private function setRegex(Route $route, array $paths): array
166+
{
167+
foreach ($paths as $parameter => $rule) {
168+
if (!isset($route->wheres[$parameter])) {
169+
continue;
170+
}
171+
$paths[$parameter] .= '|regex:/' . $route->wheres[$parameter] . '/';
172+
}
173+
174+
return $paths;
175+
}
176+
177+
/**
178+
* Set and return route path parameters, with default string type.
179+
*
180+
* @param \Illuminate\Routing\Route $route
181+
* @return array<string, string>
182+
*/
183+
private function initAllParametersWithStringType(Route $route): array
184+
{
185+
return array_fill_keys($route->parameterNames(), 'string');
186+
}
187+
188+
/**
189+
* Get type from method reflection parameter.
190+
* Return string if type is not declared.
191+
*
192+
* @param \ReflectionParameter $methodParameter
193+
* @return string
194+
*/
195+
private function getParameterType(ReflectionParameter $methodParameter): string
196+
{
197+
$reflectionNamedType = $methodParameter->getType();
198+
199+
if ($reflectionNamedType === null) {
200+
return 'string';
201+
}
202+
203+
// See https://github.com/phpstan/phpstan/issues/3886
204+
if (!$reflectionNamedType instanceof ReflectionNamedType) {
205+
return 'string';
206+
}
207+
208+
return self::TYPE_MAP[$reflectionNamedType->getName()] ?? $reflectionNamedType->getName();
209+
}
210+
211+
/**
212+
* @param \Illuminate\Routing\Route $route
213+
* @param array<string, string> $paths
214+
* @return array<string, string>
215+
*/
216+
private function mutateKeyNameWithBindingField(Route $route, array $paths): array
217+
{
218+
$mutatedPath = [];
219+
220+
foreach ($route->parameterNames() as $name) {
221+
$bindingName = $route->bindingFieldFor($name);
222+
223+
if ($bindingName === null) {
224+
$mutatedPath[$name] = $paths[$name];
225+
continue;
226+
}
227+
228+
$mutatedPath["$name:$bindingName"] = $paths[$name];
229+
}
230+
231+
return $mutatedPath;
232+
}
233+
}

0 commit comments

Comments
 (0)