Completed
Push — master ( 8d9a85...38fe88 )
by Marcel
02:43
created

GenerateDocumentation::setUserToBeImpersonated()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 14
rs 9.4285
cc 3
eloc 10
nc 3
nop 1
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
                            {--force : Force rewriting of existing routes}
30
                            {--bindings= : Route Model Bindings}
31
    ';
32
33
    /**
34
     * The console command description.
35
     *
36
     * @var string
37
     */
38
    protected $description = 'Generate your API documentation from existing Laravel routes.';
39
40
    /**
41
     * Create a new command instance.
42
     *
43
     * @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...
44
     */
45
    public function __construct()
46
    {
47
        parent::__construct();
48
    }
49
50
    /**
51
     * Execute the console command.
52
     *
53
     * @return false|null
54
     */
55
    public function handle()
56
    {
57
        if ($this->option('router') === 'laravel') {
58
            $generator = new LaravelGenerator();
59
        } else {
60
            $generator = new DingoGenerator();
61
        }
62
63
        $allowedRoutes = $this->option('routes');
64
        $routePrefix = $this->option('routePrefix');
65
66
        $this->setUserToBeImpersonated($this->option('actAsUserId'));
67
68
        if ($routePrefix === null && ! count($allowedRoutes)) {
69
            $this->error('You must provide either a route prefix or a route to generate the documentation.');
70
71
            return false;
72
        }
73
74
        if ($this->option('router') === 'laravel') {
75
            $parsedRoutes = $this->processLaravelRoutes($generator, $allowedRoutes, $routePrefix);
76
        } else {
77
            $parsedRoutes = $this->processDingoRoutes($generator, $allowedRoutes, $routePrefix);
78
        }
79
        $parsedRoutes = collect($parsedRoutes)->groupBy('resource')->sortBy('resource');
80
81
        $this->writeMarkdown($parsedRoutes);
82
    }
83
84
    /**
85
     * @param  Collection $parsedRoutes
86
     *
87
     * @return void
88
     */
89
    private function writeMarkdown($parsedRoutes)
90
    {
91
        $outputPath = $this->option('output');
92
        $targetFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'index.md';
93
94
        $infoText = view('apidoc::partials.info')
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
98
        $parsedRouteOutput = $parsedRoutes->map(function ($routeGroup) {
99
            return $routeGroup->map(function($route){
100
                $route['output'] = (string)view('apidoc::partials.route')->with('parsedRoute', $route);
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...
101
                return $route;
102
            });
103
        });
104
105
        $frontmatter = view('apidoc::partials.frontmatter');
106
        /**
107
         * In case the target file already exists, we should check if the documentation was modified
108
         * and skip the modified parts of the routes.
109
         */
110
        if (file_exists($targetFile)) {
111
            $generatedDocumentation = file_get_contents($targetFile);
112
113
            if (preg_match("/<!-- START_INFO -->(.*)<!-- END_INFO -->/is", $generatedDocumentation, $generatedInfoText)) {
114
                $infoText = trim($generatedInfoText[1],"\n");
115
            }
116
117
            if (preg_match("/---(.*)---\\s<!-- START_INFO -->/is", $generatedDocumentation, $generatedFrontmatter)) {
118
                $frontmatter = trim($generatedFrontmatter[1],"\n");
119
            }
120
121
            $parsedRouteOutput->transform(function ($routeGroup) use($generatedDocumentation) {
122
                return $routeGroup->transform(function($route) use($generatedDocumentation) {
123
                    if (preg_match("/<!-- START_".$route['id']." -->(.*)<!-- END_".$route['id']." -->/is", $generatedDocumentation, $routeMatch) && !$this->option('force')) {
124
                        $route['output'] = $routeMatch[0];
125
                    }
126
                    return $route;
127
                });
128
            });
129
        }
130
131
        $documentarian = new Documentarian();
132
133
        $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...
134
            ->with('frontmatter', $frontmatter)
135
            ->with('infoText', $infoText)
136
            ->with('outputPath', $this->option('output'))
137
            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'))
138
            ->with('parsedRoutes', $parsedRouteOutput);
139
140
        if (! is_dir($outputPath)) {
141
            $documentarian->create($outputPath);
142
        }
143
144
        file_put_contents($targetFile, $markdown);
145
146
        $this->info('Wrote index.md to: '.$outputPath);
147
148
        $this->info('Generating API HTML code');
149
150
        $documentarian->generate($outputPath);
151
152
        $this->info('Wrote HTML documentation to: '.$outputPath.'/public/index.html');
153
154
155
        if ($this->option('noPostmanCollection') !== true) {
156
            $this->info('Generating Postman collection');
157
158
            file_put_contents($outputPath.DIRECTORY_SEPARATOR.'collection.json', $this->generatePostmanCollection($parsedRoutes));
159
        }
160
    }
