• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 15040977736

15 May 2025 09:02AM UTC coverage: 21.754% (+13.3%) from 8.423%
15040977736

Pull #6960

github

web-flow
Merge 7a7a13526 into 1862d03b7
Pull Request #6960: feat(json-schema): mutualize json schema between formats

320 of 460 new or added lines in 24 files covered. (69.57%)

1863 existing lines in 109 files now uncovered.

11069 of 50882 relevant lines covered (21.75%)

29.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.66
/src/OpenApi/Factory/OpenApiFactory.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\OpenApi\Factory;
15

16
use ApiPlatform\JsonSchema\Schema;
17
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
18
use ApiPlatform\Metadata\ApiResource;
19
use ApiPlatform\Metadata\CollectionOperationInterface;
20
use ApiPlatform\Metadata\Error;
21
use ApiPlatform\Metadata\ErrorResource;
22
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
23
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
24
use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException;
25
use ApiPlatform\Metadata\Exception\RuntimeException;
26
use ApiPlatform\Metadata\HeaderParameterInterface;
27
use ApiPlatform\Metadata\HttpOperation;
28
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
29
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
30
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
31
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
32
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
33
use ApiPlatform\OpenApi\Attributes\Webhook;
34
use ApiPlatform\OpenApi\Model\Components;
35
use ApiPlatform\OpenApi\Model\Contact;
36
use ApiPlatform\OpenApi\Model\Info;
37
use ApiPlatform\OpenApi\Model\License;
38
use ApiPlatform\OpenApi\Model\Link;
39
use ApiPlatform\OpenApi\Model\MediaType;
40
use ApiPlatform\OpenApi\Model\OAuthFlow;
41
use ApiPlatform\OpenApi\Model\OAuthFlows;
42
use ApiPlatform\OpenApi\Model\Operation;
43
use ApiPlatform\OpenApi\Model\Parameter;
44
use ApiPlatform\OpenApi\Model\PathItem;
45
use ApiPlatform\OpenApi\Model\Paths;
46
use ApiPlatform\OpenApi\Model\RequestBody;
47
use ApiPlatform\OpenApi\Model\Response;
48
use ApiPlatform\OpenApi\Model\SecurityScheme;
49
use ApiPlatform\OpenApi\Model\Server;
50
use ApiPlatform\OpenApi\Model\Tag;
51
use ApiPlatform\OpenApi\OpenApi;
52
use ApiPlatform\OpenApi\Options;
53
use ApiPlatform\OpenApi\Serializer\NormalizeOperationNameTrait;
54
use ApiPlatform\State\ApiResource\Error as ApiResourceError;
55
use ApiPlatform\State\Pagination\PaginationOptions;
56
use ApiPlatform\State\Util\StateOptionsTrait;
57
use ApiPlatform\Validator\Exception\ValidationException;
58
use Psr\Container\ContainerInterface;
59
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
60
use Symfony\Component\PropertyInfo\Type as LegacyType;
61
use Symfony\Component\Routing\RouteCollection;
62
use Symfony\Component\Routing\RouterInterface;
63
use Symfony\Component\TypeInfo\Type;
64
use Symfony\Component\TypeInfo\TypeIdentifier;
65

66
/**
67
 * Generates an Open API v3 specification.
68
 */
69
final class OpenApiFactory implements OpenApiFactoryInterface
70
{
71
    use NormalizeOperationNameTrait;
72
    use StateOptionsTrait;
73
    use TypeFactoryTrait;
74

75
    public const BASE_URL = 'base_url';
76
    public const API_PLATFORM_TAG = 'x-apiplatform-tag';
77
    public const OVERRIDE_OPENAPI_RESPONSES = 'open_api_override_responses';
78
    private readonly Options $openApiOptions;
79
    private readonly PaginationOptions $paginationOptions;
80
    private ?RouteCollection $routeCollection = null;
81
    private ?ContainerInterface $filterLocator = null;
82
    /**
83
     * @var array<string|class-string, ErrorResource>
84
     */
85
    private array $localErrorResourceCache = [];
86

87
    /**
88
     * @param array<string, string[]> $formats
89
     */
90
    public function __construct(
91
        private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
92
        private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory,
93
        private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
94
        private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
95
        private readonly SchemaFactoryInterface $jsonSchemaFactory,
96
        ?ContainerInterface $filterLocator = null,
97
        private readonly array $formats = [],
98
        ?Options $openApiOptions = null,
99
        ?PaginationOptions $paginationOptions = null,
100
        private readonly ?RouterInterface $router = null,
101
        private readonly array $errorFormats = [],
102
    ) {
103
        $this->filterLocator = $filterLocator;
710✔
104
        $this->openApiOptions = $openApiOptions ?: new Options('API Platform');
710✔
105
        $this->paginationOptions = $paginationOptions ?: new PaginationOptions();
710✔
106
    }
107

108
    /**
109
     * {@inheritdoc}
110
     *
111
     * You can filter openapi operations with the `x-apiplatform-tag` on an OpenApi Operation using the `filter_tags`.
112
     *
113
     * @param array{base_url?: string, filter_tags?: string[]}&array<string, mixed> $context
114
     */
115
    public function __invoke(array $context = []): OpenApi
116
    {
117
        $baseUrl = $context[self::BASE_URL] ?? '/';
15✔
118
        $contact = null === $this->openApiOptions->getContactUrl() || null === $this->openApiOptions->getContactEmail() ? null : new Contact($this->openApiOptions->getContactName(), $this->openApiOptions->getContactUrl(), $this->openApiOptions->getContactEmail());
15✔
119
        $license = null === $this->openApiOptions->getLicenseName() ? null : new License($this->openApiOptions->getLicenseName(), $this->openApiOptions->getLicenseUrl(), $this->openApiOptions->getLicenseIdentifier());
15✔
120
        $info = new Info($this->openApiOptions->getTitle(), $this->openApiOptions->getVersion(), trim($this->openApiOptions->getDescription()), $this->openApiOptions->getTermsOfService(), $contact, $license);
15✔
121
        $servers = '/' === $baseUrl || '' === $baseUrl ? [new Server('/')] : [new Server($baseUrl)];
15✔
122
        $paths = new Paths();
15✔
123
        $schemas = new \ArrayObject();
15✔
124
        $webhooks = new \ArrayObject();
15✔
125
        $tags = [];
15✔
126

127
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
15✔
128
            $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
15✔
129
            foreach ($resourceMetadataCollection as $resourceMetadata) {
15✔
130
                $this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas, $webhooks, $tags, $context);
15✔
131
            }
132
        }
