Completed
Pull Request — master (#327)
by
unknown
01:50
created

GenerateDocumentation::getBindings()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
cc 3
nc 3
nop 0
1
<?php
2
3
namespace Mpociot\ApiDoc\Commands;
4
5
use ReflectionClass;
6
use Illuminate\Routing\Route;
7
use Illuminate\Console\Command;
8
use Mpociot\Reflection\DocBlock;
9
use Illuminate\Support\Collection;
10
use Mpociot\Documentarian\Documentarian;
11
use Mpociot\ApiDoc\Postman\CollectionWriter;
12
use Mpociot\ApiDoc\Generators\DingoGenerator;
13
use Mpociot\ApiDoc\Generators\LaravelGenerator;
14
use Mpociot\ApiDoc\Generators\AbstractGenerator;
15
use Illuminate\Support\Facades\Route as RouteFacade;
16
17
class GenerateDocumentation extends Command
18
{
19
    /**
20
     * The name and signature of the console command.
21
     *
22
     * @var string
23
     */
24
    protected $signature = 'api:generate
25
                            {--output=public/docs : The output path for the generated documentation}
26
                            {--routeDomain= : The route domain (or domains) to use for generation}
27
                            {--routePrefix= : The route prefix (or prefixes) to use for generation}
28
                            {--routes=* : The route names to use for generation}
29
                            {--middleware= : The middleware to use for generation}
30
                            {--noResponseCalls : Disable API response calls}
31
                            {--noPostmanCollection : Disable Postman collection creation}
32
                            {--useMiddlewares : Use all configured route middlewares}
33
                            {--authProvider=users : The authentication provider to use for API response calls}
34
                            {--authGuard=web : The authentication guard to use for API response calls}
35
                            {--actAsUserId= : The user ID to use for API response calls}
36
                            {--router=laravel : The router to be used (Laravel or Dingo)}
37
                            {--force : Force rewriting of existing routes}
38
                            {--bindings= : Route Model Bindings}
39
                            {--header=* : Custom HTTP headers to add to the example requests. Separate the header name and value with ":"}
40
    ';
41
42
    /**
43
     * The console command description.
44
     *
45
     * @var string
46
     */
47
    protected $description = 'Generate your API documentation from existing Laravel routes.';
48
49
    /**
50
     * Create a new command instance.
51
     *
52
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
53
     */
54
    public function __construct()
55
    {
56
        parent::__construct();
57
    }
58
59
    /**
60
     * Execute the console command.
61
     *
62
     * @return false|null
63
     */
64
    public function handle()
65
    {
66
        if ($this->option('router') === 'laravel') {
67
            $generator = new LaravelGenerator();
68
        } else {
69
            $generator = new DingoGenerator();
70
        }
71
72
        $allowedRoutes = $this->option('routes');
73
        $routeDomain = $this->option('routeDomain');
74
        $routePrefix = $this->option('routePrefix');
75
        $middleware = $this->option('middleware');
76
77
        $this->setUserToBeImpersonated($this->option('actAsUserId'));
78
79
        if ($routePrefix === null && $routeDomain === null && ! count($allowedRoutes) && $middleware === null) {
80
            $this->error('You must provide either a route prefix, a route domain, a route or a middleware to generate the documentation.');
81
82
            return false;
83
        }
84
85
        $generator->prepareMiddleware($this->option('useMiddlewares'));
0 ignored issues
show
Documentation introduced by
$this->option('useMiddlewares') is of type string|array, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
86
87
        $routePrefixes = explode(',', $routePrefix ?: '*');
88
        $routeDomains = explode(',', $routeDomain ?: '*');
89
90
        $parsedRoutes = [];
91
92
        foreach ($routeDomains as $routeDomain) {
93
            foreach ($routePrefixes as $routePrefix) {
94
                $parsedRoutes += $this->processRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware);
0 ignored issues
show
Bug introduced by
It seems like $allowedRoutes defined by $this->option('routes') on line 72 can also be of type string; however, Mpociot\ApiDoc\Commands\...tation::processRoutes() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
95
            }
96
        }
97
        $parsedRoutes = collect($parsedRoutes)->groupBy('resource')->sort(function ($a, $b) {
98
            return strcmp($a->first()['resource'], $b->first()['resource']);
99
        });
100
101
        $this->writeMarkdown($parsedRoutes);
102
    }
103
104
    /**
105
     * @param  Collection $parsedRoutes
106
     *
107
     * @return void
108
     */
109
    private function writeMarkdown($parsedRoutes)
110
    {
111
        $outputPath = $this->option('output');
112
        $targetFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'index.md';
113
        $compareFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'.compare.md';
114
115
        $infoText = view('apidoc::partials.info')
116
            ->with('outputPath', ltrim($outputPath, 'public/'))
117
            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'));
118
119
        $parsedRouteOutput = $parsedRoutes->map(function ($routeGroup) {
120
            return $routeGroup->map(function ($route) {
121
                $route['output'] = (string) view('apidoc::partials.route')->with('parsedRoute', $route)->render();
122
123
                return $route;
124
            });
125
        });
126
127
        $frontmatter = view('apidoc::partials.frontmatter');
128
        /*
129
         * In case the target file already exists, we should check if the documentation was modified
130
         * and skip the modified parts of the routes.
131
         */
132
        if (file_exists($targetFile) && file_exists($compareFile)) {
133
            $generatedDocumentation = file_get_contents($targetFile);
134
            $compareDocumentation = file_get_contents($compareFile);
135
136
            if (preg_match('/---(.*)---\\s<!-- START_INFO -->/is', $generatedDocumentation, $generatedFrontmatter)) {
137
                $frontmatter = trim($generatedFrontmatter[1], "\n");
138
            }
139
140
            $parsedRouteOutput->transform(function ($routeGroup) use ($generatedDocumentation, $compareDocumentation) {
141
                return $routeGroup->transform(function ($route) use ($generatedDocumentation, $compareDocumentation) {
142
                    if (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $generatedDocumentation, $routeMatch)) {
143
                        $routeDocumentationChanged = (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $compareDocumentation, $compareMatch) && $compareMatch[1] !== $routeMatch[1]);
144
                        if ($routeDocumentationChanged === false || $this->option('force')) {
145
                            if ($routeDocumentationChanged) {
146
                                $this->warn('Discarded manual changes for route ['.implode(',', $route['methods']).'] '.$route['uri']);
147
                            }
148
                        } else {
149
                            $this->warn('Skipping modified route ['.implode(',', $route['methods']).'] '.$route['uri']);
150
                            $route['modified_output'] = $routeMatch[0];
151
                        }
152
                    }
153
154
                    return $route;
155
                });
156
            });
157
        }
