Completed
Pull Request — master (#570)
by Marcel
02:28 queued 01:11
created

Generator::iterateThroughStrategies()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.8817
c 0
b 0
f 0
cc 6
nc 5
nop 3
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
        // Currently too lazy to tinker with Blade files; change this later
79
        unset($parsedRoute['metadata']);
80
        $parsedRoute += $metadata;
81
82
        return $parsedRoute;
83
    }
84
85
    protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
86
    {
87
        $context['metadata'] = [
88
            'groupName' => $this->config->get('default_group'),
89
            'groupDescription' => '',
90
            'title' => '',
91
            'description' => '',
92
            'authenticated' => false,
93
        ];
94
95
        return $this->iterateThroughStrategies('metadata', $context, [$route, $controller, $method, $rulesToApply]);
96
    }
97
98
    protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
99
    {
100
        return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]);
101
    }
102
103
    protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
104
    {
105
        return $this->iterateThroughStrategies('queryParameters', $context, [$route, $controller, $method, $rulesToApply]);
106
    }
107
108
    protected function fetchResponses(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
109
    {
110
        $responses = $this->iterateThroughStrategies('responses', $context, [$route, $controller, $method, $rulesToApply]);
111
        if (count($responses)) {
112
            return collect($responses)->map(function (string $response, int $status) {
113
                return [
114
                    'status' => $status ?: 200,
115
                    'content' => $response,
116
                ];
117
            })->values()->toArray();
118
        }
119
120
        return null;
121
    }
122
123
    protected function iterateThroughStrategies(string $key, array $context, array $arguments)
124
    {
125
        $strategies = $this->config->get("strategies.$key", []);
126
        $context[$key] = $context[$key] ?? [];
127
        foreach ($strategies as $strategyClass) {
128
            $strategy = new $strategyClass($this->config);
129
            $arguments[] = $context;
130
            $results = $strategy(...$arguments);
131
            if (! is_null($results)) {
132
                foreach ($results as $index => $item) {
133
                    // Using a for loop rather than array_merge or +=
134
                    // so it does not renumber numeric keys
135
                    // and also allows values to be overwritten
136
137
                    // Don't allow overwriting if an empty value is trying to replace a set one
138
                    if (! in_array($context[$key], [null, ''], true) && in_array($item, [null, ''], true)) {
139
                        continue;
140
                    } else {
141
                        $context[$key][$index] = $item;
142
                    }
143
                }
144
            }
145
        }
146
147
        return $context[$key];
148
    }
149
150
    /**
151
     * Create samples at index 0 for array parameters.
152
     * Also filter out parameters which were excluded from having examples.
153
     *
154
     * @param array $params
155
     *
156
     * @return array
157
     */
158
    protected function cleanParams(array $params)
159
    {
160
        $values = [];
161
162
        // Remove params which have no examples.
163
        $params = array_filter($params, function ($details) {
164
            return ! is_null($details['value']);
165
        });
166
167
        foreach ($params as $paramName => $details) {
168
            $this->generateConcreteSampleForArrayKeys(
169
                $paramName, $details['value'], $values
170
            );
171
        }
172
173
        return $values;
174
    }
175
176
    /**
177
     * For each array notation parameter (eg user.*, item.*.name, object.*.*, user[])
178
     * generate concrete sample (user.0, item.0.name, object.0.0, user.0) with example as value.
179
     *
180
     * @param string $paramName
181
     * @param mixed $paramExample
182
     * @param array $values The array that holds the result
183
     *
184
     * @return void
185
     */
186
    protected function generateConcreteSampleForArrayKeys($paramName, $paramExample, array &$values = [])
187
    {
188
        if (Str::contains($paramName, '[')) {
189
            // Replace usages of [] with dot notation
190
            $paramName = str_replace(['][', '[', ']', '..'], ['.', '.', '', '.*.'], $paramName);
191
        }
192
        // Then generate a sample item for the dot notation
193
        Arr::set($values, str_replace('.*', '.0', $paramName), $paramExample);
194
    }
195
}
196