Switch to plugin architecture
Showing 14 of 29 files from the diff.
src/Tools/Generator.php
changed.
src/Tools/RouteDocBlocker.php
created.
src/Tools/Utils.php
changed.
src/Tools/Traits/ParamHelpers.php
changed.
src/Strategies/Strategy.php
created.
Other files ignored by Codecov
tests/Unit/GeneratorTestCase.php
has changed.
tests/Fixtures/TestResourceController.php
has changed.
tests/Fixtures/collection.json
has changed.
tests/GenerateDocumentationTest.php
has changed.
tests/Fixtures/TestController.php
has changed.
docs/plugins.md
is new.
src/Tools/ResponseResolver.php
was deleted.
docs/index.md
has changed.
tests/Unit/DingoGeneratorTest.php
has changed.
tests/Unit/LaravelGeneratorTest.php
has changed.
tests/Fixtures/index.md
has changed.
config/apidoc.php
has changed.
@@ -0,0 +1,100 @@
Loading
1 | + | <?php |
|
2 | + | ||
3 | + | namespace Mpociot\ApiDoc\Strategies\Metadata; |
|
4 | + | ||
5 | + | use ReflectionClass; |
|
6 | + | use ReflectionMethod; |
|
7 | + | use Illuminate\Routing\Route; |
|
8 | + | use Mpociot\Reflection\DocBlock; |
|
9 | + | use Mpociot\Reflection\DocBlock\Tag; |
|
10 | + | use Mpociot\ApiDoc\Strategies\Strategy; |
|
11 | + | use Mpociot\ApiDoc\Tools\RouteDocBlocker; |
|
12 | + | ||
13 | + | class GetFromDocBlocks extends Strategy |
|
14 | + | { |
|
15 | + | public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) |
|
16 | + | { |
|
17 | + | $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); |
|
18 | + | /** @var DocBlock $methodDocBlock */ |
|
19 | + | $methodDocBlock = $docBlocks['method']; |
|
20 | + | ||
21 | + | list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroupDescriptionAndTitle($methodDocBlock, $docBlocks['class']); |
|
22 | + | ||
23 | + | return [ |
|
24 | + | 'groupName' => $routeGroupName, |
|
25 | + | 'groupDescription' => $routeGroupDescription, |
|
26 | + | 'title' => $routeTitle ?: $methodDocBlock->getShortDescription(), |
|
27 | + | 'description' => $methodDocBlock->getLongDescription()->getContents(), |
|
28 | + | 'authenticated' => $this->getAuthStatusFromDocBlock($methodDocBlock->getTags()), |
|
29 | + | ]; |
|
30 | + | } |
|
31 | + | ||
32 | + | /** |
|
33 | + | * @param array $tags Tags in the method doc block |
|
34 | + | * |
|
35 | + | * @return bool |
|
36 | + | */ |
|
37 | + | protected function getAuthStatusFromDocBlock(array $tags) |
|
38 | + | { |
|
39 | + | $authTag = collect($tags) |
|
40 | + | ->first(function ($tag) { |
|
41 | + | return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated'; |
|
42 | + | }); |
|
43 | + | ||
44 | + | return (bool) $authTag; |
|
45 | + | } |
|
46 | + | ||
47 | + | /** |
|
48 | + | * @param DocBlock $methodDocBlock |
|
49 | + | * @param DocBlock $controllerDocBlock |
|
50 | + | * |
|
51 | + | * @return array The route group name, the group description, ad the route title |
|
52 | + | */ |
|
53 | + | protected function getRouteGroupDescriptionAndTitle(DocBlock $methodDocBlock, DocBlock $controllerDocBlock) |
|
54 | + | { |
|
55 | + | // @group tag on the method overrides that on the controller |
|
56 | + | if (! empty($methodDocBlock->getTags())) { |
|
57 | + | foreach ($methodDocBlock->getTags() as $tag) { |
|
58 | + | if ($tag->getName() === 'group') { |
|
59 | + | $routeGroupParts = explode("\n", trim($tag->getContent())); |
|
60 | + | $routeGroupName = array_shift($routeGroupParts); |
|
61 | + | $routeGroupDescription = trim(implode("\n", $routeGroupParts)); |
|
62 | + | ||
63 | + | // If the route has no title (the methodDocBlock's "short description"), |
|
64 | + | // we'll assume the routeGroupDescription is actually the title |
|
65 | + | // Something like this: |
|
66 | + | // /** |
|
67 | + | // * Fetch cars. <-- This is route title. |
|
68 | + | // * @group Cars <-- This is group name. |
|
69 | + | // * APIs for cars. <-- This is group description (not required). |
|
70 | + | // **/ |
|
71 | + | // VS |
|
72 | + | // /** |
|
73 | + | // * @group Cars <-- This is group name. |
|
74 | + | // * Fetch cars. <-- This is route title, NOT group description. |
|
75 | + | // **/ |
|
76 | + | ||
77 | + | // BTW, this is a spaghetti way of doing this. |
|
78 | + | // It shall be refactored soon. Deus vult!💪 |
|
79 | + | if (empty($methodDocBlock->getShortDescription())) { |
|
80 | + | return [$routeGroupName, '', $routeGroupDescription]; |
|
81 | + | } |
|
82 | + | ||
83 | + | return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()]; |
|
84 | + | } |
|
85 | + | } |
|
86 | + | } |
|
87 | + | ||
88 | + | foreach ($controllerDocBlock->getTags() as $tag) { |
|
89 | + | if ($tag->getName() === 'group') { |
|
90 | + | $routeGroupParts = explode("\n", trim($tag->getContent())); |
|
91 | + | $routeGroupName = array_shift($routeGroupParts); |
|
92 | + | $routeGroupDescription = implode("\n", $routeGroupParts); |
|
93 | + | ||
94 | + | return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()]; |
|
95 | + | } |
|
96 | + | } |
|
97 | + | ||
98 | + | return [$this->config->get('default_group'), '', $methodDocBlock->getShortDescription()]; |
|
99 | + | } |
|
100 | + | } |
@@ -247,7 +247,7 @@
Loading
247 | 247 | */ |
|
248 | 248 | private function isValidRoute(Route $route) |
|
249 | 249 | { |
|
250 | - | $action = Utils::getRouteActionUses($route->getAction()); |
|
250 | + | $action = Utils::getRouteClassAndMethodNames($route->getAction()); |
|
251 | 251 | if (is_array($action)) { |
|
252 | 252 | $action = implode('@', $action); |
|
253 | 253 | } |
@@ -264,7 +264,7 @@
Loading
264 | 264 | */ |
|
265 | 265 | private function isRouteVisibleForDocumentation(array $action) |
|
266 | 266 | { |
|
267 | - | list($class, $method) = Utils::getRouteActionUses($action); |
|
267 | + | list($class, $method) = Utils::getRouteClassAndMethodNames($action); |
|
268 | 268 | $reflection = new ReflectionClass($class); |
|
269 | 269 | ||
270 | 270 | if (! $reflection->hasMethod($method)) { |
@@ -1,26 +1,36 @@
Loading
1 | 1 | <?php |
|
2 | 2 | ||
3 | - | namespace Mpociot\ApiDoc\Tools\ResponseStrategies; |
|
3 | + | namespace Mpociot\ApiDoc\Strategies\Responses; |
|
4 | 4 | ||
5 | 5 | use Illuminate\Routing\Route; |
|
6 | - | use Illuminate\Http\JsonResponse; |
|
6 | + | use Mpociot\Reflection\DocBlock; |
|
7 | 7 | use Mpociot\Reflection\DocBlock\Tag; |
|
8 | + | use Mpociot\ApiDoc\Strategies\Strategy; |
|
9 | + | use Mpociot\ApiDoc\Tools\RouteDocBlocker; |
|
8 | 10 | ||
9 | 11 | /** |
|
10 | 12 | * Get a response from from a file in the docblock ( @responseFile ). |
|
11 | 13 | */ |
|
12 | - | class ResponseFileStrategy |
|
14 | + | class UseResponseFileTag extends Strategy |
|
13 | 15 | { |
|
14 | 16 | /** |
|
15 | 17 | * @param Route $route |
|
16 | - | * @param array $tags |
|
17 | - | * @param array $routeProps |
|
18 | + | * @param \ReflectionClass $controller |
|
19 | + | * @param \ReflectionMethod $method |
|
20 | + | * @param array $routeRules |
|
21 | + | * @param array $context |
|
22 | + | * |
|
23 | + | * @throws \Exception |
|
18 | 24 | * |
|
19 | 25 | * @return array|null |
|
20 | 26 | */ |
|
21 | - | public function __invoke(Route $route, array $tags, array $routeProps) |
|
27 | + | public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = []) |
|
22 | 28 | { |
|
23 | - | return $this->getFileResponses($tags); |
|
29 | + | $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); |
|
30 | + | /** @var DocBlock $methodDocBlock */ |
|
31 | + | $methodDocBlock = $docBlocks['method']; |
|
32 | + | ||
33 | + | return $this->getFileResponses($methodDocBlock->getTags()); |
|
24 | 34 | } |
|
25 | 35 | ||
26 | 36 | /** |
@@ -43,14 +53,17 @@
Loading
43 | 53 | return null; |
|
44 | 54 | } |
|
45 | 55 | ||
46 | - | return array_map(function (Tag $responseFileTag) { |
|
56 | + | $responses = array_map(function (Tag $responseFileTag) { |
|
47 | 57 | preg_match('/^(\d{3})?\s?([\S]*[\s]*?)(\{.*\})?$/', $responseFileTag->getContent(), $result); |
|
48 | 58 | $status = $result[1] ?: 200; |
|
49 | 59 | $content = $result[2] ? file_get_contents(storage_path(trim($result[2])), true) : '{}'; |
|
50 | 60 | $json = ! empty($result[3]) ? str_replace("'", '"', $result[3]) : '{}'; |
|
51 | 61 | $merged = array_merge(json_decode($content, true), json_decode($json, true)); |
|
52 | 62 | ||
53 | - | return new JsonResponse($merged, (int) $status); |
|
63 | + | return [json_encode($merged), (int) $status]; |
|
54 | 64 | }, $responseFileTags); |
|
65 | + | ||
66 | + | // Convert responses to [200 => 'response', 401 => 'response'] |
|
67 | + | return collect($responses)->pluck('0', '1')->toArray(); |
|
55 | 68 | } |
|
56 | 69 | } |
@@ -2,19 +2,14 @@
Loading
2 | 2 | ||
3 | 3 | namespace Mpociot\ApiDoc\Tools; |
|
4 | 4 | ||
5 | - | use Faker\Factory; |
|
6 | 5 | use ReflectionClass; |
|
7 | 6 | use ReflectionMethod; |
|
7 | + | use Illuminate\Support\Arr; |
|
8 | 8 | use Illuminate\Support\Str; |
|
9 | 9 | use Illuminate\Routing\Route; |
|
10 | - | use Mpociot\Reflection\DocBlock; |
|
11 | - | use Mpociot\Reflection\DocBlock\Tag; |
|
12 | - | use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; |
|
13 | 10 | ||
14 | 11 | class Generator |
|
15 | 12 | { |
|
16 | - | use ParamHelpers; |
|
17 | - | ||
18 | 13 | /** |
|
19 | 14 | * @var DocumentationConfig |
|
20 | 15 | */ |
@@ -54,389 +49,145 @@
Loading
54 | 49 | */ |
|
55 | 50 | public function processRoute(Route $route, array $rulesToApply = []) |
|
56 | 51 | { |
|
57 | - | list($class, $method) = Utils::getRouteActionUses($route->getAction()); |
|
58 | - | $controller = new ReflectionClass($class); |
|
59 | - | $method = $controller->getMethod($method); |
|
60 | - | ||
61 | - | $docBlock = $this->parseDocBlock($method); |
|
62 | - | list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroup($controller, $docBlock); |
|
63 | - | $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']); |
|
64 | - | $queryParameters = $this->getQueryParameters($method, $docBlock['tags']); |
|
65 | - | $content = ResponseResolver::getResponse($route, $docBlock['tags'], [ |
|
66 | - | 'rules' => $rulesToApply, |
|
67 | - | 'body' => $bodyParameters, |
|
68 | - | 'query' => $queryParameters, |
|
69 | - | ]); |
|
52 | + | list($controllerName, $methodName) = Utils::getRouteClassAndMethodNames($route->getAction()); |
|
53 | + | $controller = new ReflectionClass($controllerName); |
|
54 | + | $method = $controller->getMethod($methodName); |
|
70 | 55 | ||
71 | 56 | $parsedRoute = [ |
|
72 | 57 | 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), |
|
73 | - | 'groupName' => $routeGroupName, |
|
74 | - | 'groupDescription' => $routeGroupDescription, |
|
75 | - | 'title' => $routeTitle ?: $docBlock['short'], |
|
76 | - | 'description' => $docBlock['long'], |
|
77 | 58 | 'methods' => $this->getMethods($route), |
|
78 | 59 | 'uri' => $this->getUri($route), |
|
79 | 60 | 'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])), |
|
80 | - | 'queryParameters' => $queryParameters, |
|
81 | - | 'bodyParameters' => $bodyParameters, |
|
82 | - | 'cleanBodyParameters' => $this->cleanParams($bodyParameters), |
|
83 | - | 'cleanQueryParameters' => $this->cleanParams($queryParameters), |
|
84 | - | 'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']), |
|
85 | - | 'response' => $content, |
|
86 | - | 'showresponse' => ! empty($content), |
|
87 | 61 | ]; |
|
88 | - | $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; |
|
62 | + | $metadata = $this->fetchMetadata($controller, $method, $route, $rulesToApply, $parsedRoute); |
|
63 | + | $parsedRoute['metadata'] = $metadata; |
|
64 | + | $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply, $parsedRoute); |
|
65 | + | $parsedRoute['bodyParameters'] = $bodyParameters; |
|
66 | + | $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters); |
|
89 | 67 | ||
90 | - | return $parsedRoute; |
|
91 | - | } |
|
68 | + | $queryParameters = $this->fetchQueryParameters($controller, $method, $route, $rulesToApply, $parsedRoute); |
|
69 | + | $parsedRoute['queryParameters'] = $queryParameters; |
|
70 | + | $parsedRoute['cleanQueryParameters'] = $this->cleanParams($queryParameters); |
|
92 | 71 | ||
93 | - | protected function getBodyParameters(ReflectionMethod $method, array $tags) |
|
94 | - | { |
|
95 | - | foreach ($method->getParameters() as $param) { |
|
96 | - | $paramType = $param->getType(); |
|
97 | - | if ($paramType === null) { |
|
98 | - | continue; |
|
99 | - | } |
|
100 | - | ||
101 | - | $parameterClassName = version_compare(phpversion(), '7.1.0', '<') |
|
102 | - | ? $paramType->__toString() |
|
103 | - | : $paramType->getName(); |
|
104 | - | ||
105 | - | try { |
|
106 | - | $parameterClass = new ReflectionClass($parameterClassName); |
|
107 | - | } catch (\ReflectionException $e) { |
|
108 | - | continue; |
|
109 | - | } |
|
110 | - | ||
111 | - | if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { |
|
112 | - | $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); |
|
113 | - | $bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags()); |
|
114 | - | ||
115 | - | if (count($bodyParametersFromDocBlock)) { |
|
116 | - | return $bodyParametersFromDocBlock; |
|
117 | - | } |
|
118 | - | } |
|
119 | - | } |
|
120 | - | ||
121 | - | return $this->getBodyParametersFromDocBlock($tags); |
|
122 | - | } |
|
123 | - | ||
124 | - | /** |
|
125 | - | * @param array $tags |
|
126 | - | * |
|
127 | - | * @return array |
|
128 | - | */ |
|
129 | - | protected function getBodyParametersFromDocBlock(array $tags) |
|
130 | - | { |
|
131 | - | $parameters = collect($tags) |
|
132 | - | ->filter(function ($tag) { |
|
133 | - | return $tag instanceof Tag && $tag->getName() === 'bodyParam'; |
|
134 | - | }) |
|
135 | - | ->mapWithKeys(function ($tag) { |
|
136 | - | preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); |
|
137 | - | $content = preg_replace('/\s?No-example.?/', '', $content); |
|
138 | - | if (empty($content)) { |
|
139 | - | // this means only name and type were supplied |
|
140 | - | list($name, $type) = preg_split('/\s+/', $tag->getContent()); |
|
141 | - | $required = false; |
|
142 | - | $description = ''; |
|
143 | - | } else { |
|
144 | - | list($_, $name, $type, $required, $description) = $content; |
|
145 | - | $description = trim($description); |
|
146 | - | if ($description == 'required' && empty(trim($required))) { |
|
147 | - | $required = $description; |
|
148 | - | $description = ''; |
|
149 | - | } |
|
150 | - | $required = trim($required) == 'required' ? true : false; |
|
151 | - | } |
|
72 | + | $responses = $this->fetchResponses($controller, $method, $route, $rulesToApply, $parsedRoute); |
|
73 | + | $parsedRoute['response'] = $responses; |
|
74 | + | $parsedRoute['showresponse'] = ! empty($responses); |
|
152 | 75 | ||
153 | - | $type = $this->normalizeParameterType($type); |
|
154 | - | list($description, $example) = $this->parseDescription($description, $type); |
|
155 | - | $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; |
|
76 | + | $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; |
|
156 | 77 | ||
157 | - | return [$name => compact('type', 'description', 'required', 'value')]; |
|
158 | - | })->toArray(); |
|
78 | + | $parsedRoute += $metadata; |
|
159 | 79 | ||
160 | - | return $parameters; |
|
80 | + | return $parsedRoute; |
|
161 | 81 | } |
|
162 | 82 | ||
163 | - | /** |
|
164 | - | * @param ReflectionMethod $method |
|
165 | - | * @param array $tags |
|
166 | - | * |
|
167 | - | * @return array |
|
168 | - | */ |
|
169 | - | protected function getQueryParameters(ReflectionMethod $method, array $tags) |
|
83 | + | protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) |
|
170 | 84 | { |
|
171 | - | foreach ($method->getParameters() as $param) { |
|
172 | - | $paramType = $param->getType(); |
|
173 | - | if ($paramType === null) { |
|
174 | - | continue; |
|
175 | - | } |
|
176 | - | ||
177 | - | $parameterClassName = version_compare(phpversion(), '7.1.0', '<') |
|
178 | - | ? $paramType->__toString() |
|
179 | - | : $paramType->getName(); |
|
180 | - | ||
181 | - | try { |
|
182 | - | $parameterClass = new ReflectionClass($parameterClassName); |
|
183 | - | } catch (\ReflectionException $e) { |
|
184 | - | continue; |
|
185 | - | } |
|
186 | - | ||
187 | - | if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { |
|
188 | - | $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); |
|
189 | - | $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags()); |
|
190 | - | ||
191 | - | if (count($queryParametersFromDocBlock)) { |
|
192 | - | return $queryParametersFromDocBlock; |
|
193 | - | } |
|
194 | - | } |
|
195 | - | } |
|
85 | + | $context['metadata'] = [ |
|
86 | + | 'groupName' => $this->config->get('default_group', ''), |
|
87 | + | 'groupDescription' => '', |
|
88 | + | 'title' => '', |
|
89 | + | 'description' => '', |
|
90 | + | 'authenticated' => false, |
|
91 | + | ]; |
|
196 | 92 | ||
197 | - | return $this->getQueryParametersFromDocBlock($tags); |
|
93 | + | return $this->iterateThroughStrategies('metadata', $context, [$route, $controller, $method, $rulesToApply]); |
|
198 | 94 | } |
|
199 | 95 | ||
200 | - | /** |
|
201 | - | * @param array $tags |
|
202 | - | * |
|
203 | - | * @return array |
|
204 | - | */ |
|
205 | - | protected function getQueryParametersFromDocBlock(array $tags) |
|
96 | + | protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) |
|
206 | 97 | { |
|
207 | - | $parameters = collect($tags) |
|
208 | - | ->filter(function ($tag) { |
|
209 | - | return $tag instanceof Tag && $tag->getName() === 'queryParam'; |
|
210 | - | }) |
|
211 | - | ->mapWithKeys(function ($tag) { |
|
212 | - | preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); |
|
213 | - | $content = preg_replace('/\s?No-example.?/', '', $content); |
|
214 | - | if (empty($content)) { |
|
215 | - | // this means only name was supplied |
|
216 | - | list($name) = preg_split('/\s+/', $tag->getContent()); |
|
217 | - | $required = false; |
|
218 | - | $description = ''; |
|
219 | - | } else { |
|
220 | - | list($_, $name, $required, $description) = $content; |
|
221 | - | $description = trim($description); |
|
222 | - | if ($description == 'required' && empty(trim($required))) { |
|
223 | - | $required = $description; |
|
224 | - | $description = ''; |
|
225 | - | } |
|
226 | - | $required = trim($required) == 'required' ? true : false; |
|
227 | - | } |
|
228 | - | ||
229 | - | list($description, $value) = $this->parseDescription($description, 'string'); |
|
230 | - | if (is_null($value) && ! $this->shouldExcludeExample($tag)) { |
|
231 | - | $value = Str::contains($description, ['number', 'count', 'page']) |
|
232 | - | ? $this->generateDummyValue('integer') |
|
233 | - | : $this->generateDummyValue('string'); |
|
234 | - | } |
|
235 | - | ||
236 | - | return [$name => compact('description', 'required', 'value')]; |
|
237 | - | })->toArray(); |
|
238 | - | ||
239 | - | return $parameters; |
|
98 | + | return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]); |
|
240 | 99 | } |
|
241 | 100 | ||
242 | - | /** |
|
243 | - | * @param array $tags |
|
244 | - | * |
|
245 | - | * @return bool |
|
246 | - | */ |
|
247 | - | protected function getAuthStatusFromDocBlock(array $tags) |
|
101 | + | protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) |
|
248 | 102 | { |
|
249 | - | $authTag = collect($tags) |
|
250 | - | ->first(function ($tag) { |
|
251 | - | return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated'; |
|
252 | - | }); |
|
253 | - | ||
254 | - | return (bool) $authTag; |
|
103 | + | return $this->iterateThroughStrategies('queryParameters', $context, [$route, $controller, $method, $rulesToApply]); |
|
255 | 104 | } |
|
256 | 105 | ||
257 | - | /** |
|
258 | - | * @param ReflectionMethod $method |
|
259 | - | * |
|
260 | - | * @return array |
|
261 | - | */ |
|
262 | - | protected function parseDocBlock(ReflectionMethod $method) |
|
106 | + | protected function fetchResponses(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) |
|
263 | 107 | { |
|
264 | - | $comment = $method->getDocComment(); |
|
265 | - | $phpdoc = new DocBlock($comment); |
|
108 | + | $responses = $this->iterateThroughStrategies('responses', $context, [$route, $controller, $method, $rulesToApply]); |
|
109 | + | if (count($responses)) { |
|
110 | + | return collect($responses)->map(function (string $response, int $status) { |
|
111 | + | return [ |
|
112 | + | 'status' => $status ?: 200, |
|
113 | + | 'content' => $response, |
|
114 | + | ]; |
|
115 | + | })->values()->toArray(); |
|
116 | + | } |
|
266 | 117 | ||
267 | - | return [ |
|
268 | - | 'short' => $phpdoc->getShortDescription(), |
|
269 | - | 'long' => $phpdoc->getLongDescription()->getContents(), |
|
270 | - | 'tags' => $phpdoc->getTags(), |
|
271 | - | ]; |
|
118 | + | return null; |
|
272 | 119 | } |
|
273 | 120 | ||
274 | - | /** |
|
275 | - | * @param ReflectionClass $controller |
|
276 | - | * @param array $methodDocBlock |
|
277 | - | * |
|
278 | - | * @return array The route group name, the group description, ad the route title |
|
279 | - | */ |
|
280 | - | protected function getRouteGroup(ReflectionClass $controller, array $methodDocBlock) |
|
121 | + | protected function iterateThroughStrategies(string $stage, array $context, array $arguments) |
|
281 | 122 | { |
|
282 | - | // @group tag on the method overrides that on the controller |
|
283 | - | if (! empty($methodDocBlock['tags'])) { |
|
284 | - | foreach ($methodDocBlock['tags'] as $tag) { |
|
285 | - | if ($tag->getName() === 'group') { |
|
286 | - | $routeGroupParts = explode("\n", trim($tag->getContent())); |
|
287 | - | $routeGroupName = array_shift($routeGroupParts); |
|
288 | - | $routeGroupDescription = trim(implode("\n", $routeGroupParts)); |
|
289 | - | ||
290 | - | // If the route has no title (aka "short"), |
|
291 | - | // we'll assume the routeGroupDescription is actually the title |
|
292 | - | // Something like this: |
|
293 | - | // /** |
|
294 | - | // * Fetch cars. <-- This is route title. |
|
295 | - | // * @group Cars <-- This is group name. |
|
296 | - | // * APIs for cars. <-- This is group description (not required). |
|
297 | - | // **/ |
|
298 | - | // VS |
|
299 | - | // /** |
|
300 | - | // * @group Cars <-- This is group name. |
|
301 | - | // * Fetch cars. <-- This is route title, NOT group description. |
|
302 | - | // **/ |
|
123 | + | $strategies = $this->config->get("strategies.$stage", []); |
|
124 | + | $context[$stage] = $context[$stage] ?? []; |
|
125 | + | foreach ($strategies as $strategyClass) { |
|
126 | + | $strategy = new $strategyClass($stage, $this->config); |
|
127 | + | $arguments[] = $context; |
|
128 | + | $results = $strategy(...$arguments); |
|
129 | + | if (! is_null($results)) { |
|
130 | + | foreach ($results as $index => $item) { |
|
131 | + | // Using a for loop rather than array_merge or += |
|
132 | + | // so it does not renumber numeric keys |
|
133 | + | // and also allows values to be overwritten |
|
303 | 134 | ||
304 | - | // BTW, this is a spaghetti way of doing this. |
|
305 | - | // It shall be refactored soon. Deus vult!💪 |
|
306 | - | if (empty($methodDocBlock['short'])) { |
|
307 | - | return [$routeGroupName, '', $routeGroupDescription]; |
|
135 | + | // Don't allow overwriting if an empty value is trying to replace a set one |
|
136 | + | if (! in_array($context[$stage], [null, ''], true) && in_array($item, [null, ''], true)) { |
|
137 | + | continue; |
|
138 | + | } else { |
|
139 | + | $context[$stage][$index] = $item; |
|
308 | 140 | } |
|
309 | - | ||
310 | - | return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']]; |
|
311 | - | } |
|
312 | - | } |
|
313 | - | } |
|
314 | - | ||
315 | - | $docBlockComment = $controller->getDocComment(); |
|
316 | - | if ($docBlockComment) { |
|
317 | - | $phpdoc = new DocBlock($docBlockComment); |
|
318 | - | foreach ($phpdoc->getTags() as $tag) { |
|
319 | - | if ($tag->getName() === 'group') { |
|
320 | - | $routeGroupParts = explode("\n", trim($tag->getContent())); |
|
321 | - | $routeGroupName = array_shift($routeGroupParts); |
|
322 | - | $routeGroupDescription = implode("\n", $routeGroupParts); |
|
323 | - | ||
324 | - | return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']]; |
|
325 | 141 | } |
|
326 | 142 | } |
|
327 | 143 | } |
|
328 | 144 | ||
329 | - | return [$this->config->get(('default_group')), '', $methodDocBlock['short']]; |
|
330 | - | } |
|
331 | - | ||
332 | - | private function normalizeParameterType($type) |
|
333 | - | { |
|
334 | - | $typeMap = [ |
|
335 | - | 'int' => 'integer', |
|
336 | - | 'bool' => 'boolean', |
|
337 | - | 'double' => 'float', |
|
338 | - | ]; |
|
339 | - | ||
340 | - | return $type ? ($typeMap[$type] ?? $type) : 'string'; |
|
341 | - | } |
|
342 | - | ||
343 | - | private function generateDummyValue(string $type) |
|
344 | - | { |
|
345 | - | $faker = Factory::create(); |
|
346 | - | if ($this->config->get('faker_seed')) { |
|
347 | - | $faker->seed($this->config->get('faker_seed')); |
|
348 | - | } |
|
349 | - | $fakeFactories = [ |
|
350 | - | 'integer' => function () use ($faker) { |
|
351 | - | return $faker->numberBetween(1, 20); |
|
352 | - | }, |
|
353 | - | 'number' => function () use ($faker) { |
|
354 | - | return $faker->randomFloat(); |
|
355 | - | }, |
|
356 | - | 'float' => function () use ($faker) { |
|
357 | - | return $faker->randomFloat(); |
|
358 | - | }, |
|
359 | - | 'boolean' => function () use ($faker) { |
|
360 | - | return $faker->boolean(); |
|
361 | - | }, |
|
362 | - | 'string' => function () use ($faker) { |
|
363 | - | return $faker->word; |
|
364 | - | }, |
|
365 | - | 'array' => function () { |
|
366 | - | return []; |
|
367 | - | }, |
|
368 | - | 'object' => function () { |
|
369 | - | return new \stdClass; |
|
370 | - | }, |
|
371 | - | ]; |
|
372 | - | ||
373 | - | $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string']; |
|
374 | - | ||
375 | - | return $fakeFactory(); |
|
145 | + | return $context[$stage]; |
|
376 | 146 | } |
|
377 | 147 | ||
378 | 148 | /** |
|
379 | - | * Allows users to specify an example for the parameter by writing 'Example: the-example', |
|
380 | - | * to be used in example requests and response calls. |
|
149 | + | * Create samples at index 0 for array parameters. |
|
150 | + | * Also filter out parameters which were excluded from having examples. |
|
381 | 151 | * |
|
382 | - | * @param string $description |
|
383 | - | * @param string $type The type of the parameter. Used to cast the example provided, if any. |
|
152 | + | * @param array $params |
|
384 | 153 | * |
|
385 | - | * @return array The description and included example. |
|
154 | + | * @return array |
|
386 | 155 | */ |
|
387 | - | private function parseDescription(string $description, string $type) |
|
156 | + | protected function cleanParams(array $params) |
|
388 | 157 | { |
|
389 | - | $example = null; |
|
390 | - | if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { |
|
391 | - | $description = $content[1]; |
|
158 | + | $values = []; |
|
392 | 159 | ||
393 | - | // examples are parsed as strings by default, we need to cast them properly |
|
394 | - | $example = $this->castToType($content[2], $type); |
|
395 | - | } |
|
160 | + | // Remove params which have no examples. |
|
161 | + | $params = array_filter($params, function ($details) { |
|
162 | + | return ! is_null($details['value']); |
|
163 | + | }); |
|
396 | 164 | ||
397 | - | return [$description, $example]; |
|
398 | - | } |
|
165 | + | foreach ($params as $paramName => $details) { |
|
166 | + | $this->generateConcreteSampleForArrayKeys( |
|
167 | + | $paramName, $details['value'], $values |
|
168 | + | ); |
|
169 | + | } |
|
399 | 170 | ||
400 | - | /** |
|
401 | - | * Allows users to specify that we shouldn't generate an example for the parameter |
|
402 | - | * by writing 'No-example'. |
|
403 | - | * |
|
404 | - | * @param Tag $tag |
|
405 | - | * |
|
406 | - | * @return bool Whether no example should be generated |
|
407 | - | */ |
|
408 | - | private function shouldExcludeExample(Tag $tag) |
|
409 | - | { |
|
410 | - | return strpos($tag->getContent(), ' No-example') !== false; |
|
171 | + | return $values; |
|
411 | 172 | } |
|
412 | 173 | ||
413 | 174 | /** |
|
414 | - | * Cast a value from a string to a specified type. |
|
175 | + | * For each array notation parameter (eg user.*, item.*.name, object.*.*, user[]) |
|
176 | + | * generate concrete sample (user.0, item.0.name, object.0.0, user.0) with example as value. |
|
415 | 177 | * |
|
416 | - | * @param string $value |
|
417 | - | * @param string $type |
|
178 | + | * @param string $paramName |
|
179 | + | * @param mixed $paramExample |
|
180 | + | * @param array $values The array that holds the result |
|
418 | 181 | * |
|
419 | - | * @return mixed |
|
182 | + | * @return void |
|
420 | 183 | */ |
|
421 | - | private function castToType(string $value, string $type) |
|
184 | + | protected function generateConcreteSampleForArrayKeys($paramName, $paramExample, array &$values = []) |
|
422 | 185 | { |
|
423 | - | $casts = [ |
|
424 | - | 'integer' => 'intval', |
|
425 | - | 'number' => 'floatval', |
|
426 | - | 'float' => 'floatval', |
|
427 | - | 'boolean' => 'boolval', |
|
428 | - | ]; |
|
429 | - | ||
430 | - | // First, we handle booleans. We can't use a regular cast, |
|
431 | - | //because PHP considers string 'false' as true. |
|
432 | - | if ($value == 'false' && $type == 'boolean') { |
|
433 | - | return false; |
|
186 | + | if (Str::contains($paramName, '[')) { |
|
187 | + | // Replace usages of [] with dot notation |
|
188 | + | $paramName = str_replace(['][', '[', ']', '..'], ['.', '.', '', '.*.'], $paramName); |
|
434 | 189 | } |
|
435 | - | ||
436 | - | if (isset($casts[$type])) { |
|
437 | - | return $casts[$type]($value); |
|
438 | - | } |
|
439 | - | ||
440 | - | return $value; |
|
190 | + | // Then generate a sample item for the dot notation |
|
191 | + | Arr::set($values, str_replace('.*', '.0', $paramName), $paramExample); |
|
441 | 192 | } |
|
442 | 193 | } |
@@ -0,0 +1,90 @@
Loading
1 | + | <?php |
|
2 | + | ||
3 | + | namespace Mpociot\ApiDoc\Strategies\BodyParameters; |
|
4 | + | ||
5 | + | use ReflectionClass; |
|
6 | + | use ReflectionMethod; |
|
7 | + | use Illuminate\Routing\Route; |
|
8 | + | use Mpociot\Reflection\DocBlock; |
|
9 | + | use Mpociot\Reflection\DocBlock\Tag; |
|
10 | + | use Mpociot\ApiDoc\Strategies\Strategy; |
|
11 | + | use Mpociot\ApiDoc\Tools\RouteDocBlocker; |
|
12 | + | use Dingo\Api\Http\FormRequest as DingoFormRequest; |
|
13 | + | use Mpociot\ApiDoc\Tools\Traits\DocBlockParamHelpers; |
|
14 | + | use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; |
|
15 | + | ||
16 | + | class GetFromBodyParamTag extends Strategy |
|
17 | + | { |
|
18 | + | use DocBlockParamHelpers; |
|
19 | + | ||
20 | + | public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) |
|
21 | + | { |
|
22 | + | foreach ($method->getParameters() as $param) { |
|
23 | + | $paramType = $param->getType(); |
|
24 | + | if ($paramType === null) { |
|
25 | + | continue; |
|
26 | + | } |
|
27 | + | ||
28 | + | $parameterClassName = version_compare(phpversion(), '7.1.0', '<') |
|
29 | + | ? $paramType->__toString() |
|
30 | + | : $paramType->getName(); |
|
31 | + | ||
32 | + | try { |
|
33 | + | $parameterClass = new ReflectionClass($parameterClassName); |
|
34 | + | } catch (\ReflectionException $e) { |
|
35 | + | continue; |
|
36 | + | } |
|
37 | + | ||
38 | + | // If there's a FormRequest, we check there for @bodyParam tags. |
|
39 | + | if (class_exists(LaravelFormRequest::class) && $parameterClass->isSubclassOf(LaravelFormRequest::class) |
|
40 | + | || class_exists(DingoFormRequest::class) && $parameterClass->isSubclassOf(DingoFormRequest::class)) { |
|
41 | + | $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); |
|
42 | + | $bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags()); |
|
43 | + | ||
44 | + | if (count($bodyParametersFromDocBlock)) { |
|
45 | + | return $bodyParametersFromDocBlock; |
|
46 | + | } |
|
47 | + | } |
|
48 | + | } |
|
49 | + | ||
50 | + | $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method']; |
|
51 | + | ||
52 | + | return $this->getBodyParametersFromDocBlock($methodDocBlock->getTags()); |
|
53 | + | } |
|
54 | + | ||
55 | + | private function getBodyParametersFromDocBlock($tags) |
|
56 | + | { |
|
57 | + | $parameters = collect($tags) |
|
58 | + | ->filter(function ($tag) { |
|
59 | + | return $tag instanceof Tag && $tag->getName() === 'bodyParam'; |
|
60 | + | }) |
|
61 | + | ->mapWithKeys(function ($tag) { |
|
62 | + | preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); |
|
63 | + | $content = preg_replace('/\s?No-example.?/', '', $content); |
|
64 | + | if (empty($content)) { |
|
65 | + | // this means only name and type were supplied |
|
66 | + | list($name, $type) = preg_split('/\s+/', $tag->getContent()); |
|
67 | + | $required = false; |
|
68 | + | $description = ''; |
|
69 | + | } else { |
|
70 | + | list($_, $name, $type, $required, $description) = $content; |
|
71 | + | $description = trim($description); |
|
72 | + | if ($description == 'required' && empty(trim($required))) { |
|
73 | + | $required = $description; |
|
74 | + | $description = ''; |
|
75 | + | } |
|
76 | + | $required = trim($required) == 'required' ? true : false; |
|
77 | + | } |
|
78 | + | ||
79 | + | $type = $this->normalizeParameterType($type); |
|
80 | + | list($description, $example) = $this->parseParamDescription($description, $type); |
|
81 | + | $value = is_null($example) && ! $this->shouldExcludeExample($tag) |
|
82 | + | ? $this->generateDummyValue($type) |
|
83 | + | : $example; |
|
84 | + | ||
85 | + | return [$name => compact('type', 'description', 'required', 'value')]; |
|
86 | + | })->toArray(); |
|
87 | + | ||
88 | + | return $parameters; |
|
89 | + | } |
|
90 | + | } |
@@ -0,0 +1,64 @@
Loading
1 | + | <?php |
|
2 | + | ||
3 | + | namespace Mpociot\ApiDoc\Tools; |
|
4 | + | ||
5 | + | use ReflectionClass; |
|
6 | + | use Illuminate\Routing\Route; |
|
7 | + | use Mpociot\Reflection\DocBlock; |
|
8 | + | ||
9 | + | class RouteDocBlocker |
|
10 | + | { |
|
11 | + | protected static $docBlocks = []; |
|
12 | + | ||
13 | + | /** |
|
14 | + | * @param Route $route |
|
15 | + | * |
|
16 | + | * @throws \ReflectionException |
|
17 | + | * |
|
18 | + | * @return array<string, DocBlock> Method and class docblocks |
|
19 | + | */ |
|
20 | + | public static function getDocBlocksFromRoute(Route $route): array |
|
21 | + | { |
|
22 | + | list($className, $methodName) = Utils::getRouteClassAndMethodNames($route); |
|
23 | + | $docBlocks = self::getCachedDocBlock($route, $className, $methodName); |
|
24 | + | if ($docBlocks) { |
|
25 | + | return $docBlocks; |
|
26 | + | } |
|
27 | + | ||
28 | + | $class = new ReflectionClass($className); |
|
29 | + | ||
30 | + | if (! $class->hasMethod($methodName)) { |
|
31 | + | throw new \Exception("Error while fetching docblock for route: Class $className does not contain method $methodName"); |
|
32 | + | } |
|
33 | + | ||
34 | + | $docBlocks = [ |
|
35 | + | 'method' => new DocBlock($class->getMethod($methodName)->getDocComment() ?: ''), |
|
36 | + | 'class' => new DocBlock($class->getDocComment() ?: ''), |
|
37 | + | ]; |
|
38 | + | self::cacheDocBlocks($route, $className, $methodName, $docBlocks); |
|
39 | + | ||
40 | + | return $docBlocks; |
|
41 | + | } |
|
42 | + | ||
43 | + | protected static function getCachedDocBlock(Route $route, string $className, string $methodName) |
|
44 | + | { |
|
45 | + | $routeId = self::getRouteCacheId($route, $className, $methodName); |
|
46 | + | ||
47 | + | return self::$docBlocks[$routeId] ?? null; |
|
48 | + | } |
|
49 | + | ||
50 | + | protected static function cacheDocBlocks(Route $route, string $className, string $methodName, array $docBlocks) |
|
51 | + | { |
|
52 | + | $routeId = self::getRouteCacheId($route, $className, $methodName); |
|
53 | + | self::$docBlocks[$routeId] = $docBlocks; |
|
54 | + | } |
|
55 | + | ||
56 | + | private static function getRouteCacheId(Route $route, string $className, string $methodName): string |
|
57 | + | { |
|
58 | + | return $route->uri() |
|
59 | + | .':' |
|
60 | + | .implode(array_diff($route->methods(), ['HEAD'])) |
|
61 | + | .$className |
|
62 | + | .$methodName; |
|
63 | + | } |
|
64 | + | } |
@@ -1,6 +1,6 @@
Loading
1 | 1 | <?php |
|
2 | 2 | ||
3 | - | namespace Mpociot\ApiDoc\Tools\ResponseStrategies; |
|
3 | + | namespace Mpociot\ApiDoc\Strategies\Responses; |
|
4 | 4 | ||
5 | 5 | use Dingo\Api\Dispatcher; |
|
6 | 6 | use Illuminate\Support\Str; |
@@ -9,34 +9,41 @@
Loading
9 | 9 | use Illuminate\Routing\Route; |
|
10 | 10 | use Mpociot\ApiDoc\Tools\Flags; |
|
11 | 11 | use Mpociot\ApiDoc\Tools\Utils; |
|
12 | + | use Mpociot\ApiDoc\Strategies\Strategy; |
|
12 | 13 | use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; |
|
13 | 14 | ||
14 | 15 | /** |
|
15 | 16 | * Make a call to the route and retrieve its response. |
|
16 | 17 | */ |
|
17 | - | class ResponseCallStrategy |
|
18 | + | class ResponseCalls extends Strategy |
|
18 | 19 | { |
|
19 | 20 | use ParamHelpers; |
|
20 | 21 | ||
21 | 22 | /** |
|
22 | 23 | * @param Route $route |
|
23 | - | * @param array $tags |
|
24 | - | * @param array $routeProps |
|
24 | + | * @param \ReflectionClass $controller |
|
25 | + | * @param \ReflectionMethod $method |
|
26 | + | * @param array $routeRules |
|
27 | + | * @param array $context |
|
25 | 28 | * |
|
26 | 29 | * @return array|null |
|
27 | 30 | */ |
|
28 | - | public function __invoke(Route $route, array $tags, array $routeProps) |
|
31 | + | public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = []) |
|
29 | 32 | { |
|
30 | - | $rulesToApply = $routeProps['rules']['response_calls'] ?? []; |
|
31 | - | if (! $this->shouldMakeApiCall($route, $rulesToApply)) { |
|
33 | + | $rulesToApply = $routeRules['response_calls'] ?? []; |
|
34 | + | if (! $this->shouldMakeApiCall($route, $rulesToApply, $context)) { |
|
32 | 35 | return null; |
|
33 | 36 | } |
|
34 | 37 | ||
35 | 38 | $this->configureEnvironment($rulesToApply); |
|
36 | - | $request = $this->prepareRequest($route, $rulesToApply, $routeProps['body'], $routeProps['query']); |
|
39 | + | ||
40 | + | // Mix in parsed parameters with manually specified parameters. |
|
41 | + | $bodyParameters = array_merge($context['cleanBodyParameters'], $rulesToApply['body'] ?? []); |
|
42 | + | $queryParameters = array_merge($context['cleanQueryParameters'], $rulesToApply['query'] ?? []); |
|
43 | + | $request = $this->prepareRequest($route, $rulesToApply, $bodyParameters, $queryParameters); |
|
37 | 44 | ||
38 | 45 | try { |
|
39 | - | $response = [$this->makeApiCall($request)]; |
|
46 | + | $response = [200 => $this->makeApiCall($request)->getContent()]; |
|
40 | 47 | } catch (\Exception $e) { |
|
41 | 48 | echo 'Exception thrown during response call for ['.implode(',', $route->methods)."] {$route->uri}.\n"; |
|
42 | 49 | if (Flags::$shouldBeVerbose) { |
@@ -81,10 +88,6 @@
Loading
81 | 88 | $request = Request::create($uri, $method, [], $cookies, [], $this->transformHeadersToServerVars($rulesToApply['headers'] ?? [])); |
|
82 | 89 | $request = $this->addHeaders($request, $route, $rulesToApply['headers'] ?? []); |
|
83 | 90 | ||
84 | - | // Mix in parsed parameters with manually specified parameters. |
|
85 | - | $queryParams = collect($this->cleanParams($queryParams))->merge($rulesToApply['query'] ?? [])->toArray(); |
|
86 | - | $bodyParams = collect($this->cleanParams($bodyParams))->merge($rulesToApply['body'] ?? [])->toArray(); |
|
87 | - | ||
88 | 91 | $request = $this->addQueryParameters($request, $queryParams); |
|
89 | 92 | $request = $this->addBodyParameters($request, $bodyParams); |
|
90 | 93 |
@@ -295,13 +298,18 @@
Loading
295 | 298 | * |
|
296 | 299 | * @return bool |
|
297 | 300 | */ |
|
298 | - | private function shouldMakeApiCall(Route $route, array $rulesToApply): bool |
|
301 | + | private function shouldMakeApiCall(Route $route, array $rulesToApply, array $context): bool |
|
299 | 302 | { |
|
300 | 303 | $allowedMethods = $rulesToApply['methods'] ?? []; |
|
301 | 304 | if (empty($allowedMethods)) { |
|
302 | 305 | return false; |
|
303 | 306 | } |
|
304 | 307 | ||
308 | + | if (! empty($context['responses'])) { |
|
309 | + | // Don't attempt a response call if there are already responses |
|
310 | + | return false; |
|
311 | + | } |
|
312 | + | ||
305 | 313 | if (is_string($allowedMethods) && $allowedMethods == '*') { |
|
306 | 314 | return true; |
|
307 | 315 | } |
|
308 | 316 | imilarity index 56% |
|
309 | 317 | ename from src/Tools/ResponseStrategies/ResponseFileStrategy.php |
|
310 | 318 | ename to src/Strategies/Responses/UseResponseFileTag.php |
@@ -19,12 +19,14 @@
Loading
19 | 19 | } |
|
20 | 20 | ||
21 | 21 | /** |
|
22 | - | * @param array $action |
|
22 | + | * @param array|Route $routeOrAction |
|
23 | 23 | * |
|
24 | 24 | * @return array|null |
|
25 | 25 | */ |
|
26 | - | public static function getRouteActionUses(array $action) |
|
26 | + | public static function getRouteClassAndMethodNames($routeOrAction) |
|
27 | 27 | { |
|
28 | + | $action = $routeOrAction instanceof Route ? $routeOrAction->getAction() : $routeOrAction; |
|
29 | + | ||
28 | 30 | if ($action['uses'] !== null) { |
|
29 | 31 | if (is_array($action['uses'])) { |
|
30 | 32 | return $action['uses']; |
@@ -0,0 +1,45 @@
Loading
1 | + | <?php |
|
2 | + | ||
3 | + | namespace Mpociot\ApiDoc\Tools\Traits; |
|
4 | + | ||
5 | + | use Mpociot\Reflection\DocBlock\Tag; |
|
6 | + | ||
7 | + | trait DocBlockParamHelpers |
|
8 | + | { |
|
9 | + | use ParamHelpers; |
|
10 | + | ||
11 | + | /** |
|
12 | + | * Allows users to specify that we shouldn't generate an example for the parameter |
|
13 | + | * by writing 'No-example'. |
|
14 | + | * |
|
15 | + | * @param Tag $tag |
|
16 | + | * |
|
17 | + | * @return bool Whether no example should be generated |
|
18 | + | */ |
|
19 | + | protected function shouldExcludeExample(Tag $tag) |
|
20 | + | { |
|
21 | + | return strpos($tag->getContent(), ' No-example') !== false; |
|
22 | + | } |
|
23 | + | ||
24 | + | /** |
|
25 | + | * Allows users to specify an example for the parameter by writing 'Example: the-example', |
|
26 | + | * to be used in example requests and response calls. |
|
27 | + | * |
|
28 | + | * @param string $description |
|
29 | + | * @param string $type The type of the parameter. Used to cast the example provided, if any. |
|
30 | + | * |
|
31 | + | * @return array The description and included example. |
|
32 | + | */ |
|
33 | + | protected function parseParamDescription(string $description, string $type) |
|
34 | + | { |
|
35 | + | $example = null; |
|
36 | + | if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { |
|
37 | + | $description = $content[1]; |
|
38 | + | ||
39 | + | // examples are parsed as strings by default, we need to cast them properly |
|
40 | + | $example = $this->castToType($content[2], $type); |
|
41 | + | } |
|
42 | + | ||
43 | + | return [$description, $example]; |
|
44 | + | } |
|
45 | + | } |
@@ -2,46 +2,91 @@
Loading
2 | 2 | ||
3 | 3 | namespace Mpociot\ApiDoc\Tools\Traits; |
|
4 | 4 | ||
5 | - | use Illuminate\Support\Arr; |
|
6 | - | use Illuminate\Support\Str; |
|
5 | + | use Faker\Factory; |
|
7 | 6 | ||
8 | 7 | trait ParamHelpers |
|
9 | 8 | { |
|
9 | + | protected function generateDummyValue(string $type) |
|
10 | + | { |
|
11 | + | $faker = Factory::create(); |
|
12 | + | if ($this->config->get('faker_seed')) { |
|
13 | + | $faker->seed($this->config->get('faker_seed')); |
|
14 | + | } |
|
15 | + | $fakeFactories = [ |
|
16 | + | 'integer' => function () use ($faker) { |
|
17 | + | return $faker->numberBetween(1, 20); |
|
18 | + | }, |
|
19 | + | 'number' => function () use ($faker) { |
|
20 | + | return $faker->randomFloat(); |
|
21 | + | }, |
|
22 | + | 'float' => function () use ($faker) { |
|
23 | + | return $faker->randomFloat(); |
|
24 | + | }, |
|
25 | + | 'boolean' => function () use ($faker) { |
|
26 | + | return $faker->boolean(); |
|
27 | + | }, |
|
28 | + | 'string' => function () use ($faker) { |
|
29 | + | return $faker->word; |
|
30 | + | }, |
|
31 | + | 'array' => function () { |
|
32 | + | return []; |
|
33 | + | }, |
|
34 | + | 'object' => function () { |
|
35 | + | return new \stdClass; |
|
36 | + | }, |
|
37 | + | ]; |
|
38 | + | ||
39 | + | $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string']; |
|
40 | + | ||
41 | + | return $fakeFactory(); |
|
42 | + | } |
|
43 | + | ||
10 | 44 | /** |
|
11 | - | * Create proper arrays from dot-noted parameter names. Also filter out parameters which were excluded from having examples. |
|
45 | + | * Cast a value from a string to a specified type. |
|
12 | 46 | * |
|
13 | - | * @param array $params |
|
47 | + | * @param string $value |
|
48 | + | * @param string $type |
|
14 | 49 | * |
|
15 | - | * @return array |
|
50 | + | * @return mixed |
|
16 | 51 | */ |
|
17 | - | protected function cleanParams(array $params) |
|
52 | + | protected function castToType(string $value, string $type) |
|
18 | 53 | { |
|
19 | - | $values = []; |
|
20 | - | $params = array_filter($params, function ($details) { |
|
21 | - | return ! is_null($details['value']); |
|
22 | - | }); |
|
54 | + | $casts = [ |
|
55 | + | 'integer' => 'intval', |
|
56 | + | 'number' => 'floatval', |
|
57 | + | 'float' => 'floatval', |
|
58 | + | 'boolean' => 'boolval', |
|
59 | + | ]; |
|
60 | + | ||
61 | + | // First, we handle booleans. We can't use a regular cast, |
|
62 | + | //because PHP considers string 'false' as true. |
|
63 | + | if ($value == 'false' && $type == 'boolean') { |
|
64 | + | return false; |
|
65 | + | } |
|
23 | 66 | ||
24 | - | foreach ($params as $name => $details) { |
|
25 | - | $this->cleanValueFrom($name, $details['value'], $values); |
|
67 | + | if (isset($casts[$type])) { |
|
68 | + | return $casts[$type]($value); |
|
26 | 69 | } |
|
27 | 70 | ||
28 | - | return $values; |
|
71 | + | return $value; |
|
29 | 72 | } |
|
30 | 73 | ||
31 | 74 | /** |
|
32 | - | * Converts dot notation names to arrays and sets the value at the right depth. |
|
75 | + | * Normalizes the stated "type" of a parameter (eg "int", "integer", "double") |
|
76 | + | * to a number of standard types (integer, boolean, float). |
|
33 | 77 | * |
|
34 | - | * @param string $name |
|
35 | - | * @param mixed $value |
|
36 | - | * @param array $values The array that holds the result |
|
78 | + | * @param string $type |
|
37 | 79 | * |
|
38 | - | * @return void |
|
80 | + | * @return mixed|string |
|
39 | 81 | */ |
|
40 | - | protected function cleanValueFrom($name, $value, array &$values = []) |
|
82 | + | protected function normalizeParameterType(string $type) |
|
41 | 83 | { |
|
42 | - | if (Str::contains($name, '[')) { |
|
43 | - | $name = str_replace(['][', '[', ']', '..'], ['.', '.', '', '.*.'], $name); |
|
44 | - | } |
|
45 | - | Arr::set($values, str_replace('.*', '.0', $name), $value); |
|
84 | + | $typeMap = [ |
|
85 | + | 'int' => 'integer', |
|
86 | + | 'bool' => 'boolean', |
|
87 | + | 'double' => 'float', |
|
88 | + | ]; |
|
89 | + | ||
90 | + | return $type ? ($typeMap[$type] ?? $type) : 'string'; |
|
46 | 91 | } |
|
47 | 92 | } |
@@ -0,0 +1,40 @@
Loading
1 | + | <?php |
|
2 | + | ||
3 | + | namespace Mpociot\ApiDoc\Strategies; |
|
4 | + | ||
5 | + | use ReflectionClass; |
|
6 | + | use ReflectionMethod; |
|
7 | + | use Illuminate\Routing\Route; |
|
8 | + | use Mpociot\ApiDoc\Tools\DocumentationConfig; |
|
9 | + | ||
10 | + | abstract class Strategy |
|
11 | + | { |
|
12 | + | /** |
|
13 | + | * @var DocumentationConfig The apidoc config |
|
14 | + | */ |
|
15 | + | protected $config; |
|
16 | + | ||
17 | + | /** |
|
18 | + | * @var string The current stage of route processing |
|
19 | + | */ |
|
20 | + | protected $stage; |
|
21 | + | ||
22 | + | public function __construct(string $stage, DocumentationConfig $config) |
|
23 | + | { |
|
24 | + | $this->stage = $stage; |
|
25 | + | $this->config = $config; |
|
26 | + | } |
|
27 | + | ||
28 | + | /** |
|
29 | + | * @param Route $route |
|
30 | + | * @param ReflectionClass $controller |
|
31 | + | * @param ReflectionMethod $method |
|
32 | + | * @param array $routeRules Array of rules for the ruleset which this route belongs to. |
|
33 | + | * @param array $context Results from the previous stages |
|
34 | + | * |
|
35 | + | * @throws \Exception |
|
36 | + | * |
|
37 | + | * @return array |
|
38 | + | */ |
|
39 | + | abstract public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []); |
|
40 | + | } |
@@ -0,0 +1,67 @@
Loading
1 | + | <?php |
|
2 | + | ||
3 | + | namespace Mpociot\ApiDoc\Strategies\Responses; |
|
4 | + | ||
5 | + | use Illuminate\Routing\Route; |
|
6 | + | use Mpociot\Reflection\DocBlock; |
|
7 | + | use Mpociot\Reflection\DocBlock\Tag; |
|
8 | + | use Mpociot\ApiDoc\Strategies\Strategy; |
|
9 | + | use Mpociot\ApiDoc\Tools\RouteDocBlocker; |
|
10 | + | ||
11 | + | /** |
|
12 | + | * Get a response from the docblock ( @response ). |
|
13 | + | */ |
|
14 | + | class UseResponseTag extends Strategy |
|
15 | + | { |
|
16 | + | /** |
|
17 | + | * @param Route $route |
|
18 | + | * @param \ReflectionClass $controller |
|
19 | + | * @param \ReflectionMethod $method |
|
20 | + | * @param array $routeRules |
|
21 | + | * @param array $context |
|
22 | + | * |
|
23 | + | * @throws \Exception |
|
24 | + | * |
|
25 | + | * @return array|null |
|
26 | + | */ |
|
27 | + | public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = []) |
|
28 | + | { |
|
29 | + | $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); |
|
30 | + | /** @var DocBlock $methodDocBlock */ |
|
31 | + | $methodDocBlock = $docBlocks['method']; |
|
32 | + | ||
33 | + | return $this->getDocBlockResponses($methodDocBlock->getTags()); |
|
34 | + | } |
|
35 | + | ||
36 | + | /** |
|
37 | + | * Get the response from the docblock if available. |
|
38 | + | * |
|
39 | + | * @param array $tags |
|
40 | + | * |
|
41 | + | * @return array|null |
|
42 | + | */ |
|
43 | + | protected function getDocBlockResponses(array $tags) |
|
44 | + | { |
|
45 | + | $responseTags = array_values( |
|
46 | + | array_filter($tags, function ($tag) { |
|
47 | + | return $tag instanceof Tag && strtolower($tag->getName()) === 'response'; |
|
48 | + | }) |
|
49 | + | ); |
|
50 | + | ||
51 | + | if (empty($responseTags)) { |
|
52 | + | return null; |
|
53 | + | } |
|
54 | + | ||
55 | + | $responses = array_map(function (Tag $responseTag) { |
|
56 | + | preg_match('/^(\d{3})?\s?([\s\S]*)$/', $responseTag->getContent(), $result); |
|
57 | + | ||
58 | + | $status = $result[1] ?: 200; |
|
59 | + | $content = $result[2] ?: '{}'; |
|
60 | + | ||
61 | + | return [$content, (int) $status]; |
|
62 | + | }, $responseTags); |
|
63 | + | ||
64 | + | // Convert responses to [200 => 'response', 401 => 'response'] |
|
65 | + | return collect($responses)->pluck('0', '1')->toArray(); |
|
66 | + | } |
|
67 | + | } |
|
0 | 68 | imilarity index 83% |
|
1 | 69 | ename from src/Tools/ResponseStrategies/TransformerTagsStrategy.php |
|
2 | 70 | ename to src/Strategies/Responses/UseTransformerTags.php |
@@ -0,0 +1,92 @@
Loading
1 | + | <?php |
|
2 | + | ||
3 | + | namespace Mpociot\ApiDoc\Strategies\QueryParameters; |
|
4 | + | ||
5 | + | use ReflectionClass; |
|
6 | + | use ReflectionMethod; |
|
7 | + | use Illuminate\Support\Str; |
|
8 | + | use Illuminate\Routing\Route; |
|
9 | + | use Mpociot\Reflection\DocBlock; |
|
10 | + | use Mpociot\Reflection\DocBlock\Tag; |
|
11 | + | use Mpociot\ApiDoc\Strategies\Strategy; |
|
12 | + | use Mpociot\ApiDoc\Tools\RouteDocBlocker; |
|
13 | + | use Dingo\Api\Http\FormRequest as DingoFormRequest; |
|
14 | + | use Mpociot\ApiDoc\Tools\Traits\DocBlockParamHelpers; |
|
15 | + | use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; |
|
16 | + | ||
17 | + | class GetFromQueryParamTag extends Strategy |
|
18 | + | { |
|
19 | + | use DocBlockParamHelpers; |
|
20 | + | ||
21 | + | public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) |
|
22 | + | { |
|
23 | + | foreach ($method->getParameters() as $param) { |
|
24 | + | $paramType = $param->getType(); |
|
25 | + | if ($paramType === null) { |
|
26 | + | continue; |
|
27 | + | } |
|
28 | + | ||
29 | + | $parameterClassName = version_compare(phpversion(), '7.1.0', '<') |
|
30 | + | ? $paramType->__toString() |
|
31 | + | : $paramType->getName(); |
|
32 | + | ||
33 | + | try { |
|
34 | + | $parameterClass = new ReflectionClass($parameterClassName); |
|
35 | + | } catch (\ReflectionException $e) { |
|
36 | + | continue; |
|
37 | + | } |
|
38 | + | ||
39 | + | // If there's a FormRequest, we check there for @queryParam tags. |
|
40 | + | if (class_exists(LaravelFormRequest::class) && $parameterClass->isSubclassOf(LaravelFormRequest::class) |
|
41 | + | || class_exists(DingoFormRequest::class) && $parameterClass->isSubclassOf(DingoFormRequest::class)) { |
|
42 | + | $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); |
|
43 | + | $queryParametersFromDocBlock = $this->getqueryParametersFromDocBlock($formRequestDocBlock->getTags()); |
|
44 | + | ||
45 | + | if (count($queryParametersFromDocBlock)) { |
|
46 | + | return $queryParametersFromDocBlock; |
|
47 | + | } |
|
48 | + | } |
|
49 | + | } |
|
50 | + | ||
51 | + | $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method']; |
|
52 | + | ||
53 | + | return $this->getqueryParametersFromDocBlock($methodDocBlock->getTags()); |
|
54 | + | } |
|
55 | + | ||
56 | + | private function getQueryParametersFromDocBlock($tags) |
|
57 | + | { |
|
58 | + | $parameters = collect($tags) |
|
59 | + | ->filter(function ($tag) { |
|
60 | + | return $tag instanceof Tag && $tag->getName() === 'queryParam'; |
|
61 | + | }) |
|
62 | + | ->mapWithKeys(function ($tag) { |
|
63 | + | preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); |
|
64 | + | $content = preg_replace('/\s?No-example.?/', '', $content); |
|
65 | + | if (empty($content)) { |
|
66 | + | // this means only name was supplied |
|
67 | + | list($name) = preg_split('/\s+/', $tag->getContent()); |
|
68 | + | $required = false; |
|
69 | + | $description = ''; |
|
70 | + | } else { |
|
71 | + | list($_, $name, $required, $description) = $content; |
|
72 | + | $description = trim($description); |
|
73 | + | if ($description == 'required' && empty(trim($required))) { |
|
74 | + | $required = $description; |
|
75 | + | $description = ''; |
|
76 | + | } |
|
77 | + | $required = trim($required) == 'required' ? true : false; |
|
78 | + | } |
|
79 | + | ||
80 | + | list($description, $value) = $this->parseParamDescription($description, 'string'); |
|
81 | + | if (is_null($value) && ! $this->shouldExcludeExample($tag)) { |
|
82 | + | $value = Str::contains($description, ['number', 'count', 'page']) |
|
83 | + | ? $this->generateDummyValue('integer') |
|
84 | + | : $this->generateDummyValue('string'); |
|
85 | + | } |
|
86 | + | ||
87 | + | return [$name => compact('description', 'required', 'value')]; |
|
88 | + | })->toArray(); |
|
89 | + | ||
90 | + | return $parameters; |
|
91 | + | } |
|
92 | + | } |
|
0 | 93 | imilarity index 88% |
|
1 | 94 | ename from src/Tools/ResponseStrategies/ResponseCallStrategy.php |
|
2 | 95 | ename to src/Strategies/Responses/ResponseCalls.php |
@@ -1,6 +1,6 @@
Loading
1 | 1 | <?php |
|
2 | 2 | ||
3 | - | namespace Mpociot\ApiDoc\Tools\ResponseStrategies; |
|
3 | + | namespace Mpociot\ApiDoc\Strategies\Responses; |
|
4 | 4 | ||
5 | 5 | use ReflectionClass; |
|
6 | 6 | use ReflectionMethod; |
@@ -8,25 +8,36 @@
Loading
8 | 8 | use League\Fractal\Manager; |
|
9 | 9 | use Illuminate\Routing\Route; |
|
10 | 10 | use Mpociot\ApiDoc\Tools\Flags; |
|
11 | + | use Mpociot\Reflection\DocBlock; |
|
11 | 12 | use League\Fractal\Resource\Item; |
|
12 | 13 | use Mpociot\Reflection\DocBlock\Tag; |
|
13 | 14 | use League\Fractal\Resource\Collection; |
|
15 | + | use Mpociot\ApiDoc\Strategies\Strategy; |
|
16 | + | use Mpociot\ApiDoc\Tools\RouteDocBlocker; |
|
14 | 17 | ||
15 | 18 | /** |
|
16 | 19 | * Parse a transformer response from the docblock ( @transformer || @transformercollection ). |
|
17 | 20 | */ |
|
18 | - | class TransformerTagsStrategy |
|
21 | + | class UseTransformerTags extends Strategy |
|
19 | 22 | { |
|
20 | 23 | /** |
|
21 | 24 | * @param Route $route |
|
22 | - | * @param array $tags |
|
23 | - | * @param array $routeProps |
|
25 | + | * @param ReflectionClass $controller |
|
26 | + | * @param ReflectionMethod $method |
|
27 | + | * @param array $rulesToApply |
|
28 | + | * @param array $context |
|
29 | + | * |
|
30 | + | * @throws \Exception |
|
24 | 31 | * |
|
25 | 32 | * @return array|null |
|
26 | 33 | */ |
|
27 | - | public function __invoke(Route $route, array $tags, array $routeProps) |
|
34 | + | public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $rulesToApply, array $context = []) |
|
28 | 35 | { |
|
29 | - | return $this->getTransformerResponse($tags); |
|
36 | + | $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); |
|
37 | + | /** @var DocBlock $methodDocBlock */ |
|
38 | + | $methodDocBlock = $docBlocks['method']; |
|
39 | + | ||
40 | + | return $this->getTransformerResponse($methodDocBlock->getTags()); |
|
30 | 41 | } |
|
31 | 42 | ||
32 | 43 | /** |
@@ -57,7 +68,7 @@
Loading
57 | 68 | ? new Collection([$modelInstance, $modelInstance], new $transformer) |
|
58 | 69 | : new Item($modelInstance, new $transformer); |
|
59 | 70 | ||
60 | - | return [response($fractal->createData($resource)->toJson())]; |
|
71 | + | return [200 => response($fractal->createData($resource)->toJson())->getContent()]; |
|
61 | 72 | } catch (\Exception $e) { |
|
62 | 73 | return null; |
|
63 | 74 | } |
Files | Complexity | Coverage |
---|---|---|
src | 315 | 90.86% |
Project Totals (19 files) | 315 | 90.86% |
1052.5
TRAVIS_PHP_VERSION=7.1.3 TRAVIS_OS_NAME=linux
1052.4
TRAVIS_PHP_VERSION=7.2.0 TRAVIS_OS_NAME=linux
1052.3
TRAVIS_PHP_VERSION=7.0.0 TRAVIS_OS_NAME=linux
1052.1
TRAVIS_PHP_VERSION=7.2.0 TRAVIS_OS_NAME=linux
1052.6
TRAVIS_PHP_VERSION=7.0.0 TRAVIS_OS_NAME=linux
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file.
The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files.
The size and color of each slice is representing the number of statements and the coverage, respectively.