浏览代码

First implementation of plugin architecture

- Split route processing into stages: metadata, bodyParameters, queryParameters, responses
- Provide tool to fetch route docblocks (with caching)
shalvah 5 年之前
父节点
当前提交
67c61fef8d

+ 17 - 0
config/apidoc.php

@@ -1,5 +1,7 @@
 <?php
 
+use Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks;
+
 return [
 
     /*
@@ -179,6 +181,21 @@ return [
         ],
     ],
 
+    'strategies' => [
+        'metadata' => [
+            GetFromDocBlocks::class,
+        ],
+        'bodyParameters' => [
+
+        ],
+        'queryParameters' => [
+
+        ],
+        'responses' => [
+
+        ],
+    ],
+
     /*
      * Custom logo path. The logo will be copied from this location
      * during the generate process. Set this to false to use the default logo.

+ 2 - 2
src/Commands/GenerateDocumentation.php

@@ -247,7 +247,7 @@ class GenerateDocumentation extends Command
      */
     private function isValidRoute(Route $route)
     {
-        $action = Utils::getRouteActionUses($route->getAction());
+        $action = Utils::getRouteClassAndMethodNames($route->getAction());
         if (is_array($action)) {
             $action = implode('@', $action);
         }
@@ -264,7 +264,7 @@ class GenerateDocumentation extends Command
      */
     private function isRouteVisibleForDocumentation(array $action)
     {
-        list($class, $method) = Utils::getRouteActionUses($action);
+        list($class, $method) = Utils::getRouteClassAndMethodNames($action);
         $reflection = new ReflectionClass($class);
 
         if (! $reflection->hasMethod($method)) {

+ 109 - 0
src/Strategies/Metadata/GetFromDocBlocks.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace Mpociot\ApiDoc\Strategies\Metadata;
+
+use ReflectionClass;
+use ReflectionMethod;
+use Illuminate\Routing\Route;
+use Mpociot\Reflection\DocBlock;
+use Mpociot\Reflection\DocBlock\Tag;
+use Mpociot\ApiDoc\Tools\DocumentationConfig;
+use Mpociot\ApiDoc\Tools\RouteDocBlocker;
+
+class GetFromDocBlocks
+{
+
+    public $config;
+
+    public function __construct(DocumentationConfig $config)
+    {
+        $this->config = $config;
+    }
+
+    public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method)
+    {
+        $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route);
+        /** @var DocBlock $methodDocBlock */
+        $methodDocBlock = $docBlocks['method'];
+
+        list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroupDescriptionAndTitle($methodDocBlock, $docBlocks['class']);
+
+        return [
+                'groupName' => $routeGroupName,
+                'groupDescription' => $routeGroupDescription,
+                'title' => $routeTitle ?: $methodDocBlock->getShortDescription(),
+                'description' => $methodDocBlock->getLongDescription()->getContents(),
+                'authenticated' => $this->getAuthStatusFromDocBlock($methodDocBlock->getTags()),
+        ];
+    }
+
+    /**
+     * @param array $tags Tags in the method doc block
+     *
+     * @return bool
+     */
+    protected function getAuthStatusFromDocBlock(array $tags)
+    {
+        $authTag = collect($tags)
+            ->first(function ($tag) {
+                return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated';
+            });
+
+        return (bool) $authTag;
+    }
+
+    /**
+     * @param DocBlock $methodDocBlock
+     *
+     * @param DocBlock $controllerDocBlock
+     *
+     * @return array The route group name, the group description, ad the route title
+     */
+    protected function getRouteGroupDescriptionAndTitle(DocBlock $methodDocBlock, DocBlock $controllerDocBlock)
+    {
+        // @group tag on the method overrides that on the controller
+        if (! empty($methodDocBlock->getTags())) {
+            foreach ($methodDocBlock->getTags() as $tag) {
+                if ($tag->getName() === 'group') {
+                    $routeGroupParts = explode("\n", trim($tag->getContent()));
+                    $routeGroupName = array_shift($routeGroupParts);
+                    $routeGroupDescription = trim(implode("\n", $routeGroupParts));
+
+                    // If the route has no title (the methodDocBlock's "short description"),
+                    // we'll assume the routeGroupDescription is actually the title
+                    // Something like this:
+                    // /**
+                    //   * Fetch cars. <-- This is route title.
+                    //   * @group Cars <-- This is group name.
+                    //   * APIs for cars. <-- This is group description (not required).
+                    //   **/
+                    // VS
+                    // /**
+                    //   * @group Cars <-- This is group name.
+                    //   * Fetch cars. <-- This is route title, NOT group description.
+                    //   **/
+
+                    // BTW, this is a spaghetti way of doing this.
+                    // It shall be refactored soon. Deus vult!💪
+                    if (empty($methodDocBlock->getShortDescription())) {
+                        return [$routeGroupName, '', $routeGroupDescription];
+                    }
+
+                    return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()];
+                }
+            }
+        }
+
+            foreach ($controllerDocBlock->getTags() as $tag) {
+                if ($tag->getName() === 'group') {
+                    $routeGroupParts = explode("\n", trim($tag->getContent()));
+                    $routeGroupName = array_shift($routeGroupParts);
+                    $routeGroupDescription = implode("\n", $routeGroupParts);
+
+                    return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()];
+                }
+            }
+
+        return [$this->config->get('default_group'), '', $methodDocBlock->getShortDescription()];
+    }
+}