161
162
    /**
163
     * @return array
164
     */
165
    private function getBindings()
166
    {
167
        $bindings = $this->option('bindings');
168
        if (empty($bindings)) {
169
            return [];
170
        }
171
        $bindings = explode('|', $bindings);
172
        $resultBindings = [];
173
        foreach ($bindings as $binding) {
174
            list($name, $id) = explode(',', $binding);
175
            $resultBindings[$name] = $id;
176
        }
177
178
        return $resultBindings;
179
    }
180
181
    /**
182
     * @param $actAs
183
     */
184
    private function setUserToBeImpersonated($actAs)
185
    {
186
        if (! empty($actAs)) {
187
            if (version_compare($this->laravel->version(), '5.2.0', '<')) {
188
                $userModel = config('auth.model');
189
                $user = $userModel::find($actAs);
190
                $this->laravel['auth']->setUser($user);
191
            } else {
192
                $userModel = config('auth.providers.users.model');
193
                $user = $userModel::find($actAs);
194
                $this->laravel['auth']->guard()->setUser($user);
195
            }
196
        }
197
    }
198
199
    /**
200
     * @return mixed
201
     */
202
    private function getRoutes()
203
    {
204
        if ($this->option('router') === 'laravel') {
205
            return Route::getRoutes();
206
        } else {
207
            return app('Dingo\Api\Routing\Router')->getRoutes()[$this->option('routePrefix')];
208
        }
209
    }
210
211
    /**
212
     * @param AbstractGenerator  $generator
213
     * @param $allowedRoutes
214
     * @param $routePrefix
215
     *
216
     * @return array
217
     */
218
    private function processLaravelRoutes(AbstractGenerator $generator, $allowedRoutes, $routePrefix)
219
    {
220
        $withResponse = $this->option('noResponseCalls') === false;
221
        $routes = $this->getRoutes();
222
        $bindings = $this->getBindings();
223
        $parsedRoutes = [];
224
        foreach ($routes as $route) {
225
            if (in_array($route->getName(), $allowedRoutes) || str_is($routePrefix, $route->getUri())) {
226
                if ($this->isValidRoute($route)) {
227
                    $parsedRoutes[] = $generator->processRoute($route, $bindings, $withResponse);
228
                    $this->info('Processed route: '.$route->getUri());
229
                } else {
230
                    $this->warn('Skipping route: '.$route->getUri().' - contains closure.');
231
                }
232
            }
233
        }
234
235
        return $parsedRoutes;
236
    }
237
238
    /**
239
     * @param AbstractGenerator $generator
240
     * @param $allowedRoutes
241
     * @param $routePrefix
242
     *
243
     * @return array
244
     */
245
    private function processDingoRoutes(AbstractGenerator $generator, $allowedRoutes, $routePrefix)
246
    {
247
        $withResponse = $this->option('noResponseCalls') === false;
248
        $routes = $this->getRoutes();
249
        $bindings = $this->getBindings();
250
        $parsedRoutes = [];
251
        foreach ($routes as $route) {
252
            if (empty($allowedRoutes) || in_array($route->getName(), $allowedRoutes) || str_is($routePrefix, $route->uri())) {
253
                $parsedRoutes[] = $generator->processRoute($route, $bindings, $withResponse);
254
                $this->info('Processed route: '.$route->uri());
255
            }
256
        }
257
258
        return $parsedRoutes;
259
    }
260
261
    /**
262
     * @param $route
263
     *
264
     * @return bool
265
     */
266
    private function isValidRoute($route)
267
    {
268
        return ! is_callable($route->getAction()['uses']) && ! is_null($route->getAction()['uses']);
269
    }
270
271
    /**
272
     * Generate Postman collection JSON file.
273
     *
274
     * @param Collection $routes
275
     *
276
     * @return string
277
     */
278
    private function generatePostmanCollection(Collection $routes)
279
    {
280
        $writer = new CollectionWriter($routes);
281
282
        return $writer->getCollection();
283
    }
284
}
285