158
159
        $documentarian = new Documentarian();
160
161
        $markdown = view('apidoc::documentarian')
162
            ->with('writeCompareFile', false)
163
            ->with('frontmatter', $frontmatter)
164
            ->with('infoText', $infoText)
165
            ->with('outputPath', $this->option('output'))
166
            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'))
167
            ->with('parsedRoutes', $parsedRouteOutput);
168
169
        if (! is_dir($outputPath)) {
170
            $documentarian->create($outputPath);
171
        }
172
173
        // Write output file
174
        file_put_contents($targetFile, $markdown);
175
176
        // Write comparable markdown file
177
        $compareMarkdown = view('apidoc::documentarian')
178
            ->with('writeCompareFile', true)
179
            ->with('frontmatter', $frontmatter)
180
            ->with('infoText', $infoText)
181
            ->with('outputPath', $this->option('output'))
182
            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'))
183
            ->with('parsedRoutes', $parsedRouteOutput);
184
185
        file_put_contents($compareFile, $compareMarkdown);
186
187
        $this->info('Wrote index.md to: '.$outputPath);
188
189
        $this->info('Generating API HTML code');
190
191
        $documentarian->generate($outputPath);
192
193
        $this->info('Wrote HTML documentation to: '.$outputPath.'/index.html');
194
195
        if ($this->option('noPostmanCollection') !== true) {
196
            $this->info('Generating Postman collection');
197
198
            file_put_contents($outputPath.DIRECTORY_SEPARATOR.'collection.json', $this->generatePostmanCollection($parsedRoutes));
199
        }
