mpociot / laravel-apidoc-generator
Showing 14 of 29 files from the diff.

@@ -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%
Notifications are pending CI completion. Periodically Codecov will check the CI state, when complete notifications will be submitted. Push notifications now.
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

No yaml found.

Create your codecov.yml to customize your Codecov experience

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.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading