vendor/api-platform/core/src/Core/OpenApi/Factory/OpenApiFactory.php line 341

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the API Platform project.
  4.  *
  5.  * (c) Kévin Dunglas <dunglas@gmail.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. declare(strict_types=1);
  11. namespace ApiPlatform\Core\OpenApi\Factory;
  12. use ApiPlatform\Core\Api\FilterLocatorTrait;
  13. use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
  14. use ApiPlatform\Core\Api\OperationType;
  15. use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
  16. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  17. use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  18. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  19. use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
  20. use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
  21. use ApiPlatform\JsonSchema\Schema;
  22. use ApiPlatform\JsonSchema\TypeFactoryInterface;
  23. use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
  24. use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
  25. use ApiPlatform\OpenApi\Model;
  26. use ApiPlatform\OpenApi\Model\ExternalDocumentation;
  27. use ApiPlatform\OpenApi\Model\PathItem;
  28. use ApiPlatform\OpenApi\OpenApi;
  29. use ApiPlatform\OpenApi\Options;
  30. use ApiPlatform\PathResolver\OperationPathResolverInterface;
  31. use ApiPlatform\State\Pagination\PaginationOptions;
  32. use Psr\Container\ContainerInterface;
  33. use Symfony\Component\PropertyInfo\Type;
  34. /**
  35.  * Generates an Open API v3 specification.
  36.  */
  37. final class OpenApiFactory implements OpenApiFactoryInterface
  38. {
  39.     use FilterLocatorTrait;
  40.     public const BASE_URL 'base_url';
  41.     public const OPENAPI_DEFINITION_NAME 'openapi_definition_name';
  42.     private $resourceNameCollectionFactory;
  43.     private $resourceMetadataFactory;
  44.     private $propertyNameCollectionFactory;
  45.     private $propertyMetadataFactory;
  46.     private $operationPathResolver;
  47.     private $subresourceOperationFactory;
  48.     private $formats;
  49.     private $jsonSchemaFactory;
  50.     private $jsonSchemaTypeFactory;
  51.     private $openApiOptions;
  52.     private $paginationOptions;
  53.     private $identifiersExtractor;
  54.     public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactoryResourceMetadataFactoryInterface $resourceMetadataFactoryPropertyNameCollectionFactoryInterface $propertyNameCollectionFactoryPropertyMetadataFactoryInterface $propertyMetadataFactorySchemaFactoryInterface $jsonSchemaFactoryTypeFactoryInterface $jsonSchemaTypeFactoryOperationPathResolverInterface $operationPathResolverContainerInterface $filterLocatorSubresourceOperationFactoryInterface $subresourceOperationFactoryIdentifiersExtractorInterface $identifiersExtractor null, array $formats = [], Options $openApiOptions nullPaginationOptions $paginationOptions null)
  55.     {
  56.         $this->resourceNameCollectionFactory $resourceNameCollectionFactory;
  57.         $this->jsonSchemaFactory $jsonSchemaFactory;
  58.         $this->jsonSchemaTypeFactory $jsonSchemaTypeFactory;
  59.         $this->formats $formats;
  60.         $this->setFilterLocator($filterLocatortrue);
  61.         $this->resourceMetadataFactory $resourceMetadataFactory;
  62.         $this->propertyNameCollectionFactory $propertyNameCollectionFactory;
  63.         $this->propertyMetadataFactory $propertyMetadataFactory;
  64.         $this->operationPathResolver $operationPathResolver;
  65.         $this->subresourceOperationFactory $subresourceOperationFactory;
  66.         $this->identifiersExtractor $identifiersExtractor;
  67.         $this->openApiOptions $openApiOptions ?: new Options('API Platform');
  68.         $this->paginationOptions $paginationOptions ?: new PaginationOptions();
  69.     }
  70.     public function __invoke(array $context = []): OpenApi
  71.     {
  72.         $baseUrl $context[self::BASE_URL] ?? '/';
  73.         $contact null === $this->openApiOptions->getContactUrl() || null === $this->openApiOptions->getContactEmail() ? null : new Model\Contact($this->openApiOptions->getContactName(), $this->openApiOptions->getContactUrl(), $this->openApiOptions->getContactEmail());
  74.         $license null === $this->openApiOptions->getLicenseName() ? null : new Model\License($this->openApiOptions->getLicenseName(), $this->openApiOptions->getLicenseUrl());
  75.         $info = new Model\Info($this->openApiOptions->getTitle(), $this->openApiOptions->getVersion(), trim($this->openApiOptions->getDescription()), $this->openApiOptions->getTermsOfService(), $contact$license);
  76.         $servers '/' === $baseUrl || '' === $baseUrl ? [new Model\Server('/')] : [new Model\Server($baseUrl)];
  77.         $paths = new Model\Paths();
  78.         $links = [];
  79.         $schemas = new \ArrayObject();
  80.         foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
  81.             $resourceMetadata $this->resourceMetadataFactory->create($resourceClass);
  82.             // Items needs to be parsed first to be able to reference the lines from the collection operation
  83.             $this->collectPaths($resourceMetadata$resourceClassOperationType::ITEM$context$paths$links$schemas);
  84.             $this->collectPaths($resourceMetadata$resourceClassOperationType::COLLECTION$context$paths$links$schemas);
  85.             $this->collectPaths($resourceMetadata$resourceClassOperationType::SUBRESOURCE$context$paths$links$schemas);
  86.         }
  87.         $securitySchemes $this->getSecuritySchemes();
  88.         $securityRequirements = [];
  89.         foreach (array_keys($securitySchemes) as $key) {
  90.             $securityRequirements[] = [$key => []];
  91.         }
  92.         return new OpenApi(
  93.             $info,
  94.             $servers,
  95.             $paths,
  96.             new Model\Components(
  97.                 $schemas,
  98.                 new \ArrayObject(),
  99.                 new \ArrayObject(),
  100.                 new \ArrayObject(),
  101.                 new \ArrayObject(),
  102.                 new \ArrayObject(),
  103.                 new \ArrayObject($securitySchemes)
  104.             ),
  105.             $securityRequirements
  106.         );
  107.     }
  108.     private function collectPaths(ResourceMetadata $resourceMetadatastring $resourceClassstring $operationType, array $contextModel\Paths $paths, array &$links, \ArrayObject $schemas): void
  109.     {
  110.         $resourceShortName $resourceMetadata->getShortName();
  111.         $operations OperationType::COLLECTION === $operationType $resourceMetadata->getCollectionOperations() : (OperationType::ITEM === $operationType $resourceMetadata->getItemOperations() : $this->subresourceOperationFactory->create($resourceClass));
  112.         if (!$operations) {
  113.             return;
  114.         }
  115.         $rootResourceClass $resourceClass;
  116.         foreach ($operations as $operationName => $operation) {
  117.             if (OperationType::COLLECTION === $operationType && !$resourceMetadata->getItemOperations()) {
  118.                 $identifiers = [];
  119.             } else {
  120.                 $identifiers = (array) ($operation['identifiers'] ?? $resourceMetadata->getAttribute('identifiers'null === $this->identifiersExtractor ? ['id'] : $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass)));
  121.             }
  122.             if (\count($identifiers) > $resourceMetadata->getAttribute('composite_identifier'true) : false) {
  123.                 $identifiers = ['id'];
  124.             }
  125.             $resourceClass $operation['resource_class'] ?? $rootResourceClass;
  126.             $path $this->getPath($resourceShortName$operationName$operation$operationType);
  127.             $method $resourceMetadata->getTypedOperationAttribute($operationType$operationName'method''GET');
  128.             if (!\in_array($methodPathItem::$methodstrue)) {
  129.                 continue;
  130.             }
  131.             [$requestMimeTypes$responseMimeTypes] = $this->getMimeTypes($resourceClass$operationName$operationType$resourceMetadata);
  132.             $operationId $operation['openapi_context']['operationId'] ?? lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
  133.             $linkedOperationId 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM);
  134.             $pathItem $paths->getPath($path) ?: new Model\PathItem();
  135.             $forceSchemaCollection OperationType::SUBRESOURCE === $operationType ? ($operation['collection'] ?? false) : false;
  136.             $schema = new Schema('openapi');
  137.             $schema->setDefinitions($schemas);
  138.             $operationOutputSchemas = [];
  139.             foreach ($responseMimeTypes as $operationFormat) {
  140.                 $operationOutputSchema $this->jsonSchemaFactory->buildSchema($resourceClass$operationFormatSchema::TYPE_OUTPUT$operationType$operationName$schemanull$forceSchemaCollection);
  141.                 $operationOutputSchemas[$operationFormat] = $operationOutputSchema;
  142.                 $this->appendSchemaDefinitions($schemas$operationOutputSchema->getDefinitions());
  143.             }
  144.             $parameters = [];
  145.             $responses = [];
  146.             if ($operation['openapi_context']['parameters'] ?? false) {
  147.                 foreach ($operation['openapi_context']['parameters'] as $parameter) {
  148.                     $parameters[] = new Model\Parameter($parameter['name'], $parameter['in'], $parameter['description'] ?? ''$parameter['required'] ?? false$parameter['deprecated'] ?? false$parameter['allowEmptyValue'] ?? false$parameter['schema'] ?? [], $parameter['style'] ?? null$parameter['explode'] ?? false$parameter['allowReserved '] ?? false$parameter['example'] ?? null, isset($parameter['examples']) ? new \ArrayObject($parameter['examples']) : null, isset($parameter['content']) ? new \ArrayObject($parameter['content']) : null);
  149.                 }
  150.             }
  151.             // Set up parameters
  152.             if (OperationType::ITEM === $operationType) {
  153.                 foreach ($identifiers as $parameterName => $identifier) {
  154.                     $parameterName = \is_string($parameterName) ? $parameterName $identifier;
  155.                     $parameter = new Model\Parameter($parameterName'path''Resource identifier'truefalsefalse, ['type' => 'string']);
  156.                     if ($this->hasParameter($parameter$parameters)) {
  157.                         continue;
  158.                     }
  159.                     $parameters[] = $parameter;
  160.                 }
  161.                 $links[$operationId] = $this->getLink($resourceClass$operationId$path);
  162.             } elseif (OperationType::COLLECTION === $operationType && 'GET' === $method) {
  163.                 foreach (array_merge($this->getPaginationParameters($resourceMetadata$operationName), $this->getFiltersParameters($resourceMetadata$operationName$resourceClass)) as $parameter) {
  164.                     if ($this->hasParameter($parameter$parameters)) {
  165.                         continue;
  166.                     }
  167.                     $parameters[] = $parameter;
  168.                 }
  169.             } elseif (OperationType::SUBRESOURCE === $operationType) {
  170.                 foreach ($operation['identifiers'] as $parameterName => [$class$property]) {
  171.                     $parameter = new Model\Parameter($parameterName'path'$this->resourceMetadataFactory->create($class)->getShortName().' identifier'truefalsefalse, ['type' => 'string']);
  172.                     if ($this->hasParameter($parameter$parameters)) {
  173.                         continue;
  174.                     }
  175.                     $parameters[] = $parameter;
  176.                 }
  177.                 if ($operation['collection']) {
  178.                     $subresourceMetadata $this->resourceMetadataFactory->create($resourceClass);
  179.                     foreach (array_merge($this->getPaginationParameters($resourceMetadata$operationName), $this->getFiltersParameters($subresourceMetadata$operationName$resourceClass)) as $parameter) {
  180.                         if ($this->hasParameter($parameter$parameters)) {
  181.                             continue;
  182.                         }
  183.                         $parameters[] = $parameter;
  184.                     }
  185.                 }
  186.             }
  187.             // Create responses
  188.             switch ($method) {
  189.                 case 'GET':
  190.                     $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''200');
  191.                     $responseContent $this->buildContent($responseMimeTypes$operationOutputSchemas);
  192.                     $responses[$successStatus] = new Model\Response(sprintf('%s %s'$resourceShortNameOperationType::COLLECTION === $operationType 'collection' 'resource'), $responseContent);
  193.                     break;
  194.                 case 'POST':
  195.                     $responseLinks = new \ArrayObject(isset($links[$linkedOperationId]) ? [ucfirst($linkedOperationId) => $links[$linkedOperationId]] : []);
  196.                     $responseContent $this->buildContent($responseMimeTypes$operationOutputSchemas);
  197.                     $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''201');
  198.                     $responses[$successStatus] = new Model\Response(sprintf('%s resource created'$resourceShortName), $responseContentnull$responseLinks);
  199.                     $responses['400'] = new Model\Response('Invalid input');
  200.                     $responses['422'] = new Model\Response('Unprocessable entity');
  201.                     break;
  202.                 case 'PATCH':
  203.                 case 'PUT':
  204.                     $responseLinks = new \ArrayObject(isset($links[$linkedOperationId]) ? [ucfirst($linkedOperationId) => $links[$linkedOperationId]] : []);
  205.                     $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''200');
  206.                     $responseContent $this->buildContent($responseMimeTypes$operationOutputSchemas);
  207.                     $responses[$successStatus] = new Model\Response(sprintf('%s resource updated'$resourceShortName), $responseContentnull$responseLinks);
  208.                     $responses['400'] = new Model\Response('Invalid input');
  209.                     $responses['422'] = new Model\Response('Unprocessable entity');
  210.                     break;
  211.                 case 'DELETE':
  212.                     $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''204');
  213.                     $responses[$successStatus] = new Model\Response(sprintf('%s resource deleted'$resourceShortName));
  214.                     break;
  215.             }
  216.             if (OperationType::ITEM === $operationType) {
  217.                 $responses['404'] = new Model\Response('Resource not found');
  218.             }
  219.             if (!$responses) {
  220.                 $responses['default'] = new Model\Response('Unexpected error');
  221.             }
  222.             if ($contextResponses $operation['openapi_context']['responses'] ?? false) {
  223.                 foreach ($contextResponses as $statusCode => $contextResponse) {
  224.                     $responses[$statusCode] = new Model\Response($contextResponse['description'] ?? '', isset($contextResponse['content']) ? new \ArrayObject($contextResponse['content']) : null, isset($contextResponse['headers']) ? new \ArrayObject($contextResponse['headers']) : null, isset($contextResponse['links']) ? new \ArrayObject($contextResponse['links']) : null);
  225.                 }
  226.             }
  227.             $requestBody null;
  228.             if ($contextRequestBody $operation['openapi_context']['requestBody'] ?? false) {
  229.                 $requestBody = new Model\RequestBody($contextRequestBody['description'] ?? '', new \ArrayObject($contextRequestBody['content']), $contextRequestBody['required'] ?? false);
  230.             } elseif ('PUT' === $method || 'POST' === $method || 'PATCH' === $method) {
  231.                 $operationInputSchemas = [];
  232.                 foreach ($requestMimeTypes as $operationFormat) {
  233.                     $operationInputSchema $this->jsonSchemaFactory->buildSchema($resourceClass$operationFormatSchema::TYPE_INPUT$operationType$operationName$schemanull$forceSchemaCollection);
  234.                     $operationInputSchemas[$operationFormat] = $operationInputSchema;
  235.                     $this->appendSchemaDefinitions($schemas$operationInputSchema->getDefinitions());
  236.                 }
  237.                 $requestBody = new Model\RequestBody(sprintf('The %s %s resource''POST' === $method 'new' 'updated'$resourceShortName), $this->buildContent($requestMimeTypes$operationInputSchemas), true);
  238.             }
  239.             $pathItem $pathItem->{'with'.ucfirst($method)}(new Model\Operation(
  240.                 $operationId,
  241.                 $operation['openapi_context']['tags'] ?? (OperationType::SUBRESOURCE === $operationType $operation['shortNames'] : [$resourceShortName]),
  242.                 $responses,
  243.                 $operation['openapi_context']['summary'] ?? $this->getPathDescription($resourceShortName$method$operationType),
  244.                 $operation['openapi_context']['description'] ?? $this->getPathDescription($resourceShortName$method$operationType),
  245.                 isset($operation['openapi_context']['externalDocs']) ? new ExternalDocumentation($operation['openapi_context']['externalDocs']['description'] ?? null$operation['openapi_context']['externalDocs']['url']) : null,
  246.                 $parameters,
  247.                 $requestBody,
  248.                 isset($operation['openapi_context']['callbacks']) ? new \ArrayObject($operation['openapi_context']['callbacks']) : null,
  249.                 $operation['openapi_context']['deprecated'] ?? (bool) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'deprecation_reason'falsetrue),
  250.                 $operation['openapi_context']['security'] ?? null,
  251.                 $operation['openapi_context']['servers'] ?? null,
  252.                 array_filter($operation['openapi_context'] ?? [], static function ($item) {
  253.                     return preg_match('/^x-.*$/i'$item);
  254.                 }, \ARRAY_FILTER_USE_KEY)
  255.             ));
  256.             $paths->addPath($path$pathItem);
  257.         }
  258.     }
  259.     private function buildContent(array $responseMimeTypes, array $operationSchemas): \ArrayObject
  260.     {
  261.         /** @var \ArrayObject<Model\MediaType> */
  262.         $content = new \ArrayObject();
  263.         foreach ($responseMimeTypes as $mimeType => $format) {
  264.             $content[$mimeType] = new Model\MediaType(new \ArrayObject($operationSchemas[$format]->getArrayCopy(false)));
  265.         }
  266.         return $content;
  267.     }
  268.     private function getMimeTypes(string $resourceClassstring $operationNamestring $operationTypeResourceMetadata $resourceMetadata null): array
  269.     {
  270.         $requestFormats $resourceMetadata->getTypedOperationAttribute($operationType$operationName'input_formats'$this->formatstrue);
  271.         $responseFormats $resourceMetadata->getTypedOperationAttribute($operationType$operationName'output_formats'$this->formatstrue);
  272.         $requestMimeTypes $this->flattenMimeTypes($requestFormats);
  273.         $responseMimeTypes $this->flattenMimeTypes($responseFormats);
  274.         return [$requestMimeTypes$responseMimeTypes];
  275.     }
  276.     private function flattenMimeTypes(array $responseFormats): array
  277.     {
  278.         $responseMimeTypes = [];
  279.         foreach ($responseFormats as $responseFormat => $mimeTypes) {
  280.             foreach ($mimeTypes as $mimeType) {
  281.                 $responseMimeTypes[$mimeType] = $responseFormat;
  282.             }
  283.         }
  284.         return $responseMimeTypes;
  285.     }
  286.     /**
  287.      * Gets the path for an operation.
  288.      *
  289.      * If the path ends with the optional _format parameter, it is removed
  290.      * as optional path parameters are not yet supported.
  291.      *
  292.      * @see https://github.com/OAI/OpenAPI-Specification/issues/93
  293.      */
  294.     private function getPath(string $resourceShortNamestring $operationName, array $operationstring $operationType): string
  295.     {
  296.         $path $this->operationPathResolver->resolveOperationPath($resourceShortName$operation$operationType$operationName);
  297.         if ('.{_format}' === substr($path, -10)) {
  298.             $path substr($path0, -10);
  299.         }
  300.         return str_starts_with($path'/') ? $path '/'.$path;
  301.     }
  302.     private function getPathDescription(string $resourceShortNamestring $methodstring $operationType): string
  303.     {
  304.         switch ($method) {
  305.             case 'GET':
  306.                 $pathSummary OperationType::COLLECTION === $operationType 'Retrieves the collection of %s resources.' 'Retrieves a %s resource.';
  307.                 break;
  308.             case 'POST':
  309.                 $pathSummary 'Creates a %s resource.';
  310.                 break;
  311.             case 'PATCH':
  312.                 $pathSummary 'Updates the %s resource.';
  313.                 break;
  314.             case 'PUT':
  315.                 $pathSummary 'Replaces the %s resource.';
  316.                 break;
  317.             case 'DELETE':
  318.                 $pathSummary 'Removes the %s resource.';
  319.                 break;
  320.             default:
  321.                 return $resourceShortName;
  322.         }
  323.         return sprintf($pathSummary$resourceShortName);
  324.     }
  325.     /**
  326.      * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
  327.      */
  328.     private function getLink(string $resourceClassstring $operationIdstring $path): Model\Link
  329.     {
  330.         $parameters = [];
  331.         foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
  332.             $propertyMetadata $this->propertyMetadataFactory->create($resourceClass$propertyName);
  333.             if (!$propertyMetadata->isIdentifier()) {
  334.                 continue;
  335.             }
  336.             $parameters[$propertyName] = sprintf('$response.body#/%s'$propertyName);
  337.         }
  338.         return new Model\Link(
  339.             $operationId,
  340.             new \ArrayObject($parameters),
  341.             null,
  342.             === \count($parameters) ? sprintf('The `%1$s` value returned in the response can be used as the `%1$s` parameter in `GET %2$s`.'key($parameters), $path) : sprintf('The values returned in the response can be used in `GET %s`.'$path)
  343.         );
  344.     }
  345.     /**
  346.      * Gets parameters corresponding to enabled filters.
  347.      */
  348.     private function getFiltersParameters(ResourceMetadata $resourceMetadatastring $operationNamestring $resourceClass): array
  349.     {
  350.         $parameters = [];
  351.         $resourceFilters $resourceMetadata->getCollectionOperationAttribute($operationName'filters', [], true);
  352.         foreach ($resourceFilters as $filterId) {
  353.             if (!$filter $this->getFilter($filterId)) {
  354.                 continue;
  355.             }
  356.             foreach ($filter->getDescription($resourceClass) as $name => $data) {
  357.                 $schema $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypestrue) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], falsenull$data['is_collection'] ?? false)) : ['type' => 'string']);
  358.                 $parameters[] = new Model\Parameter(
  359.                     $name,
  360.                     'query',
  361.                     $data['description'] ?? '',
  362.                     $data['required'] ?? false,
  363.                     $data['openapi']['deprecated'] ?? false,
  364.                     $data['openapi']['allowEmptyValue'] ?? true,
  365.                     $schema,
  366.                     'array' === $schema['type'] && \in_array($data['type'],
  367.                         [Type::BUILTIN_TYPE_ARRAYType::BUILTIN_TYPE_OBJECT], true) ? 'deepObject' 'form',
  368.                     $data['openapi']['explode'] ?? ('array' === $schema['type']),
  369.                     $data['openapi']['allowReserved'] ?? false,
  370.                     $data['openapi']['example'] ?? null,
  371.                     isset($data['openapi']['examples']
  372.                     ) ? new \ArrayObject($data['openapi']['examples']) : null);
  373.             }
  374.         }
  375.         return $parameters;
  376.     }
  377.     private function getPaginationParameters(ResourceMetadata $resourceMetadatastring $operationName): array
  378.     {
  379.         if (!$this->paginationOptions->isPaginationEnabled()) {
  380.             return [];
  381.         }
  382.         $parameters = [];
  383.         if ($resourceMetadata->getCollectionOperationAttribute($operationName'pagination_enabled'truetrue)) {
  384.             $parameters[] = new Model\Parameter($this->paginationOptions->getPaginationPageParameterName(), 'query''The collection page number'falsefalsetrue, ['type' => 'integer''default' => 1]);
  385.             if ($resourceMetadata->getCollectionOperationAttribute($operationName'pagination_client_items_per_page'$this->paginationOptions->getClientItemsPerPage(), true)) {
  386.                 $schema = [
  387.                     'type' => 'integer',
  388.                     'default' => $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_items_per_page'30true),
  389.                     'minimum' => 0,
  390.                 ];
  391.                 if (null !== $maxItemsPerPage $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_maximum_items_per_page'nulltrue)) {
  392.                     $schema['maximum'] = $maxItemsPerPage;
  393.                 }
  394.                 $parameters[] = new Model\Parameter($this->paginationOptions->getItemsPerPageParameterName(), 'query''The number of items per page'falsefalsetrue$schema);
  395.             }
  396.         }
  397.         if ($resourceMetadata->getCollectionOperationAttribute($operationName'pagination_client_enabled'$this->paginationOptions->getPaginationClientEnabled(), true)) {
  398.             $parameters[] = new Model\Parameter($this->paginationOptions->getPaginationClientEnabledParameterName(), 'query''Enable or disable pagination'falsefalsetrue, ['type' => 'boolean']);
  399.         }
  400.         return $parameters;
  401.     }
  402.     private function getOauthSecurityScheme(): Model\SecurityScheme
  403.     {
  404.         $oauthFlow = new Model\OAuthFlow($this->openApiOptions->getOAuthAuthorizationUrl(), $this->openApiOptions->getOAuthTokenUrl(), $this->openApiOptions->getOAuthRefreshUrl(), new \ArrayObject($this->openApiOptions->getOAuthScopes()));
  405.         $description sprintf(
  406.             'OAuth 2.0 %s Grant',
  407.             strtolower(preg_replace('/[A-Z]/'' \\0'lcfirst($this->openApiOptions->getOAuthFlow())))
  408.         );
  409.         $implicit $password $clientCredentials $authorizationCode null;
  410.         switch ($this->openApiOptions->getOAuthFlow()) {
  411.             case 'implicit':
  412.                 $implicit $oauthFlow;
  413.                 break;
  414.             case 'password':
  415.                 $password $oauthFlow;
  416.                 break;
  417.             case 'application':
  418.             case 'clientCredentials':
  419.                 $clientCredentials $oauthFlow;
  420.                 break;
  421.             case 'accessCode':
  422.             case 'authorizationCode':
  423.                 $authorizationCode $oauthFlow;
  424.                 break;
  425.             default:
  426.                 throw new \LogicException('OAuth flow must be one of: implicit, password, clientCredentials, authorizationCode');
  427.         }
  428.         return new Model\SecurityScheme($this->openApiOptions->getOAuthType(), $descriptionnullnullnullnull, new Model\OAuthFlows($implicit$password$clientCredentials$authorizationCode), null);
  429.     }
  430.     private function getSecuritySchemes(): array
  431.     {
  432.         $securitySchemes = [];
  433.         if ($this->openApiOptions->getOAuthEnabled()) {
  434.             $securitySchemes['oauth'] = $this->getOauthSecurityScheme();
  435.         }
  436.         foreach ($this->openApiOptions->getApiKeys() as $key => $apiKey) {
  437.             $description sprintf('Value for the %s %s parameter.'$apiKey['name'], $apiKey['type']);
  438.             $securitySchemes[$key] = new Model\SecurityScheme('apiKey'$description$apiKey['name'], $apiKey['type']);
  439.         }
  440.         return $securitySchemes;
  441.     }
  442.     private function appendSchemaDefinitions(\ArrayObject $schemas, \ArrayObject $definitions): void
  443.     {
  444.         foreach ($definitions as $key => $value) {
  445.             $schemas[$key] = $value;
  446.         }
  447.     }
  448.     /**
  449.      * @param Model\Parameter[] $parameters
  450.      */
  451.     private function hasParameter(Model\Parameter $parameter, array $parameters): bool
  452.     {
  453.         foreach ($parameters as $existingParameter) {
  454.             if ($existingParameter->getName() === $parameter->getName() && $existingParameter->getIn() === $parameter->getIn()) {
  455.                 return true;
  456.             }
  457.         }
  458.         return false;
  459.     }
  460. }