133

134
        $securitySchemes = $this->getSecuritySchemes();
15✔
135
        $securityRequirements = [];
15✔
136

137
        foreach (array_keys($securitySchemes) as $key) {
15✔
138
            $securityRequirements[] = [$key => []];
15✔
139
        }
140

141
        $globalTags = $this->openApiOptions->getTags() ?: array_values($tags) ?: [];
15✔
142

143
        return new OpenApi(
15✔
144
            $info,
15✔
145
            $servers,
15✔
146
            $paths,
15✔
147
            new Components(
15✔
148
                $schemas,
15✔
149
                new \ArrayObject(),
15✔
150
                new \ArrayObject(),
15✔
151
                new \ArrayObject(),
15✔
152
                new \ArrayObject(),
15✔
153
                new \ArrayObject(),
15✔
154
                new \ArrayObject($securitySchemes)
15✔
155
            ),
15✔
156
            $securityRequirements,
15✔
157
            $globalTags,
15✔
158
            null,
15✔
159
            null,
15✔
160
            $webhooks
15✔
161
        );
15✔
162
    }
163

164
    private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas, \ArrayObject $webhooks, array &$tags, array $context = []): void
165
    {
166
        if (0 === $resource->getOperations()->count()) {
15✔
167
            return;
×
168
        }
169

170
        $defaultError = $this->getErrorResource($this->openApiOptions->getErrorResourceClass() ?? ApiResourceError::class);
15✔
171
        $defaultValidationError = $this->getErrorResource($this->openApiOptions->getValidationErrorResourceClass() ?? ValidationException::class, 422, 'Unprocessable entity');
15✔
172

173
        // This filters on our extension x-apiplatform-tag as the openapi operation tag is used for ordering operations
174
        $filteredTags = $context['filter_tags'] ?? [];
15✔
175
        if (!\is_array($filteredTags)) {
15✔
UNCOV
176
            $filteredTags = [$filteredTags];
×
177
        }
178

179
        foreach ($resource->getOperations() as $operationName => $operation) {
15✔
180
            $resourceShortName = $operation->getShortName() ?? $operation;
15✔
181
            // No path to return
182
            if (null === $operation->getUriTemplate() && null === $operation->getRouteName()) {
15✔
183
                continue;
×
184
            }
185

186
            $openapiAttribute = $operation->getOpenapi();
15✔
187

188
            // Operation ignored from OpenApi
189
            if ($operation instanceof HttpOperation && false === $openapiAttribute) {
15✔
190
                continue;
15✔
191
            }
192

193
            // See https://github.com/api-platform/core/issues/6993 we would like to allow only `false` but as we typed `bool` we have this check
194
            $operationTag = !\is_object($openapiAttribute) ? [] : ($openapiAttribute->getExtensionProperties()[self::API_PLATFORM_TAG] ?? []);
15✔
195
            if (!\is_array($operationTag)) {
15✔
196
                $operationTag = [$operationTag];
15✔
197
            }
198

199
            if ($filteredTags && $filteredTags !== array_intersect($filteredTags, $operationTag)) {
15✔
UNCOV
200
                continue;
×
201
            }
202

203
            $resourceClass = $operation->getClass() ?? $resource->getClass();
15✔
204
            $routeName = $operation->getRouteName() ?? $operation->getName();
15✔
205

206
            if (!$this->routeCollection && $this->router) {
15✔
207
                $this->routeCollection = $this->router->getRouteCollection();
15✔
208
            }
209

210
            if ($this->routeCollection && $routeName && $route = $this->routeCollection->get($routeName)) {
15✔
211
                $path = $route->getPath();
15✔
212
            } else {
213
                $path = rtrim($operation->getRoutePrefix() ?? '', '/').'/'.ltrim($operation->getUriTemplate() ?? '', '/');
×
214
            }
215

216
            $path = $this->getPath($path);
15✔
217
            $method = $operation->getMethod() ?? 'GET';
15✔
218

219
            if (!\in_array($method, PathItem::$methods, true)) {
15✔
220
                continue;
×
221
            }
222

223
            $pathItem = null;
15✔
224

225
            if ($openapiAttribute instanceof Webhook) {
15✔
226
                $pathItem = $openapiAttribute->getPathItem() ?: new PathItem();
15✔
227
                $openapiOperation = $pathItem->{'get'.ucfirst(strtolower($method))}() ?: new Operation();
15✔
228
            } elseif (!\is_object($openapiAttribute)) {
15✔
229
                $openapiOperation = new Operation();
15✔
230
            } else {
231
                $openapiOperation = $openapiAttribute;
15✔
232
            }
233

234
            // Complete with defaults
235
            $openapiOperation = new Operation(
15✔
236
                operationId: null !== $openapiOperation->getOperationId() ? $openapiOperation->getOperationId() : $this->normalizeOperationName($operationName),
15✔
237
                tags: null !== $openapiOperation->getTags() ? $openapiOperation->getTags() : [$operation->getShortName() ?: $resourceShortName],
15✔
238
                responses: null !== $openapiOperation->getResponses() ? $openapiOperation->getResponses() : [],
15✔
239
                summary: null !== $openapiOperation->getSummary() ? $openapiOperation->getSummary() : $this->getPathDescription($resourceShortName, $method, $operation instanceof CollectionOperationInterface),
15✔
240
                description: null !== $openapiOperation->getDescription() ? $openapiOperation->getDescription() : $this->getPathDescription($resourceShortName, $method, $operation instanceof CollectionOperationInterface),
15✔
241
                externalDocs: $openapiOperation->getExternalDocs(),
15✔
242
                parameters: null !== $openapiOperation->getParameters() ? $openapiOperation->getParameters() : [],
15✔
243
                requestBody: $openapiOperation->getRequestBody(),
15✔
244
                callbacks: $openapiOperation->getCallbacks(),
15✔
245
                deprecated: null !== $openapiOperation->getDeprecated() ? $openapiOperation->getDeprecated() : ($operation->getDeprecationReason() ? true : null),
15✔
246
                security: null !== $openapiOperation->getSecurity() ? $openapiOperation->getSecurity() : null,
15✔
247
                servers: null !== $openapiOperation->getServers() ? $openapiOperation->getServers() : null,
15✔
248
                extensionProperties: $openapiOperation->getExtensionProperties(),
15✔
249
            );
15✔
250

251
            foreach ($openapiOperation->getTags() as $v) {
15✔
252
                $tags[$v] = new Tag(name: $v, description: $resource->getDescription() ?? "Resource '$v' operations.");
15✔
253
            }
254

255
            [$requestMimeTypes, $responseMimeTypes] = $this->getMimeTypes($operation);
15✔
256

257
            if ($path) {
15✔
258
                $pathItem = $paths->getPath($path) ?: new PathItem();
15✔
259
            } elseif (!$pathItem) {
×
260
                $pathItem = new PathItem();
×
261
            }
262

263
            $forceSchemaCollection = $operation instanceof CollectionOperationInterface && 'GET' === $method;
15✔
264
            $schema = new Schema('openapi');
15✔
265
            $schema->setDefinitions($schemas);
15✔
266

267
            $operationOutputSchemas = [];
15✔
268

269
            foreach ($responseMimeTypes as $operationFormat) {
15✔
270
                $operationOutputSchema = null;
15✔
271
                if (str_starts_with($operationFormat, 'json')) {
15✔
272
                    $operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operation, $schema, null, $forceSchemaCollection);
15✔
273
                    $this->appendSchemaDefinitions($schemas, $operationOutputSchema->getDefinitions());
15✔
274
                }
275

276
                $operationOutputSchemas[$operationFormat] = $operationOutputSchema;
15✔
277
            }
278

279
            // Set up parameters
280
            $openapiParameters = $openapiOperation->getParameters();
15✔
281
            foreach ($operation->getUriVariables() ?? [] as $parameterName => $uriVariable) {
15✔
282
                if ($uriVariable->getExpandedValue() ?? false) {
15✔
283
                    continue;
15✔
284
                }
285

286
                $parameter = new Parameter(
15✔
287
                    $parameterName,
15✔
288
                    'path',
15✔
289
                    $uriVariable->getDescription() ?? "$resourceShortName identifier",
15✔
290
                    $uriVariable->getRequired() ?? true,
15✔
291
                    false,
15✔
292
                    null,
15✔
293
                    $uriVariable->getSchema() ?? ['type' => 'string'],
15✔
294
                );
15✔
295

296
                if ($linkParameter = $uriVariable->getOpenApi()) {
15✔
297
                    $parameter = $this->mergeParameter($parameter, $linkParameter);
×
298
                }
299

300
                if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $parameter)) {
15✔
301
                    $openapiParameters[$i] = $this->mergeParameter($parameter, $operationParameter);
×
302
                    continue;
×
303
                }
