Completed
Push — master ( 138e24...047e0d )
by Marcel
06:22
created

GenerateDocumentation::getRoutes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 8
rs 9.4285
cc 2
eloc 5
nc 2
nop 0
1
<?php
2
3
namespace Mpociot\ApiDoc\Commands;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Support\Collection;
7
use Illuminate\Support\Facades\Route;
8
use Mpociot\ApiDoc\Generators\AbstractGenerator;
9
use Mpociot\ApiDoc\Generators\DingoGenerator;
10
use Mpociot\ApiDoc\Generators\LaravelGenerator;
11
use Mpociot\ApiDoc\Postman\CollectionWriter;
12
use Mpociot\Documentarian\Documentarian;
13
14
class GenerateDocumentation extends Command
15
{
16
    /**
17
     * The name and signature of the console command.
18
     *
19
     * @var string
20
     */
21
    protected $signature = 'api:generate 
22
                            {--output=public/docs : The output path for the generated documentation}
23
                            {--routePrefix= : The route prefix to use for generation}
24
                            {--routes=* : The route names to use for generation}
25
                            {--noResponseCalls : Disable API response calls}
26
                            {--noPostmanCollection : Disable Postman collection creation}
27
                            {--actAsUserId= : The user ID to use for API response calls}
28
                            {--router=laravel : The router to be used (Laravel or Dingo)}
29
                            {--bindings= : Route Model Bindings}
30
    ';
31
32
    /**
33
     * The console command description.
34
     *
35
     * @var string
36
     */
37
    protected $description = 'Generate your API documentation from existing Laravel routes.';
38
39
    /**
40
     * Create a new command instance.
41
     *
42
     * @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...
43
     */
44
    public function __construct()
45
    {
46
        parent::__construct();
47
    }
48
49
    /**
50
     * Execute the console command.
51
     *
52
     * @return false|null
53
     */
54
    public function handle()
55
    {
56
        if ($this->option('router') === 'laravel') {
57
            $generator = new LaravelGenerator();
58
        } else {
59
            $generator = new DingoGenerator();
60
        }
61
62
        $allowedRoutes = $this->option('routes');
63
        $routePrefix = $this->option('routePrefix');
64
65
        $this->setUserToBeImpersonated($this->option('actAsUserId'));
66
67
        if ($routePrefix === null && ! count($allowedRoutes)) {
68
            $this->error('You must provide either a route prefix or a route to generate the documentation.');
69
70
            return false;
71
        }
72
73
        if ($this->option('router') === 'laravel') {
74
            $parsedRoutes = $this->processLaravelRoutes($generator, $allowedRoutes, $routePrefix);
75
        } else {
76
            $parsedRoutes = $this->processDingoRoutes($generator, $allowedRoutes, $routePrefix);
77
        }
78
        $parsedRoutes = collect($parsedRoutes)->groupBy('resource')->sortBy('resource');
79
80
        $this->writeMarkdown($parsedRoutes);
81
    }
82
83
    /**
84
     * @param  Collection $parsedRoutes
85
     *
86
     * @return void
87
     */
88
    private function writeMarkdown($parsedRoutes)
89
    {
90
        $outputPath = $this->option('output');
91
92
        $documentarian = new Documentarian();
93
94
        $markdown = view('apidoc::documentarian')
1 ignored issue
show
Bug introduced by
The method with does only exist in Illuminate\View\View, but not in Illuminate\Contracts\View\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
95
            ->with('outputPath', $this->option('output'))
96
            ->with('showPostmanCollectionButton', !$this->option('noPostmanCollection'))
97
            ->with('parsedRoutes', $parsedRoutes->all());
98
99
        if (! is_dir($outputPath)) {
100
            $documentarian->create($outputPath);
101
        }
102
103
        file_put_contents($outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'index.md', $markdown);
104
105
        $this->info('Wrote index.md to: '.$outputPath);
106
107
        $this->info('Generating API HTML code');
108
109
        $documentarian->generate($outputPath);
110
111
        $this->info('Wrote HTML documentation to: '.$outputPath.'/public/index.html');
112
113
114
        if ($this->option('noPostmanCollection') !== true) {
115
            $this->info('Generating Postman collection');
116
117
            file_put_contents($outputPath.DIRECTORY_SEPARATOR.'collection.json', $this->generatePostmanCollection($parsedRoutes));
118
        }
119
    }
120
121
    /**
122
     * @return array
123
     */
124
    private function getBindings()
125
    {
126
        $bindings = $this->option('bindings');
127
        if (empty($bindings)) {
128
            return [];
129
        }
130
        $bindings = explode('|', $bindings);
131
        $resultBindings = [];
132
        foreach ($bindings as $binding) {
133
            list($name, $id) = explode(',', $binding);
134
            $resultBindings[$name] = $id;
135
        }
136
137
        return $resultBindings;
138
    }
139
140
    /**
141
     * @param $actAs
142
     */
143
    private function setUserToBeImpersonated($actAs)
144
    {
145
        if (! empty($actAs)) {
146
            if (version_compare($this->laravel->version(), '5.2.0', '<')) {
147
                $userModel = config('auth.model');
148
                $user = $userModel::find($actAs);
149
                $this->laravel['auth']->setUser($user);
150
            } else {
151
                $userModel = config('auth.providers.users.model');
152
                $user = $userModel::find($actAs);
153
                $this->laravel['auth']->guard()->setUser($user);
154
            }
155
        }
156
    }
157
158
    /**
159
     * @return mixed
160
     */
161
    private function getRoutes()
162
    {
163
        if ($this->option('router') === 'laravel') {
164
            return Route::getRoutes();
165
        } else {
166
            return app('Dingo\Api\Routing\Router')->getRoutes()[$this->option('routePrefix')];
167
        }
168
    }
169
170
    /**
171
     * @param AbstractGenerator  $generator
172
     * @param $allowedRoutes
173
     * @param $routePrefix
174
     *
175
     * @return array
176
     */
177
    private function processLaravelRoutes(AbstractGenerator $generator, $allowedRoutes, $routePrefix)
178
    {
179
        $withResponse = $this->option('noResponseCalls') === false;
180
        $routes = $this->getRoutes();
181
        $bindings = $this->getBindings();
182
        $parsedRoutes = [];
183
        foreach ($routes as $route) {
184
            if (in_array($route->getName(), $allowedRoutes) || str_is($routePrefix, $route->getUri())) {
185
                if ($this->isValidRoute($route)) {
186
                    $parsedRoutes[] = $generator->processRoute($route, $bindings, $withResponse);
187
                    $this->info('Processed route: '.$route->getUri());
188
                } else {
189
                    $this->warn('Skipping route: '.$route->getUri().' - contains closure.');
190
                }
191
            }
192
        }
193
194
        return $parsedRoutes;
195
    }
196
197
    /**
198
     * @param AbstractGenerator $generator
199
     * @param $allowedRoutes
200
     * @param $routePrefix
201
     *
202
     * @return array
203
     */
204
    private function processDingoRoutes(AbstractGenerator $generator, $allowedRoutes, $routePrefix)
205
    {
206
        $withResponse = $this->option('noResponseCalls') === false;
207
        $routes = $this->getRoutes();
208
        $bindings = $this->getBindings();
209
        $parsedRoutes = [];
210
        foreach ($routes as $route) {
211
            if (empty($allowedRoutes) || in_array($route->getName(), $allowedRoutes) || str_is($routePrefix, $route->uri())) {
212
                $parsedRoutes[] = $generator->processRoute($route, $bindings, $withResponse);
213
                $this->info('Processed route: '.$route->uri());
214
            }
215
        }
216
217
        return $parsedRoutes;
218
    }
219
220
    /**
221
     * @param $route
222
     *
223
     * @return bool
224
     */
225
    private function isValidRoute($route)
226
    {
227
        return ! is_callable($route->getAction()['uses']);
228
    }
229
230
    /**
231
     * Generate Postman collection JSON file
232
     *
233
     * @param Collection $routes
234
     * @return string
235
     */
236
    private function generatePostmanCollection(Collection $routes)
237
    {
238
        $writer = new CollectionWriter($routes);
239
        return $writer->getCollection();
240
    }
241
}
242