200
    }
201
202
    /**
203
     * @return array
204
     */
205
    private function getBindings()
206
    {
207
        $bindings = $this->option('bindings');
208
        if (empty($bindings)) {
209
            return [];
210
        }
211
212
        $bindings = explode('|', $bindings);
213
        $resultBindings = [];
214
        foreach ($bindings as $binding) {
215
            list($name, $id) = explode(',', $binding);
216
            $resultBindings[$name] = $id;
217
        }
218
219
        return $resultBindings;
220
    }
221
222
    /**
223
     * @param $actAs
224
     */
225
    private function setUserToBeImpersonated($actAs)
226
    {
227
        if (! empty($actAs)) {
228
            if (version_compare($this->laravel->version(), '5.2.0', '<')) {
229
                $userModel = config('auth.model');
230
                $user = $userModel::find($actAs);
231
                $this->laravel['auth']->setUser($user);
232
            } else {
233
                $provider = $this->option('authProvider');
234
                $userModel = config("auth.providers.$provider.model");
235
                $user = $userModel::find($actAs);
236
                $this->laravel['auth']->guard($this->option('authGuard'))->setUser($user);
237
            }
238
        }
239
    }
240
241
    /**
242
     * @return mixed
243
     */
244
    private function getRoutes()
245
    {
246
        if ($this->option('router') === 'laravel') {
247
            return RouteFacade::getRoutes();
248
        } else {
249
            return app('Dingo\Api\Routing\Router')->getRoutes();
250
        }
251
    }
252
253
    /**
254
     * @param AbstractGenerator  $generator
255
     * @param $allowedRoutes
256
     * @param $routeDomain
257
     * @param $routePrefix
258
     *
259
     * @return array
260
     */
261
    private function processRoutes(AbstractGenerator $generator, array $allowedRoutes, $routeDomain, $routePrefix, $middleware)
262
    {
263
        $withResponse = $this->option('noResponseCalls') == false;
264
        $routes = $this->getRoutes();
265
        $bindings = $this->getBindings();
266
        $parsedRoutes = [];
267
        foreach ($routes as $route) {
268
            /** @var Route $route */
269
            if (in_array($route->getName(), $allowedRoutes)
270
                || (str_is($routeDomain, $generator->getDomain($route))
271
                    && str_is($routePrefix, $generator->getUri($route)))
272
                || in_array($middleware, $route->middleware())
273
               ) {
274
                if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
275
                    $parsedRoutes[] = $generator->processRoute($route, $bindings, $this->option('header'), $withResponse && in_array('GET', $generator->getMethods($route)));
0 ignored issues
show
Documentation introduced by
$this->option('header') is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
276
                    $this->info('Processed route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
277
                } else {
278
                    $this->warn('Skipping route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
279
                }
280
            }
281
        }
282
283
        return $parsedRoutes;
284
    }
285
286
    /**
287
     * @param $route
288
     *
289
     * @return bool
290
     */
291
    private function isValidRoute(Route $route)
292
    {
293
        return ! is_callable($route->getAction()['uses']) && ! is_null($route->getAction()['uses']);
294
    }
295
296
    /**
297
     * @param $route
298
     *
299
     * @return bool
300
     */
301
    private function isRouteVisibleForDocumentation($route)
302
    {
303
        list($class, $method) = explode('@', $route);
304
        $reflection = new ReflectionClass($class);
305
        $comment = $reflection->getMethod($method)->getDocComment();
306
        if ($comment) {
307
            $phpdoc = new DocBlock($comment);
308
309
            return collect($phpdoc->getTags())
310
                ->filter(function ($tag) use ($route) {
311
                    return $tag->getName() === 'hideFromAPIDocumentation';
312
                })
313
                ->isEmpty();
314
        }
315
316
        return true;
317
    }
318
319
    /**
320
     * Generate Postman collection JSON file.
321
     *
322
     * @param Collection $routes
323
     *
324
     * @return string
325
     */
326
    private function generatePostmanCollection(Collection $routes)
327
    {
328
        $writer = new CollectionWriter($routes);
329
330
        return $writer->getCollection();
331
    }
332
}
333