瀏覽代碼

Merge pull request #1 from mpociot/master

Update from base
Shalvah A 6 年之前
父節點
當前提交
3e21c5f07f

+ 9 - 1
README.md

@@ -34,7 +34,15 @@ To generate your API documentation, use the `api:generate` artisan command.
 
 ```sh
 $ php artisan api:generate --routePrefix="api/v1/*"
+
 ```
+You can pass in multiple prefixes by spearating each prefix with comma.
+
+```sh
+$ php artisan api:generate --routePrefix="api/v1/*,api/public/*"
+```
+It will generate documentation for all of the routes whose prefixes are `api/v1/` and `api/public/`
+
 
 This command will scan your applications routes for the URIs matching `api/v1/*` and will parse these controller methods and form requests. For example:
 
@@ -53,7 +61,7 @@ Route::group(array('prefix' => 'api/v1', 'middleware' => []), function () {
 Option | Description
 --------- | -------
 `output` | The output path used for the generated documentation. Default: `public/docs`
-`routePrefix` | The route prefix to use for generation - `*` can be used as a wildcard
+`routePrefix` | The route prefix to use for generation - `*` can be used as a wildcard 
 `routes` | The route names to use for generation - Required if no routePrefix is provided
 `middleware` | The middlewares to use for generation
 `noResponseCalls` | Disable API response calls

+ 39 - 16
src/Mpociot/ApiDoc/Commands/GenerateDocumentation.php

@@ -22,7 +22,8 @@ class GenerateDocumentation extends Command
      */
     protected $signature = 'api:generate
                             {--output=public/docs : The output path for the generated documentation}
-                            {--routePrefix= : The route prefix to use for generation}
+                            {--routeDomain= : The route domain (or domains) to use for generation}
+                            {--routePrefix= : The route prefix (or prefixes) to use for generation}
                             {--routes=* : The route names to use for generation}
                             {--middleware= : The middleware to use for generation}
                             {--noResponseCalls : Disable API response calls}
@@ -68,23 +69,37 @@ class GenerateDocumentation extends Command
         }
 
         $allowedRoutes = $this->option('routes');
+        $routeDomain = $this->option('routeDomain');
         $routePrefix = $this->option('routePrefix');
         $middleware = $this->option('middleware');
 
         $this->setUserToBeImpersonated($this->option('actAsUserId'));
 
-        if ($routePrefix === null && ! count($allowedRoutes) && $middleware === null) {
-            $this->error('You must provide either a route prefix or a route or a middleware to generate the documentation.');
+        if ($routePrefix === null && $routeDomain === null && ! count($allowedRoutes) && $middleware === null) {
+            $this->error('You must provide either a route prefix, a route domain, a route or a middleware to generate the documentation.');
 
             return false;
         }
 
         $generator->prepareMiddleware($this->option('useMiddlewares'));
 
+        $routePrefixes = explode(',', $routePrefix ?: '*');
+        $routeDomains = explode(',', $routeDomain ?: '*');
+
+        $parsedRoutes = [];
+
         if ($this->option('router') === 'laravel') {
-            $parsedRoutes = $this->processLaravelRoutes($generator, $allowedRoutes, $routePrefix, $middleware);
+            foreach ($routeDomains as $routeDomain) {
+                foreach ($routePrefixes as $routePrefix) {
+                    $parsedRoutes += $this->processLaravelRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware);
+                }
+            }
         } else {
-            $parsedRoutes = $this->processDingoRoutes($generator, $allowedRoutes, $routePrefix, $middleware);
+            foreach ($routeDomains as $routeDomain) {
+                foreach ($routePrefixes as $routePrefix) {
+                    $parsedRoutes += $this->processDingoRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware);
+                }
+            }
         }
         $parsedRoutes = collect($parsedRoutes)->groupBy('resource')->sort(function ($a, $b) {
             return strcmp($a->first()['resource'], $b->first()['resource']);
@@ -125,10 +140,6 @@ class GenerateDocumentation extends Command
             $generatedDocumentation = file_get_contents($targetFile);
             $compareDocumentation = file_get_contents($compareFile);
 
-            if (preg_match('/<!-- START_INFO -->(.*)<!-- END_INFO -->/is', $generatedDocumentation, $generatedInfoText)) {
-                $infoText = trim($generatedInfoText[1], "\n");
-            }
-
             if (preg_match('/---(.*)---\\s<!-- START_INFO -->/is', $generatedDocumentation, $generatedFrontmatter)) {
                 $frontmatter = trim($generatedFrontmatter[1], "\n");
             }
@@ -222,12 +233,12 @@ class GenerateDocumentation extends Command
         if (! empty($actAs)) {
             if (version_compare($this->laravel->version(), '5.2.0', '<')) {
                 $userModel = config('auth.model');
-                $user = $userModel::find((int) $actAs);
+                $user = $userModel::find($actAs);
                 $this->laravel['auth']->setUser($user);
             } else {
                 $provider = $this->option('authProvider');
                 $userModel = config("auth.providers.$provider.model");
-                $user = $userModel::find((int) $actAs);
+                $user = $userModel::find($actAs);
                 $this->laravel['auth']->guard($this->option('authGuard'))->setUser($user);
             }
         }
@@ -248,20 +259,25 @@ class GenerateDocumentation extends Command
     /**
      * @param AbstractGenerator  $generator
      * @param $allowedRoutes
+     * @param $routeDomain
      * @param $routePrefix
      *
      * @return array
      */
-    private function processLaravelRoutes(AbstractGenerator $generator, $allowedRoutes, $routePrefix, $middleware)
+    private function processLaravelRoutes(AbstractGenerator $generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware)
     {
         $withResponse = $this->option('noResponseCalls') === false;
         $routes = $this->getRoutes();
         $bindings = $this->getBindings();
         $parsedRoutes = [];
         foreach ($routes as $route) {
-            if (in_array($route->getName(), $allowedRoutes) || str_is($routePrefix, $generator->getUri($route)) || in_array($middleware, $route->middleware())) {
+            if (in_array($route->getName(), $allowedRoutes)
+                || (str_is($routeDomain, $generator->getDomain($route))
+                    && str_is($routePrefix, $generator->getUri($route)))
+                || in_array($middleware, $route->middleware())
+               ) {
                 if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
-                    $parsedRoutes[] = $generator->processRoute($route, $bindings, $this->option('header'), $withResponse);
+                    $parsedRoutes[] = $generator->processRoute($route, $bindings, $this->option('header'), $withResponse && in_array('GET', $generator->getMethods($route)));
                     $this->info('Processed route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
                 } else {
                     $this->warn('Skipping route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
@@ -275,18 +291,25 @@ class GenerateDocumentation extends Command
     /**
      * @param AbstractGenerator $generator
      * @param $allowedRoutes
+     * @param $routeDomain
      * @param $routePrefix
      *
      * @return array
      */
-    private function processDingoRoutes(AbstractGenerator $generator, $allowedRoutes, $routePrefix, $middleware)
+    private function processDingoRoutes(AbstractGenerator $generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware)
     {
         $withResponse = $this->option('noResponseCalls') === false;
         $routes = $this->getRoutes();
         $bindings = $this->getBindings();
         $parsedRoutes = [];
         foreach ($routes as $route) {
-            if (empty($allowedRoutes) || in_array($route->getName(), $allowedRoutes) || str_is($routePrefix, $route->uri()) || in_array($middleware, $route->middleware())) {
+            if (empty($allowedRoutes)
+                // TODO extract this into a method
+                || in_array($route->getName(), $allowedRoutes)
+                || (str_is($routeDomain, $generator->getDomain($route))
+                    && str_is($routePrefix, $generator->getUri($route)))
+                || in_array($middleware, $route->middleware())
+               ) {
                 if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
                     $parsedRoutes[] = $generator->processRoute($route, $bindings, $this->option('header'), $withResponse);
                     $this->info('Processed route: ['.implode(',', $route->getMethods()).'] '.$route->uri());

+ 16 - 2
src/Mpociot/ApiDoc/Generators/AbstractGenerator.php

@@ -14,6 +14,13 @@ use Mpociot\ApiDoc\Parsers\RuleDescriptionParser as Description;
 
 abstract class AbstractGenerator
 {
+    /**
+     * @param $route
+     *
+     * @return mixed
+     */
+    abstract public function getDomain($route);
+
     /**
      * @param $route
      *
@@ -44,7 +51,7 @@ abstract class AbstractGenerator
      *
      * @return  void
      */
-    abstract public function prepareMiddleware($disable = false);
+    abstract public function prepareMiddleware($enable = false);
 
     /**
      * Get the response from the docblock if available.
@@ -147,7 +154,7 @@ abstract class AbstractGenerator
         })->collapse()->toArray();
 
         //Changes url with parameters like /users/{user} to /users/1
-        $uri = preg_replace('/{(.*?)}/', 1, $uri);
+        $uri = preg_replace('/{(.*?)}/', 1, $uri); // 1 is the default value for route parameters
 
         return $this->callRoute(array_shift($methods), $uri, [], [], [], $headers);
     }
@@ -163,6 +170,7 @@ abstract class AbstractGenerator
         $uri = $this->getUri($route);
         foreach ($bindings as $model => $id) {
             $uri = str_replace('{'.$model.'}', $id, $uri);
+            $uri = str_replace('{'.$model.'?}', $id, $uri);
         }
 
         return $uri;
@@ -462,6 +470,12 @@ abstract class AbstractGenerator
                 $attributeData['value'] = $faker->ipv4;
                 $attributeData['type'] = $rule;
                 break;
+            default:
+                $unknownRuleDescription = Description::parse($rule)->getDescription();
+                if ($unknownRuleDescription) {
+                    $attributeData['description'][] = $unknownRuleDescription;
+                }
+                break;
         }
 
         if ($attributeData['value'] === '') {

+ 9 - 1
src/Mpociot/ApiDoc/Generators/DingoGenerator.php

@@ -68,6 +68,14 @@ class DingoGenerator extends AbstractGenerator
         return call_user_func_array([$dispatcher, strtolower($method)], [$uri]);
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function getDomain($route)
+    {
+        return $route->domain();
+    }
+
     /**
      * {@inheritdoc}
      */
@@ -81,6 +89,6 @@ class DingoGenerator extends AbstractGenerator
      */
     public function getMethods($route)
     {
-        return $route->getMethods();
+        return array_diff($route->getMethods(), ['HEAD']);
     }
 }

+ 25 - 10
src/Mpociot/ApiDoc/Generators/LaravelGenerator.php

@@ -11,9 +11,20 @@ use Mpociot\Reflection\DocBlock\Tag;
 use Illuminate\Support\Facades\Request;
 use League\Fractal\Resource\Collection;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Contracts\Validation\Factory as ValidationFactory;
 
 class LaravelGenerator extends AbstractGenerator
 {
+    /**
+     * @param Route $route
+     *
+     * @return mixed
+     */
+    public function getDomain($route)
+    {
+        return $route->domain();
+    }
+
     /**
      * @param Route $route
      *
@@ -36,10 +47,12 @@ class LaravelGenerator extends AbstractGenerator
     public function getMethods($route)
     {
         if (version_compare(app()->version(), '5.4', '<')) {
-            return $route->getMethods();
+            $methods = $route->getMethods();
+        } else {
+            $methods = $route->methods();
         }
 
-        return $route->methods();
+        return array_diff($methods, ['HEAD']);
     }
 
     /**
@@ -54,11 +67,16 @@ class LaravelGenerator extends AbstractGenerator
     {
         $content = '';
 
+        $routeDomain = $route->domain();
         $routeAction = $route->getAction();
         $routeGroup = $this->getRouteGroup($routeAction['uses']);
         $routeDescription = $this->getRouteDescription($routeAction['uses']);
         $showresponse = null;
 
+        // set correct route domain
+        $headers[] = "HTTP_HOST: {$routeDomain}";
+        $headers[] = "SERVER_NAME: {$routeDomain}";
+
         if ($withResponse) {
             $response = null;
             $docblockResponse = $this->getDocblockResponse($routeDescription['tags']);
@@ -105,9 +123,9 @@ class LaravelGenerator extends AbstractGenerator
      *
      * @return  void
      */
-    public function prepareMiddleware($disable = true)
+    public function prepareMiddleware($enable = true)
     {
-        App::instance('middleware.disable', true);
+        App::instance('middleware.disable', ! $enable);
     }
 
     /**
@@ -140,11 +158,6 @@ class LaravelGenerator extends AbstractGenerator
 
         $kernel->terminate($request, $response);
 
-        if (file_exists($file = App::bootstrapPath().'/app.php')) {
-            $app = require $file;
-            $app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
-        }
-
         return $response;
     }
 
@@ -266,7 +279,9 @@ class LaravelGenerator extends AbstractGenerator
                     $parameterReflection->request->add($bindings);
 
                     if (method_exists($parameterReflection, 'validator')) {
-                        return app()->call([$parameterReflection, 'validator'])
+                        $factory = app()->make(ValidationFactory::class);
+
+                        return app()->call([$parameterReflection, 'validator'], [$factory])
                             ->getRules();
                     } else {
                         return app()->call([$parameterReflection, 'rules']);

+ 11 - 1
tests/ApiDocGeneratorTest.php

@@ -53,7 +53,7 @@ class ApiDocGeneratorTest extends TestCase
 
         $route = new Route(['GET'], '/get', ['uses' => TestController::class.'@parseMethodDescription']);
         $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['GET', 'HEAD'], $parsed['methods']);
+        $this->assertSame(['GET'], $parsed['methods']);
 
         $route = new Route(['POST'], '/post', ['uses' => TestController::class.'@parseMethodDescription']);
         $parsed = $this->generator->processRoute($route);
@@ -337,6 +337,16 @@ class ApiDocGeneratorTest extends TestCase
         }
     }
 
+    public function testCustomFormRequestValidatorIsSupported()
+    {
+        RouteFacade::post('/post', TestController::class.'@customFormRequestValidator');
+        $route = new Route(['POST'], '/post', ['uses' => TestController::class.'@customFormRequestValidator']);
+        $parsed = $this->generator->processRoute($route);
+        $parameters = $parsed['parameters'];
+
+        $this->assertNotEmpty($parameters);
+    }
+
     public function testCanParseResponseTag()
     {
         RouteFacade::post('/responseTag', TestController::class.'@responseTag');

+ 1 - 1
tests/DingoGeneratorTest.php

@@ -68,7 +68,7 @@ class DingoGeneratorTest extends TestCase
         });
         $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[0];
         $parsed = $this->generator->processRoute($route);
-        $this->assertSame(['GET', 'HEAD'], $parsed['methods']);
+        $this->assertSame(['GET'], $parsed['methods']);
 
         $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[1];
         $parsed = $this->generator->processRoute($route);

+ 30 - 0
tests/Fixtures/CustomValidatorRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tests\Fixtures;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class CustomValidatorRequest extends FormRequest
+{
+    /**
+     * Validate the input.
+     *
+     * @param \Illuminate\Validation\Factory $factory
+     *
+     * @return \Illuminate\Validation\Validator
+     */
+    public function validator($factory)
+    {
+        return $factory->make(
+            $this->validationData(), $this->container->call([$this, 'foo']),
+            $this->messages(), $this->attributes()
+        );
+    }
+
+    public function foo()
+    {
+        return [
+            'required' => 'required',
+        ];
+    }
+}

+ 5 - 0
tests/Fixtures/TestController.php

@@ -27,6 +27,11 @@ class TestController extends Controller
         return '';
     }
 
+    public function customFormRequestValidator(CustomValidatorRequest $request)
+    {
+        return '';
+    }
+
     public function addRouteBindingsToRequestClass(DynamicRequest $request)
     {
         return '';

+ 4 - 8
tests/Fixtures/index.md

@@ -21,7 +21,7 @@ Welcome to the generated API reference.
 <!-- END_INFO -->
 
 #general
-<!-- START_08307893aff90cc5097c48a1c8fc2f6d -->
+<!-- START_0bef4e738c9d6720ad43b062015d1078 -->
 ## Example title.
 
 This will be the long description.
@@ -59,12 +59,10 @@ null
 ### HTTP Request
 `GET api/test`
 
-`HEAD api/test`
 
+<!-- END_0bef4e738c9d6720ad43b062015d1078 -->
 
-<!-- END_08307893aff90cc5097c48a1c8fc2f6d -->
-
-<!-- START_8ba174f2507a0967efd46fab3764b80e -->
+<!-- START_960a1b2b0f0f4dde8ce993307397f9c4 -->
 ## api/fetch
 
 > Example request:
@@ -105,8 +103,6 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/fetch`
 
-`HEAD api/fetch`
-
 
-<!-- END_8ba174f2507a0967efd46fab3764b80e -->
+<!-- END_960a1b2b0f0f4dde8ce993307397f9c4 -->
 

+ 8 - 16
tests/Fixtures/resource_index.md

@@ -21,7 +21,7 @@ Welcome to the generated API reference.
 <!-- END_INFO -->
 
 #general
-<!-- START_2ea88ff35aa222f5582e50f39a2b35fd -->
+<!-- START_2b6e5a4b188cb183c7e59558cce36cb6 -->
 ## Display a listing of the resource.
 
 > Example request:
@@ -58,12 +58,10 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/user`
 
-`HEAD api/user`
 
+<!-- END_2b6e5a4b188cb183c7e59558cce36cb6 -->
 
-<!-- END_2ea88ff35aa222f5582e50f39a2b35fd -->
-
-<!-- START_99a7210df460e7fd8ad2508ee28b9763 -->
+<!-- START_7f66c974d24032cb19061d55d801f62b -->
 ## Show the form for creating a new resource.
 
 > Example request:
@@ -100,10 +98,8 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/user/create`
 
-`HEAD api/user/create`
-
 
-<!-- END_99a7210df460e7fd8ad2508ee28b9763 -->
+<!-- END_7f66c974d24032cb19061d55d801f62b -->
 
 <!-- START_f0654d3f2fc63c11f5723f233cc53c83 -->
 ## Store a newly created resource in storage.
@@ -138,7 +134,7 @@ $.ajax(settings).done(function (response) {
 
 <!-- END_f0654d3f2fc63c11f5723f233cc53c83 -->
 
-<!-- START_7a5835399fad9a53bc0430d6e3054297 -->
+<!-- START_ceec0e0b1d13d731ad96603d26bccc2f -->
 ## Display the specified resource.
 
 > Example request:
@@ -175,12 +171,10 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/user/{user}`
 
-`HEAD api/user/{user}`
 
+<!-- END_ceec0e0b1d13d731ad96603d26bccc2f -->
 
-<!-- END_7a5835399fad9a53bc0430d6e3054297 -->
-
-<!-- START_5ed9d10b12650f9536edfa994fafae15 -->
+<!-- START_f4aa12af19ba08e1448d7eafc9f55e67 -->
 ## Show the form for editing the specified resource.
 
 > Example request:
@@ -217,10 +211,8 @@ $.ajax(settings).done(function (response) {
 ### HTTP Request
 `GET api/user/{user}/edit`
 
-`HEAD api/user/{user}/edit`
-
 
-<!-- END_5ed9d10b12650f9536edfa994fafae15 -->
+<!-- END_f4aa12af19ba08e1448d7eafc9f55e67 -->
 
 <!-- START_a4a2abed1e8e8cad5e6a3282812fe3f3 -->
 ## Update the specified resource in storage.

+ 6 - 6
tests/GenerateDocumentationTest.php

@@ -64,8 +64,8 @@ class GenerateDocumentationTest extends TestCase
         $output = $this->artisan('api:generate', [
             '--routePrefix' => 'api/*',
         ]);
-        $this->assertContains('Skipping route: [GET,HEAD] api/closure', $output);
-        $this->assertContains('Processed route: [GET,HEAD] api/test', $output);
+        $this->assertContains('Skipping route: [GET] api/closure', $output);
+        $this->assertContains('Processed route: [GET] api/test', $output);
     }
 
     public function testConsoleCommandDoesNotWorkWithClosureUsingDingo()
@@ -85,8 +85,8 @@ class GenerateDocumentationTest extends TestCase
                 '--router' => 'dingo',
                 '--routePrefix' => 'v1',
             ]);
-            $this->assertContains('Skipping route: [GET,HEAD] closure', $output);
-            $this->assertContains('Processed route: [GET,HEAD] test', $output);
+            $this->assertContains('Skipping route: [GET] closure', $output);
+            $this->assertContains('Processed route: [GET] test', $output);
         });
     }
 
@@ -98,8 +98,8 @@ class GenerateDocumentationTest extends TestCase
         $output = $this->artisan('api:generate', [
             '--routePrefix' => 'api/*',
         ]);
-        $this->assertContains('Skipping route: [GET,HEAD] api/skip', $output);
-        $this->assertContains('Processed route: [GET,HEAD] api/test', $output);
+        $this->assertContains('Skipping route: [GET] api/skip', $output);
+        $this->assertContains('Processed route: [GET] api/test', $output);
     }
 
     public function testCanParseResourceRoutes()