304

305
                $openapiParameters[] = $parameter;
15✔
306
            }
307

308
            $openapiOperation = $openapiOperation->withParameters($openapiParameters);
15✔
309

310
            if ($operation instanceof CollectionOperationInterface && 'POST' !== $method) {
15✔
311
                foreach (array_merge($this->getPaginationParameters($operation), $this->getFiltersParameters($operation)) as $parameter) {
15✔
312
                    if ($operationParameter = $this->hasParameter($openapiOperation, $parameter)) {
15✔
313
                        continue;
×
314
                    }
315

316
                    $openapiOperation = $openapiOperation->withParameter($parameter);
15✔
317
                }
318
            }
319

320
            $entityClass = $this->getStateOptionsClass($operation, $operation->getClass());
15✔
321
            $openapiParameters = $openapiOperation->getParameters();
15✔
322
            foreach ($operation->getParameters() ?? [] as $key => $p) {
15✔
323
                if (false === $p->getOpenApi()) {
15✔
324
                    continue;
15✔
325
                }
326

327
                if (($f = $p->getFilter()) && \is_string($f) && $this->filterLocator && $this->filterLocator->has($f)) {
15✔
328
                    $filter = $this->filterLocator->get($f);
15✔
329

330
                    if ($d = $filter->getDescription($entityClass)) {
15✔
331
                        foreach ($d as $name => $description) {
15✔
332
                            if ($prop = $p->getProperty()) {
15✔
333
                                $name = str_replace($prop, $key, $name);
15✔
334
                            }
335

336
                            $openapiParameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $f);
15✔
337
                        }
338

339
                        continue;
15✔
340
                    }
341
                }
342

343
                $in = $p instanceof HeaderParameterInterface ? 'header' : 'query';
15✔
344
                $defaultParameter = new Parameter(
15✔
345
                    $key,
15✔
346
                    $in,
15✔
347
                    $p->getDescription() ?? "$resourceShortName $key",
15✔
348
                    $p->getRequired() ?? false,
15✔
349
                    false,
15✔
350
                    null,
15✔
351
                    $p->getSchema() ?? ['type' => 'string'],
15✔
352
                );
15✔
353

354
                $linkParameter = $p->getOpenApi();
15✔
355
                if (null === $linkParameter) {
15✔
356
                    if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $defaultParameter)) {
15✔
357
                        $openapiParameters[$i] = $this->mergeParameter($defaultParameter, $operationParameter);
×
358
                    } else {
359
                        $openapiParameters[] = $defaultParameter;
15✔
360
                    }
361

362
                    continue;
15✔
363
                }
364

365
                if (\is_array($linkParameter)) {
15✔
366
                    foreach ($linkParameter as $lp) {
15✔
367
                        $parameter = $this->mergeParameter($defaultParameter, $lp);
15✔
368
                        if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $parameter)) {
15✔
369
                            $openapiParameters[$i] = $this->mergeParameter($parameter, $operationParameter);
×
370
                            continue;
×
371
                        }
372

373
                        $openapiParameters[] = $parameter;
15✔
374
                    }
375
                    continue;
15✔
376
                }
377

378
                $parameter = $this->mergeParameter($defaultParameter, $linkParameter);
15✔
379
                if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $parameter)) {
15✔
380
                    $openapiParameters[$i] = $this->mergeParameter($parameter, $operationParameter);
×
381
                    continue;
×
382
                }
383
                $openapiParameters[] = $parameter;
15✔
384
            }
385

386
            $openapiOperation = $openapiOperation->withParameters($openapiParameters);
15✔
387
            $existingResponses = $openapiOperation->getResponses() ?: [];
