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

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 $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, SchemaFactoryInterface $jsonSchemaFactory, TypeFactoryInterface $jsonSchemaTypeFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $filterLocator, SubresourceOperationFactoryInterface $subresourceOperationFactory, IdentifiersExtractorInterface $identifiersExtractor = null, array $formats = [], Options $openApiOptions = null, PaginationOptions $paginationOptions = null)
  55. {
  56. $this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
  57. $this->jsonSchemaFactory = $jsonSchemaFactory;
  58. $this->jsonSchemaTypeFactory = $jsonSchemaTypeFactory;
  59. $this->formats = $formats;
  60. $this->setFilterLocator($filterLocator, true);
  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, $resourceClass, OperationType::ITEM, $context, $paths, $links, $schemas);
  84. $this->collectPaths($resourceMetadata, $resourceClass, OperationType::COLLECTION, $context, $paths, $links, $schemas);
  85. $this->collectPaths($resourceMetadata, $resourceClass, OperationType::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 $resourceMetadata, string $resourceClass, string $operationType, array $context, Model\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) > 1 ? $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($method, PathItem::$methods, true)) {
  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, $operationFormat, Schema::TYPE_OUTPUT, $operationType, $operationName, $schema, null, $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', true, false, false, ['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', true, false, false, ['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', $resourceShortName, OperationType::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), $responseContent, null, $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), $responseContent, null, $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, $operationFormat, Schema::TYPE_INPUT, $operationType, $operationName, $schema, null, $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', false, true),
  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 $resourceClass, string $operationName, string $operationType, ResourceMetadata $resourceMetadata = null): array
  269. {
  270. $requestFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_formats', $this->formats, true);
  271. $responseFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_formats', $this->formats, true);
  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 $resourceShortName, string $operationName, array $operation, string $operationType): string
  295. {
  296. $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
  297. if ('.{_format}' === substr($path, -10)) {
  298. $path = substr($path, 0, -10);
  299. }
  300. return str_starts_with($path, '/') ? $path : '/'.$path;
  301. }
  302. private function getPathDescription(string $resourceShortName, string $method, string $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 $resourceClass, string $operationId, string $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. 1 === \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 $resourceMetadata, string $operationName, string $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::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $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_ARRAY, Type::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 $resourceMetadata, string $operationName): array
  378. {
  379. if (!$this->paginationOptions->isPaginationEnabled()) {
  380. return [];
  381. }
  382. $parameters = [];
  383. if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
  384. $parameters[] = new Model\Parameter($this->paginationOptions->getPaginationPageParameterName(), 'query', 'The collection page number', false, false, true, ['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', 30, true),
  389. 'minimum' => 0,
  390. ];
  391. if (null !== $maxItemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_maximum_items_per_page', null, true)) {
  392. $schema['maximum'] = $maxItemsPerPage;
  393. }
  394. $parameters[] = new Model\Parameter($this->paginationOptions->getItemsPerPageParameterName(), 'query', 'The number of items per page', false, false, true, $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', false, false, true, ['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(), $description, null, null, null, null, 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. }