+ 34 - 107
src/Tools/Generator.php

@@ -53,15 +53,21 @@ class Generator
      */
     public function processRoute(Route $route, array $rulesToApply = [])
     {
-        list($class, $method) = Utils::getRouteActionUses($route->getAction());
-        $controller = new ReflectionClass($class);
-        $method = $controller->getMethod($method);
-
-        $docBlock = $this->parseDocBlock($method);
-        list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroup($controller, $docBlock);
-        $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']);
-        $queryParameters = $this->getQueryParameters($method, $docBlock['tags']);
-        $content = ResponseResolver::getResponse($route, $docBlock['tags'], [
+        list($controllerName, $methodName) = Utils::getRouteClassAndMethodNames($route->getAction());
+        $controller = new ReflectionClass($controllerName);
+        $method = $controller->getMethod($methodName);
+
+        $metadata = $this->fetchMetadata($controller, $method, $route);
+        // $this->fetchBodyParameters();
+        // $this->fetchQueryParameters();
+        // $this->fetchResponse();
+
+        $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route);
+        /** @var DocBlock $methodDocBlock */
+        $methodDocBlock = $docBlocks['method'];
+        $bodyParameters = $this->getBodyParameters($method, $methodDocBlock->getTags());
+        $queryParameters = $this->getQueryParameters($method, $methodDocBlock->getTags());
+        $content = ResponseResolver::getResponse($route, $methodDocBlock->getTags(), [
             'rules' => $rulesToApply,
             'body' => $bodyParameters,
             'query' => $queryParameters,
@@ -69,10 +75,6 @@ class Generator
 
         $parsedRoute = [
             'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
-            'groupName' => $routeGroupName,
-            'groupDescription' => $routeGroupDescription,
-            'title' => $routeTitle ?: $docBlock['short'],
-            'description' => $docBlock['long'],
             'methods' => $this->getMethods($route),
             'uri' => $this->getUri($route),
             'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])),
@@ -80,15 +82,30 @@ class Generator
             'bodyParameters' => $bodyParameters,
             'cleanBodyParameters' => $this->cleanParams($bodyParameters),
             'cleanQueryParameters' => $this->cleanParams($queryParameters),
-            'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']),
             'response' => $content,
             'showresponse' => ! empty($content),
         ];
         $parsedRoute['headers'] = $rulesToApply['headers'] ?? [];
+        $parsedRoute += $metadata;
 
         return $parsedRoute;
     }
 
+    protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route)
+    {
+        $metadataStrategies = $this->config->get('strategies.metadata', []);
+        $results = [];
+
+        foreach ($metadataStrategies as $strategyClass) {
+            $strategy = new $strategyClass($this->config);
+            $results = $strategy($route, $controller, $method);
+            if (count($results)) {
+                break;
+            }
+        }
+        return count($results) ? $results : [];
+    }
+
     protected function getBodyParameters(ReflectionMethod $method, array $tags)
     {
         foreach ($method->getParameters() as $param) {
@@ -150,7 +167,7 @@ class Generator
                 }
 
                 $type = $this->normalizeParameterType($type);
-                list($description, $example) = $this->parseDescription($description, $type);
+                list($description, $example) = $this->parseParamDescription($description, $type);
                 $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example;
 
                 return [$name => compact('type', 'description', 'required', 'value')];
@@ -225,7 +242,7 @@ class Generator
                     $required = trim($required) == 'required' ? true : false;
                 }
 
-                list($description, $value) = $this->parseDescription($description, 'string');
+                list($description, $value) = $this->parseParamDescription($description, 'string');
                 if (is_null($value) && ! $this->shouldExcludeExample($tag)) {
                     $value = str_contains($description, ['number', 'count', 'page'])
                         ? $this->generateDummyValue('integer')
@@ -238,96 +255,6 @@ class Generator
         return $parameters;
     }
 