15✔
388
            $overrideResponses = $operation->getExtraProperties()[self::OVERRIDE_OPENAPI_RESPONSES] ?? $this->openApiOptions->getOverrideResponses();
15✔
389
            $errors = null;
15✔
390
            if ($operation instanceof HttpOperation && null !== ($errors = $operation->getErrors())) {
15✔
391
                /** @var array<class-string|string, Error> */
392
                $errorOperations = [];
15✔
393
                foreach ($errors as $error) {
15✔
394
                    $errorOperations[$error] = $this->getErrorResource($error);
15✔
395
                }
396

397
                $openapiOperation = $this->addOperationErrors($openapiOperation, $errorOperations, $resourceMetadataCollection, $schema, $schemas, $operation);
15✔
398
            }
399

400
            if ($overrideResponses || !$existingResponses) {
15✔
401
                // Create responses
402
                switch ($method) {
403
                    case 'GET':
15✔
404
                        $successStatus = (string) $operation->getStatus() ?: 200;
15✔
405
                        $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, \sprintf('%s %s', $resourceShortName, $operation instanceof CollectionOperationInterface ? 'collection' : 'resource'), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas);
15✔
406
                        break;
15✔
407
                    case 'POST':
15✔
408
                        $successStatus = (string) $operation->getStatus() ?: 201;
15✔
409
                        $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, \sprintf('%s resource created', $resourceShortName), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas, $resourceMetadataCollection);
15✔
410

411
                        if (null === $errors) {
15✔
412
                            $openapiOperation = $this->addOperationErrors($openapiOperation, [
15✔
413
                                $defaultError->withStatus(400)->withDescription('Invalid input'),
15✔
414
                                $defaultValidationError,
15✔
415
                            ], $resourceMetadataCollection, $schema, $schemas, $operation);
15✔
416
                        }
417
                        break;
15✔
418
                    case 'PATCH':
15✔
419
                    case 'PUT':
15✔
420
                        $successStatus = (string) $operation->getStatus() ?: 200;
15✔
421
                        $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, \sprintf('%s resource updated', $resourceShortName), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas, $resourceMetadataCollection);
15✔
422

423
                        if (null === $errors) {
15✔
424
                            $openapiOperation = $this->addOperationErrors($openapiOperation, [
15✔
425
                                $defaultError->withStatus(400)->withDescription('Invalid input'),
15✔
426
                                $defaultValidationError,
15✔
427
                            ], $resourceMetadataCollection, $schema, $schemas, $operation);
15✔
428
                        }
429
                        break;
15✔
430
                    case 'DELETE':
15✔
431
                        $successStatus = (string) $operation->getStatus() ?: 204;
15✔
432
                        $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, \sprintf('%s resource deleted', $resourceShortName), $openapiOperation);
15✔
433
                        break;
15✔
434
                }
435
            }
436

437
            if ($overrideResponses && !isset($existingResponses[403]) && $operation->getSecurity()) {
15✔
438
                $openapiOperation = $this->addOperationErrors($openapiOperation, [
15✔
439
                    $defaultError->withStatus(403)->withDescription('Forbidden'),
15✔
440
                ], $resourceMetadataCollection, $schema, $schemas, $operation);
15✔
441
            }
442

443
            if ($overrideResponses && !$operation instanceof CollectionOperationInterface && 'POST' !== $operation->getMethod() && !isset($existingResponses[404]) && null === $errors) {
15✔
444
                $openapiOperation = $this->addOperationErrors($openapiOperation, [
15✔
445
                    $defaultError->withStatus(404)->withDescription('Not found'),
15✔
446
                ], $resourceMetadataCollection, $schema, $schemas, $operation);
15✔
447
            }
448

449
            if (!$openapiOperation->getResponses()) {
15✔
450
                $openapiOperation = $openapiOperation->withResponse('default', new Response('Unexpected error'));
×
451
            }
452

453
            if (
454
                \in_array($method, ['PATCH', 'PUT', 'POST'], true)
15✔
455
                && !(false === ($input = $operation->getInput()) || (\is_array($input) && null === $input['class']))
15✔
456
            ) {
457
                $content = $openapiOperation->getRequestBody()?->getContent();
15✔
458
                if (null === $content) {
15✔
459
                    $operationInputSchemas = [];
15✔
460
                    foreach ($requestMimeTypes as $operationFormat) {
15✔
461
                        $operationInputSchema = null;
15✔
462
                        if (str_starts_with($operationFormat, 'json')) {
15✔
463
                            $operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operation, $schema, null, $forceSchemaCollection);
15✔
464
                            $this->appendSchemaDefinitions($schemas, $operationInputSchema->getDefinitions());
15✔
465
                        }
466

467
                        $operationInputSchemas[$operationFormat] = $operationInputSchema;
15✔
468
                    }
469
                    $content = $this->buildContent($requestMimeTypes, $operationInputSchemas);
15✔
470
                }
471

472
                $openapiOperation = $openapiOperation->withRequestBody(new RequestBody(
15✔
473
                    description: $openapiOperation->getRequestBody()?->getDescription() ?? \sprintf('The %s %s resource', 'POST' === $method ? 'new' : 'updated', $resourceShortName),
15✔
474
                    content: $content,
15✔
475
                    required: $openapiOperation->getRequestBody()?->getRequired() ?? true,
15✔
476
                ));
15✔
477
            }
478

479
            if ($openapiAttribute instanceof Webhook) {
15✔
480
                $webhooks[$openapiAttribute->getName()] = $pathItem->{'with'.ucfirst($method)}($openapiOperation);
15✔
481
                continue;
15✔
482
            }
483

484
            // We merge content types for errors, maybe that this logic could be applied to every resources at some point
485
            if ($operation instanceof Error && ($existingPathItem = $paths->getPath($path)) && ($existingOperation = $existingPathItem->getGet()) && ($currentResponse = $openapiOperation->getResponses()[200] ?? null)) {
15✔
486
                $errorResponse = $existingOperation->getResponses()[200];
×
487
                $currentResponseContent = $currentResponse->getContent();
×
488

489
                foreach ($errorResponse->getContent() as $mime => $content) {
×
490
                    $currentResponseContent[$mime] = $content;
×
491
                }
492

493
                $openapiOperation = $existingOperation->withResponse(200, $currentResponse->withContent($currentResponseContent));
×
494
            }
