From 827d5ea15225646f82c2b68b26dfa714f7f4b7ca Mon Sep 17 00:00:00 2001 From: Ener-Getick Date: Thu, 30 Jun 2016 23:30:37 +0200 Subject: [PATCH] Initialize --- .gitignore | 3 + .php_cs | 27 ++++ .travis.yml | 29 ++++ ApiDocGenerator.php | 41 ++++++ Extractor/ExtractorInterface.php | 19 +++ Extractor/RouteExtractor.php | 113 ++++++++++++++++ .../Routing/NelmioAnnotationExtractor.php | 125 ++++++++++++++++++ Extractor/Routing/RouteExtractorInterface.php | 20 +++ Extractor/Routing/RouteExtractorTrait.php | 43 ++++++ Extractor/Routing/RouteMetadataExtractor.php | 34 +++++ composer.json | 33 +++++ phpunit.xml.dist | 26 ++++ 12 files changed, 513 insertions(+) create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 ApiDocGenerator.php create mode 100644 Extractor/ExtractorInterface.php create mode 100644 Extractor/RouteExtractor.php create mode 100644 Extractor/Routing/NelmioAnnotationExtractor.php create mode 100644 Extractor/Routing/RouteExtractorInterface.php create mode 100644 Extractor/Routing/RouteExtractorTrait.php create mode 100644 Extractor/Routing/RouteMetadataExtractor.php create mode 100644 composer.json create mode 100644 phpunit.xml.dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80ae21a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/* +composer.lock +.php_cs.cache diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..367c4b5 --- /dev/null +++ b/.php_cs @@ -0,0 +1,27 @@ +in(__DIR__) +; + +$header = <<level(FixerInterface::SYMFONY_LEVEL) + ->fixers(array('align_double_arrow', 'header_comment')) + ->finder($finder) + ->setUsingCache(true) +; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fe79f81 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: php + +php: + - 5.5 + - 5.6 + - 7.0 + - hhvm + +sudo: false + +cache: + directories: + - $HOME/.composer/cache + +branches: + only: + - master + - /^\d+\.\d+$/ + +matrix: + fast_finish: true + include: + - php: 5.5 + env: COMPOSER_FLAGS="--prefer-lowest" + +before_install: + - composer self-update + +install: composer update $COMPOSER_FLAGS --prefer-dist diff --git a/ApiDocGenerator.php b/ApiDocGenerator.php new file mode 100644 index 0000000..894c54f --- /dev/null +++ b/ApiDocGenerator.php @@ -0,0 +1,41 @@ +extractors = $extractors; + } + + /** + * @return Swagger + */ + public function extract() + { + $swagger = new Swagger(); + foreach ($this->extractors as $extractor) { + $extractor->extractIn($swagger); + } + + return $swagger; + } +} diff --git a/Extractor/ExtractorInterface.php b/Extractor/ExtractorInterface.php new file mode 100644 index 0000000..fb2eb58 --- /dev/null +++ b/Extractor/ExtractorInterface.php @@ -0,0 +1,19 @@ +router = $router; + $this->controllerNameParser = $controllerNameParser; + $this->routeExtractors = $routeExtractors; + } + + /** + * @return Swagger + */ + public function extract() + { + if (0 === count($this->routeExtractors)) { + return; + } + + $swagger = new Swagger(); + foreach ($this->getRoutes() as $route) { + // if able to resolve the controller + if ($method = $this->getReflectionMethod($route->getDefault('_controller'))) { + // Extract as many informations as possible about this route + foreach ($this->routeExtractors as $extractor) { + $extractor->extractIn($swagger, $route, $method); + } + } + } + + return $swagger; + } + + /** + * Return a list of route to inspect. + * + * @return Route[] An array of routes + */ + private function getRoutes() + { + return $this->router->getRouteCollection()->all(); + } + + /** + * Returns the ReflectionMethod for the given controller string. + * + * @param string $controller + * + * @return \ReflectionMethod|null + */ + private function getReflectionMethod($controller) + { + if (false === strpos($controller, '::') && 2 === substr_count($controller, ':')) { + $controller = $this->controllerNameParser->parse($controller); + } + + if (preg_match('#(.+)::([\w]+)#', $controller, $matches)) { + $class = $matches[1]; + $method = $matches[2]; + } elseif (class_exists($controller)) { + $class = $controller; + $method = '__invoke'; + } else { + if (preg_match('#(.+):([\w]+)#', $controller, $matches)) { + $controller = $matches[1]; + $method = $matches[2]; + } + + if ($this->container->has($controller)) { + if (class_exists(ClassUtils::class)) { + $class = ClassUtils::getRealClass(get_class($this->container->get($controller))); + } + + if (!isset($method) && method_exists($class, '__invoke')) { + $method = '__invoke'; + } + } + } + + if (isset($class) && isset($method)) { + try { + return new \ReflectionMethod($class, $method); + } catch (\ReflectionException $e) { + } + } + } +} diff --git a/Extractor/Routing/NelmioAnnotationExtractor.php b/Extractor/Routing/NelmioAnnotationExtractor.php new file mode 100644 index 0000000..896823f --- /dev/null +++ b/Extractor/Routing/NelmioAnnotationExtractor.php @@ -0,0 +1,125 @@ +annotationReader = $annotationReader; + $this->nelmioLoaded = class_exists(ApiDoc::class); + } + + public function extractIn(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod) + { + if (!$this->nelmioLoaded) { + return; + } + + $annotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, ApiDoc::class); + // some fields aren't available otherwise + $annotationArray = $annotation->toArray(); + if (null === $annotation) { + return; + } + + foreach ($this->getOperations($api, $route) as $operation) { + if ($annotation->getDescription()) { + $operation->setDescription($annotation->getDescription()); + } + if (null !== $annotation->getDeprecated()) { + $operation->setDeprecated($operation->getDeprecated || $annotation->getDeprecated()); + } + + // Request parameters + foreach ($annotation->getParameters() as $name => $configuration) { + $parameter = $operation->getParameters()->get($name, 'formData'); + if (isset($configuration['required'])) { + $parameter->setRequired($parameter->getRequired() || $configuration['required']); + } + + $this->configureParameter($parameter, $configuration); + } + + // Query parameters + foreach ($annotation->getRequirements() as $name => $configuration) { + $parameter = $operation->getParameters()->get($name, 'query'); + $parameter->setRequired(true); + + $this->configureParameter($parameter, $configuration); + } + foreach ($annotation->getFilters() as $name => $configuration) { + $parameter = $operation->getParameters()->get($name, 'query'); + $this->configureParameter($parameter, $configuration); + } + + // External docs + if (isset($annotationArray['link'])) { + $operation->getExternalDocs()->setUrl($annotationArray['link']); + } + + // Responses + if (isset($annotationArray['statusCodes'])) { + $responses = $operation->getResponses(); + foreach ($annotationArray['statusCodes'] as $statusCode => $description) { + $response = $responses->get($statusCode); + $response->setDescription($description); + } + } + } + } + + private function configureParameter(Parameter $parameter, array $configuration) + { + $dataType = null; + if (isset($configuration['dataType'])) { + $dataType = $configuration['dataType']; + } elseif ($configuration['requirement']) { + $dataType = $configuration['requirement']; + } + + if ('[]' === substr($requirement, -2)) { + $parameter->setType('array'); + $items = $parameter; + do { + $items->setCollectionFormat('multi'); + $requirement = substr($requirement, 0, -2); + + $items = $items->getItems(); + } while ('[]' === substr($requirement, -2)); + + $items->setType(Swagger::T_STRING); + $items->setFormat($requirement); + } else { + $parameter->setType(Swagger::T_STRING); + $parameter->setFormat($requirement); + } + + if (isset($configuration['description'])) { + $parameter->setDescription($configuration['description']); + } + if (isset($configuration['default'])) { + $parameter->setDefault($configuration['default']); + } + } +} diff --git a/Extractor/Routing/RouteExtractorInterface.php b/Extractor/Routing/RouteExtractorInterface.php new file mode 100644 index 0000000..c052a39 --- /dev/null +++ b/Extractor/Routing/RouteExtractorInterface.php @@ -0,0 +1,20 @@ +getPaths()->get($route->getPath()); + $methods = $route->getMethods() ?: Swagger::$METHODS; + foreach ($methods as $method) { + $method = strtolower($method); + if (!in_array($method, Swagger::$METHODS)) { + continue; + } + + $operations[] = $path->getOperation($method); + } + + return $operations; + } +} diff --git a/Extractor/Routing/RouteMetadataExtractor.php b/Extractor/Routing/RouteMetadataExtractor.php new file mode 100644 index 0000000..3805d24 --- /dev/null +++ b/Extractor/Routing/RouteMetadataExtractor.php @@ -0,0 +1,34 @@ +getOperations($api, $route) as $operation) { + $operation->getSchemes()->addAll($route->getSchemes()); + + foreach ($route->getRequirements() as $parameterName => $requirement) { + $parameter = $operation->getParameters()->get($parameterName, 'path'); + $parameter->setRequired(true); + $parameter->setType(swagger\Swagger::T_STRING); + $parameter->setFormat($requirement); + } + } + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f2bd605 --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "exsyst/api-doc-bundle", + "type": "symfony-bundle", + "description": "[WIP] Generates Swagger docs from several sources", + "license": "MIT", + "authors": [ + { + "name": "EXSyst" + } + ], + "require": { + "php": ">=5.5", + "symfony/framework-bundle": "^2.7|^3.0", + "gossi/swagger": "^0.2" + }, + "require-dev": { + "nelmio/api-doc-bundle": "^2.0", + "symfony/phpunit-bridge": "^2.7|^3.0" + }, + "suggest": { + "nelmio/api-doc-bundle": "For using the ApiDoc annotation." + }, + "autoload": { + "psr-4": { + "EXSyst\\Bundle\\ApiDocBundle\\": "" + } + }, + "extra": { + "branch-alias": { + "dev-master": "0.1.x-dev" + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..760bc87 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + + + + + ./Tests/ + + + + + ./ + + ./vendor + ./Tests/ + + + +