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