495

496
            $paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
15✔
497
        }
498
    }
499

500
    private function buildOpenApiResponse(array $existingResponses, int|string $status, string $description, ?Operation $openapiOperation = null, ?HttpOperation $operation = null, ?array $responseMimeTypes = null, ?array $operationOutputSchemas = null, ?ResourceMetadataCollection $resourceMetadataCollection = null): Operation
501
    {
502
        if (isset($existingResponses[$status])) {
15✔
503
            return $openapiOperation;
×
504
        }
505
        $responseLinks = $responseContent = null;
15✔
506
        if ($responseMimeTypes && $operationOutputSchemas) {
15✔
507
            $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas);
15✔
508
        }
509
        if ($resourceMetadataCollection && $operation) {
15✔
510
            $responseLinks = $this->getLinks($resourceMetadataCollection, $operation);
15✔
511
        }
512

513
        return $openapiOperation->withResponse($status, new Response($description, $responseContent, null, $responseLinks));
15✔
514
    }
515

516
    /**
517
     * @param array<string, string> $responseMimeTypes
518
     * @param array<string, Schema> $operationSchemas
519
     *
520
     * @return \ArrayObject<MediaType>
521
     */
522
    private function buildContent(array $responseMimeTypes, array $operationSchemas): \ArrayObject
523
    {
524
        /** @var \ArrayObject<MediaType> $content */
525
        $content = new \ArrayObject();
15✔
526

527
        foreach ($responseMimeTypes as $mimeType => $format) {
15✔
528
            $content[$mimeType] = isset($operationSchemas[$format]) ? new MediaType(schema: new \ArrayObject($operationSchemas[$format]->getArrayCopy(false))) : new \ArrayObject();
15✔
529
        }
530

531
        return $content;
15✔
532
    }
533

534
    /**
535
     * @return array[array<string, string>, array<string, string>]
536
     */
537
    private function getMimeTypes(HttpOperation $operation): array
538
    {
539
        $requestFormats = $operation->getInputFormats() ?: [];
15✔
540
        $responseFormats = $operation->getOutputFormats() ?: [];
15✔
541

542
        $requestMimeTypes = $this->flattenMimeTypes($requestFormats);
15✔
543
        $responseMimeTypes = $this->flattenMimeTypes($responseFormats);
15✔
544

545
        return [$requestMimeTypes, $responseMimeTypes];
15✔
546
    }
547

548
    /**
549
     * @param array<string, string[]> $responseFormats
550
     *
551
     * @return array<string, string>
552
     */
553
    private function flattenMimeTypes(array $responseFormats): array
554
    {
555
        $responseMimeTypes = [];
15✔
556
        foreach ($responseFormats as $responseFormat => $mimeTypes) {
15✔
557
            foreach ($mimeTypes as $mimeType) {
15✔
558
                $responseMimeTypes[$mimeType] = $responseFormat;
15✔
559
            }
560
        }
561

562
        return $responseMimeTypes;
15✔
563
    }
564

565
    /**
566
     * Gets the path for an operation.
567
     *
568
     * If the path ends with the optional _format parameter, it is removed
569
     * as optional path parameters are not yet supported.
570
     *
571
     * @see https://github.com/OAI/OpenAPI-Specification/issues/93
572
     */
573
    private function getPath(string $path): string
574
    {
575
        // Handle either API Platform's URI Template (rfc6570) or Symfony's route
576
        if (str_ends_with($path, '{._format}') || str_ends_with($path, '.{_format}')) {
15✔
577
            $path = substr($path, 0, -10);
15✔
578
        }
579

580
        return str_starts_with($path, '/') ? $path : '/'.$path;
15✔
581
    }
582

583
    private function getPathDescription(string $resourceShortName, string $method, bool $isCollection): string
584
    {
585
        switch ($method) {
586
            case 'GET':
15✔
587
                $pathSummary = $isCollection ? 'Retrieves the collection of %s resources.' : 'Retrieves a %s resource.';
15✔
588
                break;
15✔
589
            case 'POST':
15✔
590
                $pathSummary = 'Creates a %s resource.';
15✔
591
                break;
15✔
592
            case 'PATCH':
15✔
593
                $pathSummary = 'Updates the %s resource.';
15✔
594
                break;
15✔
595
            case 'PUT':
15✔
596
                $pathSummary = 'Replaces the %s resource.';
15✔
597
                break;
15✔
598
            case 'DELETE':
15✔
599
                $pathSummary = 'Removes the %s resource.';
15✔
600
                break;
15✔
601
            default:
602
                return $resourceShortName;
×
603
        }
604

605
        return \sprintf($pathSummary, $resourceShortName);
15✔
606
    }
607

608
    /**
609
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
610
     *
611
     * @return \ArrayObject<Link>
612
     */
613
    private function getLinks(ResourceMetadataCollection $resourceMetadataCollection, HttpOperation $currentOperation): \ArrayObject
614
    {
615
        /** @var \ArrayObject<Link> $links */
616
        $links = new \ArrayObject();
15✔
617

618
        // Only compute get links for now
619
        foreach ($resourceMetadataCollection as $resource) {
15✔
620
            foreach ($resource->getOperations() as $operationName => $operation) {
15✔
621
                $parameters = [];
15✔
622
                $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET';
15✔
623
                if (
624
                    $operationName === $operation->getName()
15✔
625
                    || isset($links[$operationName])
15✔
626
                    || $operation instanceof CollectionOperationInterface
15✔
627
                    || 'GET' !== $method
15✔
628
                ) {
629
                    continue;
15✔
630
                }
631

632
                // Operation ignored from OpenApi
633
                if ($operation instanceof HttpOperation && (false === $operation->getOpenapi() || $operation->getOpenapi() instanceof Webhook)) {
×
634
                    continue;
×
635
                }
636

637
                $operationUriVariables = $operation->getUriVariables();
×
638
                foreach ($currentOperation->getUriVariables() ?? [] as $parameterName => $uriVariableDefinition) {
×
639
                    if (!isset($operationUriVariables[$parameterName])) {
×
640
                        continue;
×
641
                    }
642

643
                    if ($operationUriVariables[$parameterName]->getIdentifiers() === $uriVariableDefinition->getIdentifiers() && $operationUriVariables[$parameterName]->getFromClass() === $uriVariableDefinition->getFromClass()) {
×
644
                        $parameters[$parameterName] = '$request.path.'.($uriVariableDefinition->getIdentifiers()[0] ?? 'id');
×
645
                    }
646
                }
647

648
                foreach ($operationUriVariables ?? [] as $parameterName => $uriVariableDefinition) {
×
649
                    if (isset($parameters[$parameterName])) {
×
650
                        continue;
×
651
                    }
652

653
                    if ($uriVariableDefinition->getFromClass() === $currentOperation->getClass()) {
×
654
                        $parameters[$parameterName] = '$response.body#/'.($uriVariableDefinition->getIdentifiers()[0] ?? 'id');
×
655
                    }
656
                }
657

658
                $links[$operationName] = new Link(
×
659
                    $operationName,
×
660
                    new \ArrayObject($parameters),
×
661
                    null,
×
662
                    $operation->getDescription() ?? ''
×
663
                );
×
664
            }
665
        }
666

667
        return $links;
15✔
668
    }
