Completed
Pull Request — master (#570)
by Marcel
01:55
created

Generator::fetchBodyParameters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 5
1
<?php
2
3
namespace Mpociot\ApiDoc\Tools;
4
5
use ReflectionClass;
6
use ReflectionMethod;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Str;
9
use Illuminate\Routing\Route;
10
11
class Generator
12
{
13
    /**
14
     * @var DocumentationConfig
15
     */
16
    private $config;
17
18
    public function __construct(DocumentationConfig $config = null)
19
    {
20
        // If no config is injected, pull from global
21
        $this->config = $config ?: new DocumentationConfig(config('apidoc'));
22
    }
23
24
    /**
25
     * @param Route $route
26
     *
27
     * @return mixed
28
     */
29
    public function getUri(Route $route)
30
    {
31
        return $route->uri();
32
    }
33
34
    /**
35
     * @param Route $route
36
     *
37
     * @return mixed
38
     */
39
    public function getMethods(Route $route)
40
    {
41
        return array_diff($route->methods(), ['HEAD']);
42
    }
43
44
    /**
45
     * @param \Illuminate\Routing\Route $route
46
     * @param array $rulesToApply Rules to apply when generating documentation for this route
47
     *
48
     * @return array
49
     */
50
    public function processRoute(Route $route, array $rulesToApply = [])
51
    {
52
        list($controllerName, $methodName) = Utils::getRouteClassAndMethodNames($route->getAction());
53
        $controller = new ReflectionClass($controllerName);
54
        $method = $controller->getMethod($methodName);
55
56
        $parsedRoute = [
57
            'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
58
            'methods' => $this->getMethods($route),
59
            'uri' => $this->getUri($route),
60
            'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])),
61
        ];
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);
67
68
        $queryParameters = $this->fetchQueryParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
69
        $parsedRoute['queryParameters'] = $queryParameters;
70
        $parsedRoute['cleanQueryParameters'] = $this->cleanParams($queryParameters);
71
72
        $responses = $this->fetchResponses($controller, $method, $route, $rulesToApply, $parsedRoute);
73
        $parsedRoute['response'] = $responses;
74
        $parsedRoute['showresponse'] = ! empty($responses);
75
76
        $parsedRoute['headers'] = $rulesToApply['headers'] ?? [];
77
78
        $parsedRoute += $metadata;
79
80
        return $parsedRoute;
81
    }
82
83
    protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
84
    {
85
        $context['metadata'] = [
86
            'groupName' => $this->config->get('default_group', ''),
87
            'groupDescription' => '',
88
            'title' => '',
89
            'description' => '',
90
            'authenticated' => false,
91
        ];
92
93
        return $this->iterateThroughStrategies('metadata', $context, [$route, $controller, $method, $rulesToApply]);
94
    }
95
96
    protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
97
    {
98
        return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]);
99
    }
100
101
    protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
102
    {
103
        return $this->iterateThroughStrategies('queryParameters', $context, [$route, $controller, $method, $rulesToApply]);
104
    }
105
106
    protected function fetchResponses(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
107
    {
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
        }
117
118
        return null;
119
    }
120
121
    protected function iterateThroughStrategies(string $stage, array $context, array $arguments)
122
    {
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
134
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;
140
                    }
141
                }
142
            }
143
        }
144
145
        return $context[$stage];
146
    }
147
148
    /**
149
     * Create samples at index 0 for array parameters.
150
     * Also filter out parameters which were excluded from having examples.
151
     *
152
     * @param array $params
153
     *
154
     * @return array
155
     */
156
    protected function cleanParams(array $params)
157
    {
158
        $values = [];
159
160
        // Remove params which have no examples.
161
        $params = array_filter($params, function ($details) {
162
            return ! is_null($details['value']);
163
        });
164
165
        foreach ($params as $paramName => $details) {
166
            $this->generateConcreteSampleForArrayKeys(
167
                $paramName, $details['value'], $values
168
            );
169
        }
170
171
        return $values;
172
    }
173
174
    /**
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.
177
     *
178
     * @param string $paramName
179
     * @param mixed $paramExample
180
     * @param array $values The array that holds the result
181
     *
182
     * @return void
183
     */
184
    protected function generateConcreteSampleForArrayKeys($paramName, $paramExample, array &$values = [])
185
    {
186
        if (Str::contains($paramName, '[')) {
187
            // Replace usages of [] with dot notation
188
            $paramName = str_replace(['][', '[', ']', '..'], ['.', '.', '', '.*.'], $paramName);
189
        }
190
        // Then generate a sample item for the dot notation
191
        Arr::set($values, str_replace('.*', '.0', $paramName), $paramExample);
192
    }
193
}
194