Prechádzať zdrojové kódy

Switch from `bindings` to `@urlParam` annotation

shalvah 5 rokov pred
rodič
commit
baa862721d

+ 1 - 2
TODO.md

@@ -1,4 +1,3 @@
 Major
-- Bring `bindings` outside of `response_calls`
-- Should `routes.*.apply.response_calls.headers` be replaced by `routes.*.apply.headers`?
+- Should `routes.*.apply.response_calls.headers` be replaced by `routes.*.apply.headers`? yes
 - Should we move HTML generation from Blade to fully PHP? (L)

+ 5 - 16
config/apidoc.php

@@ -119,20 +119,6 @@ return [
                      */
                     'methods' => ['GET'],
 
-                    /*
-                     * For URLs which have parameters (/users/{user}, /orders/{id?}),
-                     * specify what values the parameters should be replaced with.
-                     * Note that you must specify the full parameter,
-                     * including curly brackets and question marks if any.
-                     *
-                     * You may also specify the preceding path, to allow for variations,
-                     * for instance 'users/{id}' => 1 and 'apps/{id}' => 'htTviP'.
-                     * However, there must only be one parameter per path.
-                     */
-                    'bindings' => [
-                        // '{user}' => 1,
-                    ],
-
                     /*
                      * Laravel config variables which should be set for the API call.
                      * This is a good place to ensure that notifications, emails
@@ -183,12 +169,15 @@ return [
         'metadata' => [
             \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class,
         ],
-        'bodyParameters' => [
-            \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+        'urlParameters' => [
+            \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class,
         ],
         'queryParameters' => [
             \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class,
         ],
+        'bodyParameters' => [
+            \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+        ],
         'responses' => [
             \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class,
             \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class,

+ 14 - 6
resources/views/partials/route.blade.php

@@ -47,13 +47,13 @@
 `{{$method}} {{$route['uri']}}`
 
 @endforeach
-@if(count($route['bodyParameters']))
-#### Body Parameters
+@if(count($route['urlParameters']))
+#### URL Parameters
 
-Parameter | Type | Status | Description
---------- | ------- | ------- | ------- | -----------
-@foreach($route['bodyParameters'] as $attribute => $parameter)
-    {{$attribute}} | {{$parameter['type']}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
+Parameter | Status | Description
+--------- | ------- | ------- | -------
+@foreach($route['urlParameters'] as $attribute => $parameter)
+    {{$attribute}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
 @endforeach
 @endif
 @if(count($route['queryParameters']))
@@ -65,5 +65,13 @@ Parameter | Status | Description
     {{$attribute}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
 @endforeach
 @endif
+@if(count($route['bodyParameters']))
+#### Body Parameters
+Parameter | Type | Status | Description
+--------- | ------- | ------- | ------- | -----------
+@foreach($route['bodyParameters'] as $attribute => $parameter)
+    {{$attribute}} | {{$parameter['type']}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
+    @endforeach
+@endif
 
 <!-- END_{{$route['id']}} -->

+ 6 - 3
src/Strategies/BodyParameters/GetFromBodyParamTag.php

@@ -25,9 +25,7 @@ class GetFromBodyParamTag extends Strategy
                 continue;
             }
 
-            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
-                ? $paramType->__toString()
-                : $paramType->getName();
+            $parameterClassName = $paramType->getName();
 
             try {
                 $parameterClass = new ReflectionClass($parameterClassName);
@@ -59,6 +57,11 @@ class GetFromBodyParamTag extends Strategy
                 return $tag instanceof Tag && $tag->getName() === 'bodyParam';
             })
             ->mapWithKeys(function ($tag) {
+                // Format:
+                // @bodyParam <name> <type> <"required" (optional)> <description>
+                // Examples:
+                // @bodyParam text string required The text.
+                // @bodyParam user_id integer The ID of the user.
                 preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
                 $content = preg_replace('/\s?No-example.?/', '', $content);
                 if (empty($content)) {

+ 8 - 5
src/Strategies/QueryParameters/GetFromQueryParamTag.php

@@ -26,9 +26,7 @@ class GetFromQueryParamTag extends Strategy
                 continue;
             }
 
-            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
-                ? $paramType->__toString()
-                : $paramType->getName();
+            $parameterClassName = $paramType->getName();
 
             try {
                 $parameterClass = new ReflectionClass($parameterClassName);
@@ -40,7 +38,7 @@ class GetFromQueryParamTag extends Strategy
             if (class_exists(LaravelFormRequest::class) && $parameterClass->isSubclassOf(LaravelFormRequest::class)
                 || class_exists(DingoFormRequest::class) && $parameterClass->isSubclassOf(DingoFormRequest::class)) {
                 $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
-                $queryParametersFromDocBlock = $this->getqueryParametersFromDocBlock($formRequestDocBlock->getTags());
+                $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags());
 
                 if (count($queryParametersFromDocBlock)) {
                     return $queryParametersFromDocBlock;
@@ -50,7 +48,7 @@ class GetFromQueryParamTag extends Strategy
 
         $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method'];
 
-        return $this->getqueryParametersFromDocBlock($methodDocBlock->getTags());
+        return $this->getQueryParametersFromDocBlock($methodDocBlock->getTags());
     }
 
     private function getQueryParametersFromDocBlock($tags)
@@ -60,6 +58,11 @@ class GetFromQueryParamTag extends Strategy
                 return $tag instanceof Tag && $tag->getName() === 'queryParam';
             })
             ->mapWithKeys(function ($tag) {
+                // Format:
+                // @queryParam <name> <"required" (optional)> <description>
+                // Examples:
+                // @queryParam text string required The text.
+                // @queryParam user_id The ID of the user.
                 preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
                 $content = preg_replace('/\s?No-example.?/', '', $content);
                 if (empty($content)) {

+ 4 - 3
src/Strategies/Responses/ResponseCalls.php

@@ -40,7 +40,8 @@ class ResponseCalls extends Strategy
         // Mix in parsed parameters with manually specified parameters.
         $bodyParameters = array_merge($context['cleanBodyParameters'], $rulesToApply['body'] ?? []);
         $queryParameters = array_merge($context['cleanQueryParameters'], $rulesToApply['query'] ?? []);
-        $request = $this->prepareRequest($route, $rulesToApply, $bodyParameters, $queryParameters);
+        $urlParameters = $context['cleanUrlParameters'];
+        $request = $this->prepareRequest($route, $rulesToApply, $urlParameters, $bodyParameters, $queryParameters);
 
         try {
             $response = $this->makeApiCall($request);
@@ -80,9 +81,9 @@ class ResponseCalls extends Strategy
      *
      * @return Request
      */
-    protected function prepareRequest(Route $route, array $rulesToApply, array $bodyParams, array $queryParams)
+    protected function prepareRequest(Route $route, array $rulesToApply, array $urlParams, array $bodyParams, array $queryParams)
     {
-        $uri = Utils::getFullUrl($route, $rulesToApply['bindings'] ?? []);
+        $uri = Utils::getFullUrl($route, $urlParams);
         $routeMethods = $this->getMethods($route);
         $method = array_shift($routeMethods);
         $cookies = isset($rulesToApply['cookies']) ? $rulesToApply['cookies'] : [];

+ 95 - 0
src/Strategies/UrlParameters/GetFromUrlParamTag.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace Mpociot\ApiDoc\Strategies\UrlParameters;
+
+use ReflectionClass;
+use ReflectionMethod;
+use Illuminate\Support\Str;
+use Illuminate\Routing\Route;
+use Mpociot\Reflection\DocBlock;
+use Mpociot\Reflection\DocBlock\Tag;
+use Mpociot\ApiDoc\Strategies\Strategy;
+use Mpociot\ApiDoc\Tools\RouteDocBlocker;
+use Dingo\Api\Http\FormRequest as DingoFormRequest;
+use Mpociot\ApiDoc\Tools\Traits\DocBlockParamHelpers;
+use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest;
+
+class GetFromUrlParamTag extends Strategy
+{
+    use DocBlockParamHelpers;
+
+    public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = [])
+    {
+        foreach ($method->getParameters() as $param) {
+            $paramType = $param->getType();
+            if ($paramType === null) {
+                continue;
+            }
+
+            $parameterClassName = $paramType->getName();
+
+            try {
+                $parameterClass = new ReflectionClass($parameterClassName);
+            } catch (\ReflectionException $e) {
+                continue;
+            }
+
+            // If there's a FormRequest, we check there for @urlParam tags.
+            if (class_exists(LaravelFormRequest::class) && $parameterClass->isSubclassOf(LaravelFormRequest::class)
+                || class_exists(DingoFormRequest::class) && $parameterClass->isSubclassOf(DingoFormRequest::class)) {
+                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
+                $queryParametersFromDocBlock = $this->getUrlParametersFromDocBlock($formRequestDocBlock->getTags());
+
+                if (count($queryParametersFromDocBlock)) {
+                    return $queryParametersFromDocBlock;
+                }
+            }
+        }
+
+        $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method'];
+
+        return $this->getUrlParametersFromDocBlock($methodDocBlock->getTags());
+    }
+
+    private function getUrlParametersFromDocBlock($tags)
+    {
+        $parameters = collect($tags)
+            ->filter(function ($tag) {
+                return $tag instanceof Tag && $tag->getName() === 'urlParam';
+            })
+            ->mapWithKeys(function ($tag) {
+                // Format:
+                // @urlParam <name> <"required" (optional)> <description>
+                // Examples:
+                // @urlParam id string required The id of the post.
+                // @urlParam user_id The ID of the user.
+                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
+                $content = preg_replace('/\s?No-example.?/', '', $content);
+                if (empty($content)) {
+                    // this means only name was supplied
+                    list($name) = preg_split('/\s+/', $tag->getContent());
+                    $required = false;
+                    $description = '';
+                } else {
+                    list($_, $name, $required, $description) = $content;
+                    $description = trim($description);
+                    if ($description == 'required' && empty(trim($required))) {
+                        $required = $description;
+                        $description = '';
+                    }
+                    $required = trim($required) == 'required' ? true : false;
+                }
+
+                list($description, $value) = $this->parseParamDescription($description, 'string');
+                if (is_null($value) && ! $this->shouldExcludeExample($tag)) {
+                    $value = Str::contains($description, ['number', 'count', 'page'])
+                        ? $this->generateDummyValue('integer')
+                        : $this->generateDummyValue('string');
+                }
+
+                return [$name => compact('description', 'required', 'value')];
+            })->toArray();
+
+        return $parameters;
+    }
+}

+ 23 - 9
src/Tools/Generator.php

@@ -57,18 +57,24 @@ class Generator
             'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
             'methods' => $this->getMethods($route),
             'uri' => $this->getUri($route),
-            'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])),
         ];
         $metadata = $this->fetchMetadata($controller, $method, $route, $rulesToApply, $parsedRoute);
         $parsedRoute['metadata'] = $metadata;