669

670
    /**
671
     * Gets parameters corresponding to enabled filters.
672
     */
673
    private function getFiltersParameters(CollectionOperationInterface|HttpOperation $operation): array
674
    {
675
        $parameters = [];
15✔
676
        $resourceFilters = $operation->getFilters();
15✔
677
        $entityClass = $this->getStateOptionsClass($operation, $operation->getClass());
15✔
678

679
        foreach ($resourceFilters ?? [] as $filterId) {
15✔
680
            if (!$this->filterLocator->has($filterId)) {
15✔
681
                continue;
×
682
            }
683

684
            $filter = $this->filterLocator->get($filterId);
15✔
685
            foreach ($filter->getDescription($entityClass) as $name => $description) {
15✔
686
                $parameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $filterId);
15✔
687
            }
688
        }
689

690
        return $parameters;
15✔
691
    }
692

693
    /**
694
     * @param array<string, mixed> $description
695
     */
696
    private function getFilterParameter(string $name, array $description, string $shortName, string $filter): Parameter
697
    {
698
        if (isset($description['swagger'])) {
15✔
699
            trigger_deprecation('api-platform/core', '4.0', \sprintf('Using the "swagger" field of the %s::getDescription() (%s) is deprecated.', $filter, $shortName));
×
700
        }
701

702
        if (!isset($description['openapi']) || $description['openapi'] instanceof Parameter) {
15✔
703
            $schema = $description['schema'] ?? [];
15✔
704

705
            if (method_exists(PropertyInfoExtractor::class, 'getType')) {
15✔
706
                if (isset($description['type']) && \in_array($description['type'], TypeIdentifier::values(), true) && !isset($schema['type'])) {
15✔
707
                    $type = Type::builtin($description['type']);
15✔
708
                    if ($description['is_collection'] ?? false) {
15✔
709
                        $type = Type::array($type, Type::int());
15✔
710
                    }
711

712
                    $schema += $this->getType($type);
15✔
713
                }
714
            // TODO: remove in 5.x
715
            } else {
716
                if (isset($description['type']) && \in_array($description['type'], LegacyType::$builtinTypes, true) && !isset($schema['type'])) {
×
717
                    $schema += $this->getType(new LegacyType($description['type'], false, null, $description['is_collection'] ?? false));
×
718
                }
719
            }
720

721
            if (!isset($schema['type'])) {
15✔
722
                $schema['type'] = 'string';
15✔
723
            }
724

725
            $arrayValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::ARRAY->value : LegacyType::BUILTIN_TYPE_ARRAY;
15✔
726
            $objectValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::OBJECT->value : LegacyType::BUILTIN_TYPE_OBJECT;
15✔
727

728
            $style = 'array' === ($schema['type'] ?? null) && \in_array(
15✔
729
                $description['type'],
15✔
730
                [$arrayValueType, $objectValueType],
15✔
731
                true
15✔
732
            ) ? 'deepObject' : 'form';
15✔
733

734
            $parameter = isset($description['openapi']) && $description['openapi'] instanceof Parameter ? $description['openapi'] : new Parameter(in: 'query', name: $name, style: $style, explode: $description['is_collection'] ?? false);
15✔
735

736
            if ('' === $parameter->getDescription() && ($str = $description['description'] ?? '')) {
15✔
737
                $parameter = $parameter->withDescription($str);
×
738
            }
739

740
            if (false === $parameter->getRequired() && false !== ($required = $description['required'] ?? false)) {
15✔
741
                $parameter = $parameter->withRequired($required);
15✔
742
            }
743

744
            return $parameter->withSchema($schema);
15✔
745
        }
746

747
        trigger_deprecation('api-platform/core', '4.0', \sprintf('Not using "%s" on the "openapi" field of the %s::getDescription() (%s) is deprecated.', Parameter::class, $filter, $shortName));
×
748

749
        $schema = $description['schema'] ?? null;
×
750

751
        if (!$schema) {
×
752
            if (method_exists(PropertyInfoExtractor::class, 'getType')) {
×
753
                if (isset($description['type']) && \in_array($description['type'], TypeIdentifier::values(), true)) {
×
754
                    $type = Type::builtin($description['type']);
×
755
                    if ($description['is_collection'] ?? false) {
×
756
                        $type = Type::array($type, key: Type::int());
×
757
                    }
758
                    $schema = $this->getType($type);
×
759
                } else {
760
                    $schema = ['type' => 'string'];
×
761
                }
762
            // TODO: remove in 5.x
763
            } else {
764
                $schema = isset($description['type']) && \in_array($description['type'], LegacyType::$builtinTypes, true)
×
765
                    ? $this->getType(new LegacyType($description['type'], false, null, $description['is_collection'] ?? false))
×
766
                    : ['type' => 'string'];
×
767
            }
768
        }
769

770
        $arrayValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::ARRAY->value : LegacyType::BUILTIN_TYPE_ARRAY;
×
771
        $objectValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::OBJECT->value : LegacyType::BUILTIN_TYPE_OBJECT;
×
772

773
        return new Parameter(
×
774
            $name,
×
775
            'query',
×
776
            $description['description'] ?? '',
×
777
            $description['required'] ?? false,
×
778
            $description['openapi']['deprecated'] ?? false,
×
779
            $description['openapi']['allowEmptyValue'] ?? null,
×
780
            $schema,
×
781
            'array' === $schema['type'] && \in_array(
×
782
                $description['type'],
×
783
                [$arrayValueType, $objectValueType],
×
784
                true
×
785
            ) ? 'deepObject' : 'form',
×
786
            $description['openapi']['explode'] ?? ('array' === $schema['type']),
×
787
            $description['openapi']['allowReserved'] ?? null,
×
788
            $description['openapi']['example'] ?? null,
×
789
            isset(
×
790
                $description['openapi']['examples']
×
791
            ) ? new \ArrayObject($description['openapi']['examples']) : null
×
792
        );
×
793
    }