-    /**
-     * @param array $tags
-     *
-     * @return bool
-     */
-    protected function getAuthStatusFromDocBlock(array $tags)
-    {
-        $authTag = collect($tags)
-            ->first(function ($tag) {
-                return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated';
-            });
-
-        return (bool) $authTag;
-    }
-
-    /**
-     * @param ReflectionMethod $method
-     *
-     * @return array
-     */
-    protected function parseDocBlock(ReflectionMethod $method)
-    {
-        $comment = $method->getDocComment();
-        $phpdoc = new DocBlock($comment);
-
-        return [
-            'short' => $phpdoc->getShortDescription(),
-            'long' => $phpdoc->getLongDescription()->getContents(),
-            'tags' => $phpdoc->getTags(),
-        ];
-    }
-
-    /**
-     * @param ReflectionClass $controller
-     * @param array $methodDocBlock
-     *
-     * @return array The route group name, the group description, ad the route title
-     */
-    protected function getRouteGroup(ReflectionClass $controller, array $methodDocBlock)
-    {
-        // @group tag on the method overrides that on the controller
-        if (! empty($methodDocBlock['tags'])) {
-            foreach ($methodDocBlock['tags'] as $tag) {
-                if ($tag->getName() === 'group') {
-                    $routeGroupParts = explode("\n", trim($tag->getContent()));
-                    $routeGroupName = array_shift($routeGroupParts);
-                    $routeGroupDescription = trim(implode("\n", $routeGroupParts));
-
-                    // If the route has no title (aka "short"),
-                    // we'll assume the routeGroupDescription is actually the title
-                    // Something like this:
-                    // /**
-                    //   * Fetch cars. <-- This is route title.
-                    //   * @group Cars <-- This is group name.
-                    //   * APIs for cars. <-- This is group description (not required).
-                    //   **/
-                    // VS
-                    // /**
-                    //   * @group Cars <-- This is group name.
-                    //   * Fetch cars. <-- This is route title, NOT group description.
-                    //   **/
-
-                    // BTW, this is a spaghetti way of doing this.
-                    // It shall be refactored soon. Deus vult!💪
-                    if (empty($methodDocBlock['short'])) {
-                        return [$routeGroupName, '', $routeGroupDescription];
-                    }
-
-                    return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']];
-                }
-            }
-        }
-
-        $docBlockComment = $controller->getDocComment();
-        if ($docBlockComment) {
-            $phpdoc = new DocBlock($docBlockComment);
-            foreach ($phpdoc->getTags() as $tag) {
-                if ($tag->getName() === 'group') {
-                    $routeGroupParts = explode("\n", trim($tag->getContent()));
-                    $routeGroupName = array_shift($routeGroupParts);
-                    $routeGroupDescription = implode("\n", $routeGroupParts);
-
-                    return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']];
-                }
-            }
-        }
-
-        return [$this->config->get(('default_group')), '', $methodDocBlock['short']];
-    }
-
     private function normalizeParameterType($type)
     {
         $typeMap = [
@@ -383,7 +310,7 @@ class Generator
      *
      * @return array The description and included example.
      */
-    private function parseDescription(string $description, string $type)
+    private function parseParamDescription(string $description, string $type)
     {
         $example = null;
         if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) {

+ 56 - 0
src/Tools/RouteDocBlocker.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tools;
+
+use ReflectionClass;
+use Illuminate\Routing\Route;
+use Mpociot\Reflection\DocBlock;
+
+class RouteDocBlocker
+{
+
+    public static $docBlocks = [];
+
+    public static function getDocBlocksFromRoute(Route $route)
+    {
+        list($className, $methodName) = Utils::getRouteClassAndMethodNames($route);
+        $docBlocks = self::getCachedDocBlock($route, $className, $methodName);
+        if ($docBlocks) {
+            return $docBlocks;
+        }
+
+        $class = new ReflectionClass($className);
+
+        if (! $class->hasMethod($methodName)) {
+            throw new \Exception("Error while fetching docblock for route: Class $className does not contain method $methodName");
+        }
+
+        $docBlocks = [
+            'method' => new DocBlock($class->getMethod($methodName)->getDocComment() ?: ''),
+            'class' => new DocBlock($class->getDocComment() ?: '')
+        ];
+        self::cacheDocBlocks($route, $className, $methodName, $docBlocks);
+        return $docBlocks;
+    }
+
+    protected static function getCachedDocBlock(Route $route, string $className, string $methodName)
+    {
+        $routeId = self::getRouteId($route, $className, $methodName);
+        return self::$docBlocks[$routeId] ?? null;
+    }
+
+    protected static function cacheDocBlocks(Route $route, string $className, string $methodName, array $docBlocks)
+    {
+        $routeId = self::getRouteId($route, $className, $methodName);
+        self::$docBlocks[$routeId] = $docBlocks;
+    }
+
+    private static function getRouteId(Route $route, string $className, string $methodName)
+    {
+        return $route->uri()
+            .':'
+            .implode(array_diff($route->methods(), ['HEAD']))
+            .$className
+            .$methodName;
+    }
+}

+ 4 - 2
src/Tools/Utils.php

@@ -19,12 +19,14 @@ class Utils
     }
 
     /**
-     * @param array $action
+     * @param array|Route $routeOrAction
      *
      * @return array|null
      */
-    public static function getRouteActionUses(array $action)
+    public static function getRouteClassAndMethodNames($routeOrAction)
     {
+        $action = $routeOrAction instanceof Route ? $routeOrAction->getAction() : $routeOrAction;
+
         if ($action['uses'] !== null) {
             if (is_array($action['uses'])) {
                 return $action['uses'];