-        $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
-        $parsedRoute['bodyParameters'] = $bodyParameters;
-        $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters);
+
+        $urlParameters = $this->fetchUrlParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
+        $parsedRoute['urlParameters'] = $urlParameters;
+        $parsedRoute['cleanUrlParameters'] = $this->cleanParams($urlParameters);
+        $parsedRoute['boundUri'] = Utils::getFullUrl($route, $parsedRoute['cleanUrlParameters']);
 
         $queryParameters = $this->fetchQueryParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
         $parsedRoute['queryParameters'] = $queryParameters;
         $parsedRoute['cleanQueryParameters'] = $this->cleanParams($queryParameters);
 
+        $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
+        $parsedRoute['bodyParameters'] = $bodyParameters;
+        $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters);
+
+
         $responses = $this->fetchResponses($controller, $method, $route, $rulesToApply, $parsedRoute);
         $parsedRoute['response'] = $responses;
         $parsedRoute['showresponse'] = ! empty($responses);
@@ -92,10 +98,9 @@ class Generator
 
         return $this->iterateThroughStrategies('metadata', $context, [$route, $controller, $method, $rulesToApply]);
     }
-
-    protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
+    protected function fetchUrlParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
     {
-        return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]);
+        return $this->iterateThroughStrategies('urlParameters', $context, [$route, $controller, $method, $rulesToApply]);
     }
 
     protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