794

795
    private function getPaginationParameters(CollectionOperationInterface|HttpOperation $operation): array
796
    {
797
        if (!$this->paginationOptions->isPaginationEnabled()) {
15✔
798
            return [];
×
799
        }
800

801
        $parameters = [];
15✔
802

803
        if ($operation->getPaginationEnabled() ?? $this->paginationOptions->isPaginationEnabled()) {
15✔
804
            $parameters[] = new Parameter(
15✔
805
                $this->paginationOptions->getPaginationPageParameterName(),
15✔
806
                'query',
15✔
807
                'The collection page number',
15✔
808
                false,
15✔
809
                false,
15✔
810
                null,
15✔
811
                ['type' => 'integer', 'default' => 1],
15✔
812
            );
15✔
813

814
            if ($operation->getPaginationClientItemsPerPage() ?? $this->paginationOptions->getClientItemsPerPage()) {
15✔
815
                $schema = [
15✔
816
                    'type' => 'integer',
15✔
817
                    'default' => $operation->getPaginationItemsPerPage() ?? $this->paginationOptions->getItemsPerPage(),
15✔
818
                    'minimum' => 0,
15✔
819
                ];
15✔
820

821
                if (null !== $maxItemsPerPage = ($operation->getPaginationMaximumItemsPerPage() ?? $this->paginationOptions->getMaximumItemsPerPage())) {
15✔
822
                    $schema['maximum'] = $maxItemsPerPage;
15✔
823
                }
824

825
                $parameters[] = new Parameter(
15✔
826
                    $this->paginationOptions->getItemsPerPageParameterName(),
15✔
827
                    'query',
15✔
828
                    'The number of items per page',
15✔
829
                    false,
15✔
830
                    false,
15✔
831
                    null,
15✔
832
                    $schema,
15✔
833
                );
15✔
834
            }
835
        }
836

837
        if ($operation->getPaginationClientEnabled() ?? $this->paginationOptions->isPaginationClientEnabled()) {
15✔
838
            $parameters[] = new Parameter(
15✔
839
                $this->paginationOptions->getPaginationClientEnabledParameterName(),
15✔
840
                'query',
15✔
841
                'Enable or disable pagination',
15✔
842
                false,
15✔
843
                false,
15✔
844
                null,
15✔
845
                ['type' => 'boolean'],
15✔
846
            );
15✔
847
        }
848

849
        return $parameters;
15✔
850
    }
851

852
    private function getOauthSecurityScheme(): SecurityScheme
853
    {
854
        $oauthFlow = new OAuthFlow($this->openApiOptions->getOAuthAuthorizationUrl(), $this->openApiOptions->getOAuthTokenUrl() ?: null, $this->openApiOptions->getOAuthRefreshUrl() ?: null, new \ArrayObject($this->openApiOptions->getOAuthScopes()));
15✔
855
        $description = \sprintf(
15✔
856
            'OAuth 2.0 %s Grant',
15✔
857
            strtolower(preg_replace('/[A-Z]/', ' \\0', lcfirst($this->openApiOptions->getOAuthFlow())))
15✔
858
        );
15✔
859
        $implicit = $password = $clientCredentials = $authorizationCode = null;
15✔
860

861
        switch ($this->openApiOptions->getOAuthFlow()) {
15✔
862
            case 'implicit':
15✔
863
                $implicit = $oauthFlow;
15✔
864
                break;
15✔
865
            case 'password':
×
866
                $password = $oauthFlow;
×
867
                break;
×
868
            case 'application':
×
869
            case 'clientCredentials':
×
870
                $clientCredentials = $oauthFlow;
×
871
                break;
×
872
            case 'accessCode':
×
873
            case 'authorizationCode':
×
874
                $authorizationCode = $oauthFlow;
×
875
                break;
×
876
            default:
877
                throw new \LogicException('OAuth flow must be one of: implicit, password, clientCredentials, authorizationCode');
×
878
        }
879

880
        return new SecurityScheme($this->openApiOptions->getOAuthType(), $description, null, null, null, null, new OAuthFlows($implicit, $password, $clientCredentials, $authorizationCode), null);
15✔
881
    }
882

883
    private function getSecuritySchemes(): array
884
    {
885
        $securitySchemes = [];
15✔
886

887
        if ($this->openApiOptions->getOAuthEnabled()) {
15✔
888
            $securitySchemes['oauth'] = $this->getOauthSecurityScheme();
15✔
889
        }
890

891
        foreach ($this->openApiOptions->getApiKeys() as $key => $apiKey) {
15✔
892
            $description = \sprintf('Value for the %s %s parameter.', $apiKey['name'], $apiKey['type']);
15✔
893
            $securitySchemes[$key] = new SecurityScheme('apiKey', $description, $apiKey['name'], $apiKey['type']);
15✔
894
        }
895

896
        foreach ($this->openApiOptions->getHttpAuth() as $key => $httpAuth) {
15✔
897
            $description = \sprintf('Value for the http %s parameter.', $httpAuth['scheme']);
×
898
            $securitySchemes[$key] = new SecurityScheme('http', $description, null, null, $httpAuth['scheme'], $httpAuth['bearerFormat'] ?? null);
×
899
        }
900

901
        return $securitySchemes;
15✔
902
    }
903

904
    /**
905
     * @param \ArrayObject<string, mixed> $schemas
906
     * @param \ArrayObject<string, mixed> $definitions
907
     */
908
    private function appendSchemaDefinitions(\ArrayObject $schemas, \ArrayObject $definitions): void
909
    {
910
        foreach ($definitions as $key => $value) {
15✔
911
            $schemas[$key] = $value;
15✔
912
        }
913
    }
914

915
    /**
916
     * @return array{0: int, 1: Parameter}|null
917
     */
918
    private function hasParameter(Operation $operation, Parameter $parameter): ?array
