-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Symfony 6 support * Symfony 6 support * Use double instead of single colon separator for routes * Update tests * Disable Route Describer for nelmio/api-doc-bundle v4+ * OpenApi Endpoints Describer * * Update README * Add symfony/routing (Symfony 4.1: Deprecated the bundle notation) * Add GitHub Actions * Update dependencies Update workflow * Keep only OpenApi\Annotations\OpenApi describer even if EXSyst\Component\Swagger\Swagger installed * Fix Warning: Attempt to read property "schemas" on string
- Loading branch information
1 parent
6769e57
commit f9ae7c2
Showing
12 changed files
with
335 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# This workflow uses actions that are not certified by GitHub. | ||
# They are provided by a third-party and are governed by | ||
# separate terms of service, privacy policy, and support | ||
# documentation. | ||
|
||
name: Symfony | ||
|
||
on: | ||
push: | ||
branches: ['master'] | ||
tags: ['*'] | ||
pull_request: | ||
branches: ['*'] | ||
|
||
permissions: | ||
contents: read | ||
|
||
jobs: | ||
symfony-tests: | ||
runs-on: ${{ matrix.operating-system }} | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
operating-system: [ 'ubuntu-latest' ] | ||
php-versions: [ '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] | ||
phpunit-versions: [ 'latest' ] | ||
steps: | ||
- name: Setup PHP | ||
uses: auto1-oss/setup-php@v2 | ||
with: | ||
php-version: ${{ matrix.php-versions }} | ||
tools: composer:2.2.23 | ||
extensions: mbstring, intl | ||
ini-values: post_max_size=256M, max_execution_time=180 | ||
coverage: xdebug | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
- name: Checkout repository | ||
uses: actions/checkout@v3 | ||
- name: Composer update on php ${{ matrix.php }} and symfony | ||
run: composer update --prefer-dist --no-progress | ||
- name: Execute tests (Unit and Feature tests) via PHPUnit | ||
run: vendor/bin/phpunit | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Auto1\ServiceAPIHandlerBundle\ApiDoc; | ||
|
||
class Components extends \OpenApi\Annotations\Components | ||
{ | ||
public function validate(array $stack = [], array $skip = [], string $ref = '', $context = null): bool | ||
{ | ||
/** | ||
* It's the only way to avoid the error that different responses for different Paths have the same response code | ||
* <User Warning: Multiple @OA\Response() with the same response="200"> | ||
* https://github.com/zircote/swagger-php/blob/master/src/Annotations/AbstractAnnotation.php#L489 | ||
*/ | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
<?php | ||
|
||
namespace Auto1\ServiceAPIHandlerBundle\ApiDoc; | ||
|
||
use Auto1\ServiceAPIComponentsBundle\Service\Endpoint\EndpointInterface; | ||
use Auto1\ServiceAPIComponentsBundle\Service\Endpoint\EndpointRegistryInterface; | ||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; | ||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; | ||
use Nelmio\ApiDocBundle\Model\Model; | ||
use Nelmio\ApiDocBundle\RouteDescriber\RouteDescriberInterface; | ||
use Nelmio\ApiDocBundle\RouteDescriber\RouteDescriberTrait; | ||
use OpenApi\Annotations\Get; | ||
use OpenApi\Annotations\Items; | ||
use OpenApi\Annotations\JsonContent; | ||
use OpenApi\Annotations\OpenApi; | ||
use OpenApi\Annotations\Operation; | ||
use OpenApi\Annotations\Parameter; | ||
use OpenApi\Annotations\PathItem; | ||
use OpenApi\Annotations\RequestBody; | ||
use OpenApi\Annotations\MediaType; | ||
use OpenApi\Annotations\Response; | ||
use OpenApi\Annotations\Schema; | ||
use OpenApi\Context; | ||
use OpenApi\Generator; | ||
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; | ||
use Symfony\Component\PropertyInfo\Type; | ||
use Symfony\Component\Routing\Route; | ||
|
||
/** | ||
* Class EndpointRouteDescriber. | ||
* | ||
* @package Auto1\ServiceAPIHandlerBundle\ApiDoc | ||
*/ | ||
class OpenApiEndpointRouteDescriber implements RouteDescriberInterface, ModelRegistryAwareInterface | ||
{ | ||
use RouteDescriberTrait; | ||
use ModelRegistryAwareTrait; | ||
|
||
private const MEDIA_TYPE = 'application/json'; | ||
|
||
/** | ||
* @var EndpointRegistryInterface | ||
*/ | ||
private $endpointRegistry; | ||
|
||
/** | ||
* @var string[] | ||
*/ | ||
private $controllerToRequestMapping; | ||
|
||
/** | ||
* @var PropertyInfoExtractorInterface | ||
*/ | ||
private $propertyExtractor; | ||
|
||
public function __construct( | ||
EndpointRegistryInterface $endpointRegistry, | ||
array $controllerToRequestMapping, | ||
PropertyInfoExtractorInterface $propertyExtractor | ||
) { | ||
$this->endpointRegistry = $endpointRegistry; | ||
$this->controllerToRequestMapping = $controllerToRequestMapping; | ||
$this->propertyExtractor = $propertyExtractor; | ||
} | ||
|
||
public function describe(OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod) | ||
{ | ||
$endpoint = $this->getEndpoint($route); | ||
|
||
if (!$endpoint instanceof EndpointInterface) { | ||
return; | ||
} | ||
|
||
$operation = $this->getOperations($api, $route)[0] ?? null; | ||
if (!$operation instanceof Operation) { | ||
return; | ||
} | ||
|
||
$this->fillEndpointTags($operation, $reflectionMethod->getDeclaringClass()); | ||
$this->fillEndpointResponse($operation, $endpoint); | ||
$this->fillEndpointParameters($operation, $route, $endpoint); | ||
|
||
$components = $api->components; | ||
|
||
$properties = []; | ||
if ($components !== Generator::UNDEFINED) { | ||
$properties = [ | ||
'schemas' => $components->schemas , | ||
'responses' => $components->responses, | ||
'parameters' => $components->parameters, | ||
'examples' => $components->examples, | ||
'requestBodies' => $components->requestBodies, | ||
'headers' => $components->headers, | ||
'securitySchemes' => $components->securitySchemes, | ||
'links' => $components->links, | ||
'callbacks' => $components->callbacks, | ||
]; | ||
} | ||
|
||
$api->components = new Components($properties); | ||
} | ||
|
||
private function getEndpoint(Route $route) | ||
{ | ||
$controller = $route->getDefault('_controller'); | ||
|
||
if (!\array_key_exists($controller, $this->controllerToRequestMapping)) { | ||
return null; | ||
} | ||
|
||
$request = $this->controllerToRequestMapping[$controller]; | ||
$endpoint = $this->endpointRegistry->getEndpoint(new $request); | ||
|
||
return $endpoint; | ||
} | ||
|
||
private function fillEndpointResponse(Operation $operation, EndpointInterface $endpoint) | ||
{ | ||
$isArrayResponse = false; | ||
$responseClass = $endpoint->getResponseClass(); | ||
if (!$responseClass) { | ||
return; | ||
} | ||
|
||
if (strpos($responseClass, '[]') !== false) { | ||
$responseClass = str_replace('[]', '', $responseClass); | ||
$isArrayResponse = true; | ||
} | ||
if (!class_exists($responseClass)) { | ||
return; | ||
} | ||
|
||
$model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $responseClass)); | ||
$ref = $this->modelRegistry->register($model); | ||
|
||
if ($isArrayResponse) { | ||
$schema = new Items(['type' => 'array', 'items' => new Items(['ref' => $ref])]); | ||
} else { | ||
$schema = new Schema(['schema' => $ref, 'ref' => $ref]); | ||
} | ||
$content = [ | ||
new MediaType([ | ||
'mediaType' => self::MEDIA_TYPE, | ||
'schema' => $schema, | ||
]), | ||
]; | ||
|
||
$response = new Response([ | ||
'description' => 'OK', | ||
'response' => 200, | ||
'content' => $content, | ||
]); | ||
$operation->responses = [$response]; | ||
} | ||
|
||
private function fillEndpointTags(Operation $operation, \ReflectionClass $reflectionClass) | ||
{ | ||
$className = $reflectionClass->getShortName(); | ||
$classTag = strtolower(preg_replace('/([a-zA-Z0-9])(?=[A-Z])/', '$1-', $className)); | ||
|
||
$operation->tags = [$classTag]; | ||
} | ||
|
||
private function fillEndpointParameters( | ||
Operation $operation, | ||
Route $route, | ||
EndpointInterface $endpoint | ||
) { | ||
if (!class_exists($endpoint->getRequestClass())) { | ||
return; | ||
} | ||
|
||
$routeParameters = $route->compile()->getPathVariables(); | ||
$dtoParameters = $this->propertyExtractor->getProperties($endpoint->getRequestClass()); | ||
|
||
//When all parameters are available in the route parameters | ||
if (null === $dtoParameters || count(array_diff($dtoParameters, $routeParameters)) == 0) { | ||
return; | ||
} | ||
|
||
/** | ||
* To avoid this error but also provide request body fields this hack is required | ||
* <The PropertyInfo component was not able to guess the type of ArrayObject::$arrayCopy> | ||
*/ | ||
if (is_subclass_of($endpoint->getRequestClass(), \ArrayObject::class)) { | ||
$type = new Type(Type::BUILTIN_TYPE_OBJECT, false, \stdClass::class); | ||
} else { | ||
$type = new Type(Type::BUILTIN_TYPE_OBJECT, false, $endpoint->getRequestClass()); | ||
} | ||
$ref = $this->modelRegistry->register(new Model($type)); | ||
|
||
$operation->requestBody = new RequestBody([ | ||
'request' => $endpoint->getRequestClass(), | ||
'content' => [ | ||
new MediaType([ | ||
'mediaType' => self::MEDIA_TYPE, | ||
'schema' => new Schema(['ref' => $ref, 'schema' => $ref]), | ||
]), | ||
], | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.