@@ -103,6 +108,12 @@ class Generator
         return $this->iterateThroughStrategies('queryParameters', $context, [$route, $controller, $method, $rulesToApply]);
     }
 
+    protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
+    {
+        return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]);
+    }
+
+
     protected function fetchResponses(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
     {
         $responses = $this->iterateThroughStrategies('responses', $context, [$route, $controller, $method, $rulesToApply]);
@@ -124,12 +135,15 @@ class Generator
             'metadata' => [
                 \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class,
             ],
-            'bodyParameters' => [
-                \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+            'urlParameters' => [
+                \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class,
             ],
             'queryParameters' => [
                 \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class,
             ],
+            'bodyParameters' => [
+                \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+            ],
             'responses' => [
                 \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class,
                 \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class,

+ 2 - 2
src/Tools/Traits/DocBlockParamHelpers.php

@@ -33,8 +33,8 @@ trait DocBlockParamHelpers
     protected function parseParamDescription(string $description, string $type)
     {
         $example = null;
-        if (preg_match('/(.*)\s+Example:\s*(.+)\s*/', $description, $content)) {
-            $description = $content[1];
+        if (preg_match('/(.*)\bExample:\s*(.+)\s*/', $description, $content)) {
+            $description = trim($content[1]);
 
             // examples are parsed as strings by default, we need to cast them properly
             $example = $this->castToType($content[2], $type);