919
    {
920
        foreach ($operation->getParameters() as $key => $existingParameter) {
15✔
921
            if ($existingParameter->getName() === $parameter->getName() && $existingParameter->getIn() === $parameter->getIn()) {
15✔
922
                return [$key, $existingParameter];
×
923
            }
924
        }
925

926
        return null;
15✔
927
    }
928

929
    private function mergeParameter(Parameter $actual, Parameter $defined): Parameter
930
    {
931
        foreach (
932
            [
15✔
933
                'name',
15✔
934
                'in',
15✔
935
                'description',
15✔
936
                'required',
15✔
937
                'deprecated',
15✔
938
                'allowEmptyValue',
15✔
939
                'style',
15✔
940
                'explode',
15✔
941
                'allowReserved',
15✔
942
                'example',
15✔
943
            ] as $method
15✔
944
        ) {
945
            $newValue = $defined->{"get$method"}();
15✔
946
            if (null !== $newValue && $actual->{"get$method"}() !== $newValue) {
15✔
947
                $actual = $actual->{"with$method"}($newValue);
15✔
948
            }
949
        }
950

951
        foreach (['examples', 'content', 'schema'] as $method) {
15✔
952
            $newValue = $defined->{"get$method"}();
15✔
953
            if ($newValue && \count($newValue) > 0 && $actual->{"get$method"}() !== $newValue) {
15✔
954
                $actual = $actual->{"with$method"}($newValue);
×
955
            }
956
        }
957

958
        return $actual;
15✔
959
    }
960

961
    /**
962
     * @param ErrorResource[]              $errors
963
     * @param \ArrayObject<string, Schema> $schemas
964
     */
965
    private function addOperationErrors(
966
        Operation $operation,
967
        array $errors,
968
        ResourceMetadataCollection $resourceMetadataCollection,
969
        Schema $schema,
970
        \ArrayObject $schemas,
971
        HttpOperation $originalOperation,
972
    ): Operation {
973
        foreach ($errors as $errorResource) {
15✔
974
            $responseMimeTypes = $this->flattenMimeTypes($errorResource->getOutputFormats() ?: $this->errorFormats);
15✔
975
            foreach ($errorResource->getOperations() as $errorOperation) {
15✔
976
                if (false === $errorOperation->getOpenApi()) {
15✔
977
                    continue;
15✔
978
                }
979

980
                $responseMimeTypes += $this->flattenMimeTypes($errorOperation->getOutputFormats() ?: $this->errorFormats);
15✔
981
            }
982

983
            foreach ($responseMimeTypes as $mime => $format) {
15✔
984
                if (!isset($this->errorFormats[$format])) {
15✔
985
                    unset($responseMimeTypes[$mime]);
×
986
                }
987
            }
988

989
            $operationErrorSchemas = [];
15✔
990
            foreach ($responseMimeTypes as $operationFormat) {
15✔
991
                $operationErrorSchema = null;
15✔
992
                if (str_starts_with($operationFormat, 'json')) {
15✔
993
                    $operationErrorSchema = $this->jsonSchemaFactory->buildSchema($errorResource->getClass(), $operationFormat, Schema::TYPE_OUTPUT, null, $schema);
15✔
994
                    $this->appendSchemaDefinitions($schemas, $operationErrorSchema->getDefinitions());
15✔
995
                }
996
                $operationErrorSchemas[$operationFormat] = $operationErrorSchema;
15✔
997
            }
998

999
            if (!$status = $errorResource->getStatus()) {
15✔
1000
                throw new RuntimeException(\sprintf('The error class "%s" has no status defined, please either implement ProblemExceptionInterface, or make it an ErrorResource with a status', $errorResource->getClass()));
×
1001
            }
1002

1003
            $operation = $this->buildOpenApiResponse($operation->getResponses() ?: [], $status, $errorResource->getDescription() ?? '', $operation, $originalOperation, $responseMimeTypes, $operationErrorSchemas, $resourceMetadataCollection);
15✔
1004
        }
1005

1006
        return $operation;
15✔
1007
    }
1008

1009
    /**
1010
     * @param string|class-string $error
1011
     */
1012
    private function getErrorResource(string $error, ?int $status = null, ?string $description = null): ErrorResource
1013
    {
1014
        if ($this->localErrorResourceCache[$error] ?? null) {
15✔
1015
            return $this->localErrorResourceCache[$error];
15✔
1016
        }
1017

1018
        if (is_a($error, ProblemExceptionInterface::class, true)) {
15✔
1019
            try {
1020
                /** @var ProblemExceptionInterface $exception */
1021
                $exception = new $error();
15✔
1022
                $status = $exception->getStatus();
15✔
1023
                $description = $exception->getTitle();
15✔
1024
            } catch (\TypeError) {
15✔
1025
            }
1026
        } elseif (class_exists($error)) {
×
1027
            throw new RuntimeException(\sprintf('The error class "%s" does not implement "%s". Did you forget a use statement?', $error, ProblemExceptionInterface::class));
×
1028
        }
1029

1030
        $defaultErrorResourceClass = $this->openApiOptions->getErrorResourceClass() ?? ApiResourceError::class;
15✔
1031

1032
        try {
1033
            $errorResource = $this->resourceMetadataFactory->create($error)[0] ?? new ErrorResource(status: $status, description: $description, class: $defaultErrorResourceClass);
15✔
1034
            if (!($errorResource instanceof ErrorResource)) {
15✔
1035
                throw new RuntimeException(\sprintf('The error class %s is not an ErrorResource', $error));
×
1036
            }
1037

1038
            // Here we want the exception status and expression to override the resource one when available
1039
            if ($status) {
15✔
1040
                $errorResource = $errorResource->withStatus($status);
15✔
1041
            }
1042

1043
            if ($description) {
15✔
1044
                $errorResource = $errorResource->withDescription($description);
15✔
1045
            }
1046
        } catch (ResourceClassNotFoundException|OperationNotFoundException) {
×
1047
            $errorResource = new ErrorResource(status: $status, description: $description, class: $defaultErrorResourceClass);
×
1048
        }
1049

1050
        if (!$errorResource->getClass()) {
15✔
1051
            $errorResource = $errorResource->withClass($error);
×
1052
        }
1053

1054
        return $this->localErrorResourceCache[$error] = $errorResource;
15✔
1055
    }
1056
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc