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

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

0 of 2 new or added lines in 1 file covered. (0.0%)

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

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

80.17
/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\Type;
60
use Symfony\Component\Routing\RouteCollection;
61
use Symfony\Component\Routing\RouterInterface;
62

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

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

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

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

UNCOV
124
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
22✔
UNCOV
125
            $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
22✔
UNCOV
126
            foreach ($resourceMetadataCollection as $resourceMetadata) {
22✔
UNCOV
127
                $this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas, $webhooks, $tags, $context);
22✔
128
            }
129
        }
130

UNCOV
131
        $securitySchemes = $this->getSecuritySchemes();
22✔
UNCOV
132
        $securityRequirements = [];
22✔
133

UNCOV
134
        foreach (array_keys($securitySchemes) as $key) {
22✔
UNCOV
135
            $securityRequirements[] = [$key => []];
22✔
136
        }
137

UNCOV
138
        $globalTags = $this->openApiOptions->getTags() ?: array_values($tags) ?: [];
22✔
139

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

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

UNCOV
167
        $defaultError = $this->getErrorResource($this->openApiOptions->getErrorResourceClass() ?? ApiResourceError::class);
22✔
UNCOV
168
        $defaultValidationError = $this->getErrorResource($this->openApiOptions->getValidationErrorResourceClass() ?? ValidationException::class, 422, 'Unprocessable entity');
22✔
169

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

UNCOV
176
        foreach ($resource->getOperations() as $operationName => $operation) {
22✔
UNCOV
177
            $resourceShortName = $operation->getShortName() ?? $operation;
22✔
178
            // No path to return
UNCOV
179
            if (null === $operation->getUriTemplate() && null === $operation->getRouteName()) {
22✔
180
                continue;
×
181
            }
182

UNCOV
183
            $openapiAttribute = $operation->getOpenapi();
22✔
184

185
            // Operation ignored from OpenApi
UNCOV
186
            if ($operation instanceof HttpOperation && false === $openapiAttribute) {
22✔
UNCOV
187
                continue;
22✔
188
            }
189

190
            // 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
UNCOV
191
            $operationTag = !\is_object($openapiAttribute) ? [] : ($openapiAttribute->getExtensionProperties()[self::API_PLATFORM_TAG] ?? []);
22✔
UNCOV
192
            if (!\is_array($operationTag)) {
22✔
UNCOV
193
                $operationTag = [$operationTag];
19✔
194
            }
195

UNCOV
196
            if ($filteredTags && $filteredTags !== array_intersect($filteredTags, $operationTag)) {
22✔
UNCOV
197
                continue;
2✔
198
            }
199

UNCOV
200
            $resourceClass = $operation->getClass() ?? $resource->getClass();
22✔
UNCOV
201
            $routeName = $operation->getRouteName() ?? $operation->getName();
22✔
202

UNCOV
203
            if (!$this->routeCollection && $this->router) {
22✔
UNCOV
204
                $this->routeCollection = $this->router->getRouteCollection();
22✔
205
            }
206

UNCOV
207
            if ($this->routeCollection && $routeName && $route = $this->routeCollection->get($routeName)) {
22✔
UNCOV
208
                $path = $route->getPath();
22✔
209
            } else {
210
                $path = rtrim($operation->getRoutePrefix() ?? '', '/').'/'.ltrim($operation->getUriTemplate() ?? '', '/');
×
211
            }
212

UNCOV
213
            $path = $this->getPath($path);
22✔
UNCOV
214
            $method = $operation->getMethod() ?? 'GET';
22✔
215

UNCOV
216
            if (!\in_array($method, PathItem::$methods, true)) {
22✔
217
                continue;
×
218
            }
219

UNCOV
220
            $pathItem = null;
22✔
221

UNCOV
222
            if ($openapiAttribute instanceof Webhook) {
22✔
223
                $pathItem = $openapiAttribute->getPathItem() ?: new PathItem();
12✔
224
                $openapiOperation = $pathItem->{'get'.ucfirst(strtolower($method))}() ?: new Operation();
12✔
UNCOV
225
            } elseif (!\is_object($openapiAttribute)) {
22✔
UNCOV
226
                $openapiOperation = new Operation();
20✔
227
            } else {
UNCOV
228
                $openapiOperation = $openapiAttribute;
19✔
229
            }
230

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

UNCOV
248
            foreach ($openapiOperation->getTags() as $v) {
22✔
UNCOV
249
                $tags[$v] = new Tag(name: $v, description: $resource->getDescription());
22✔
250
            }
251

UNCOV
252
            [$requestMimeTypes, $responseMimeTypes] = $this->getMimeTypes($operation);
22✔
253

UNCOV
254
            if ($path) {
22✔
UNCOV
255
                $pathItem = $paths->getPath($path) ?: new PathItem();
22✔
256
            } elseif (!$pathItem) {
×
257
                $pathItem = new PathItem();
×
258
            }
259

UNCOV
260
            $forceSchemaCollection = $operation instanceof CollectionOperationInterface && 'GET' === $method;
22✔
UNCOV
261
            $schema = new Schema('openapi');
22✔
UNCOV
262
            $schema->setDefinitions($schemas);
22✔
263

UNCOV
264
            $operationOutputSchemas = [];
22✔
265

UNCOV
266
            foreach ($responseMimeTypes as $operationFormat) {
22✔
UNCOV
267
                $operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operation, $schema, null, $forceSchemaCollection);
22✔
UNCOV
268
                $operationOutputSchemas[$operationFormat] = $operationOutputSchema;
22✔
UNCOV
269
                $this->appendSchemaDefinitions($schemas, $operationOutputSchema->getDefinitions());
22✔
270
            }
271

272
            // Set up parameters
UNCOV
273
            $openapiParameters = $openapiOperation->getParameters();
22✔
UNCOV
274
            foreach ($operation->getUriVariables() ?? [] as $parameterName => $uriVariable) {
22✔
UNCOV
275
                if ($uriVariable->getExpandedValue() ?? false) {
21✔
UNCOV
276
                    continue;
14✔
277
                }
278

UNCOV
279
                $parameter = new Parameter($parameterName, 'path', $uriVariable->getDescription() ?? "$resourceShortName identifier", $uriVariable->getRequired() ?? true, false, false, $uriVariable->getSchema() ?? ['type' => 'string']);
21✔
280

UNCOV
281
                if ($linkParameter = $uriVariable->getOpenApi()) {
21✔
282
                    $parameter = $this->mergeParameter($parameter, $linkParameter);
×
283
                }
284

UNCOV
285
                if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $parameter)) {
21✔
286
                    $openapiParameters[$i] = $this->mergeParameter($parameter, $operationParameter);
×
287
                    continue;
×
288
                }
289

UNCOV
290
                $openapiParameters[] = $parameter;
21✔
291
            }
292

UNCOV
293
            $openapiOperation = $openapiOperation->withParameters($openapiParameters);
22✔
294

UNCOV
295
            if ($operation instanceof CollectionOperationInterface && 'POST' !== $method) {
22✔
UNCOV
296
                foreach (array_merge($this->getPaginationParameters($operation), $this->getFiltersParameters($operation)) as $parameter) {
22✔
UNCOV
297
                    if ($operationParameter = $this->hasParameter($openapiOperation, $parameter)) {
22✔
298
                        continue;
×
299
                    }
300

UNCOV
301
                    $openapiOperation = $openapiOperation->withParameter($parameter);
22✔
302
                }
303
            }
304

UNCOV
305
            $entityClass = $this->getStateOptionsClass($operation, $operation->getClass());
22✔
UNCOV
306
            $openapiParameters = $openapiOperation->getParameters();
22✔
UNCOV
307
            foreach ($operation->getParameters() ?? [] as $key => $p) {
22✔
UNCOV
308
                if (false === $p->getOpenApi()) {
15✔
UNCOV
309
                    continue;
15✔
310
                }
311

UNCOV
312
                if (($f = $p->getFilter()) && \is_string($f) && $this->filterLocator && $this->filterLocator->has($f)) {
13✔
UNCOV
313
                    $filter = $this->filterLocator->get($f);
13✔
314

UNCOV
315
                    if ($d = $filter->getDescription($entityClass)) {
13✔
UNCOV
316
                        foreach ($d as $name => $description) {
13✔
UNCOV
317
                            if ($prop = $p->getProperty()) {
13✔
318
                                $name = str_replace($prop, $key, $name);
12✔
319
                            }
320

UNCOV
321
                            $openapiParameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $f);
13✔
322
                        }
323

UNCOV
324
                        continue;
13✔
325
                    }
326
                }
327

UNCOV
328
                $in = $p instanceof HeaderParameterInterface ? 'header' : 'query';
13✔
UNCOV
329
                $defaultParameter = new Parameter($key, $in, $p->getDescription() ?? "$resourceShortName $key", $p->getRequired() ?? false, false, false, $p->getSchema() ?? ['type' => 'string']);
13✔
330

UNCOV
331
                $linkParameter = $p->getOpenApi();
13✔
UNCOV
332
                if (null === $linkParameter) {
13✔
UNCOV
333
                    if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $defaultParameter)) {
13✔
334
                        $openapiParameters[$i] = $this->mergeParameter($defaultParameter, $operationParameter);
×
335
                    } else {
UNCOV
336
                        $openapiParameters[] = $defaultParameter;
13✔
337
                    }
338

UNCOV
339
                    continue;
13✔
340
                }
341

UNCOV
342
                if (\is_array($linkParameter)) {
13✔
343
                    foreach ($linkParameter as $lp) {
12✔
344
                        $parameter = $this->mergeParameter($defaultParameter, $lp);
12✔
345
                        if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $parameter)) {
12✔
346
                            $openapiParameters[$i] = $this->mergeParameter($parameter, $operationParameter);
×
347
                            continue;
×
348
                        }
349

350
                        $openapiParameters[] = $parameter;
12✔
351
                    }
352
                    continue;
12✔
353
                }
354

UNCOV
355
                $parameter = $this->mergeParameter($defaultParameter, $linkParameter);
13✔
UNCOV
356
                if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $parameter)) {
13✔
357
                    $openapiParameters[$i] = $this->mergeParameter($parameter, $operationParameter);
×
358
                    continue;
×
359
                }
UNCOV
360
                $openapiParameters[] = $parameter;
13✔
361
            }
362

UNCOV
363
            $openapiOperation = $openapiOperation->withParameters($openapiParameters);
22✔
UNCOV
364
            $existingResponses = $openapiOperation->getResponses() ?: [];
22✔
UNCOV
365
            $overrideResponses = $operation->getExtraProperties()[self::OVERRIDE_OPENAPI_RESPONSES] ?? $this->openApiOptions->getOverrideResponses();
22✔
UNCOV
366
            $errors = null;
22✔
UNCOV
367
            if ($operation instanceof HttpOperation && null !== ($errors = $operation->getErrors())) {
22✔
368
                /** @var array<class-string|string, Error> */
369
                $errorOperations = [];
12✔
370
                foreach ($errors as $error) {
12✔
371
                    $errorOperations[$error] = $this->getErrorResource($error);
12✔
372
                }
373

374
                $openapiOperation = $this->addOperationErrors($openapiOperation, $errorOperations, $resourceMetadataCollection, $schema, $schemas, $operation);
12✔
375
            }
376

UNCOV
377
            if ($overrideResponses || !$existingResponses) {
22✔
378
                // Create responses
379
                switch ($method) {
UNCOV
380
                    case 'GET':
22✔
UNCOV
381
                        $successStatus = (string) $operation->getStatus() ?: 200;
22✔
UNCOV
382
                        $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, \sprintf('%s %s', $resourceShortName, $operation instanceof CollectionOperationInterface ? 'collection' : 'resource'), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas);
22✔
UNCOV
383
                        break;
22✔
UNCOV
384
                    case 'POST':
20✔
UNCOV
385
                        $successStatus = (string) $operation->getStatus() ?: 201;
20✔
UNCOV
386
                        $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, \sprintf('%s resource created', $resourceShortName), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas, $resourceMetadataCollection);
20✔
387

UNCOV
388
                        if (null === $errors) {
20✔
UNCOV
389
                            $openapiOperation = $this->addOperationErrors($openapiOperation, [
20✔
UNCOV
390
                                $defaultError->withStatus(400)->withDescription('Invalid input'),
20✔
UNCOV
391
                                $defaultValidationError,
20✔
UNCOV
392
                            ], $resourceMetadataCollection, $schema, $schemas, $operation);
20✔
393
                        }
UNCOV
394
                        break;
20✔
UNCOV
395
                    case 'PATCH':
19✔
UNCOV
396
                    case 'PUT':
19✔
UNCOV
397
                        $successStatus = (string) $operation->getStatus() ?: 200;
19✔
UNCOV
398
                        $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, \sprintf('%s resource updated', $resourceShortName), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas, $resourceMetadataCollection);
19✔
399

UNCOV
400
                        if (null === $errors) {
19✔
UNCOV
401
                            $openapiOperation = $this->addOperationErrors($openapiOperation, [
19✔
UNCOV
402
                                $defaultError->withStatus(400)->withDescription('Invalid input'),
19✔
UNCOV
403
                                $defaultValidationError,
19✔
UNCOV
404
                            ], $resourceMetadataCollection, $schema, $schemas, $operation);
19✔
405
                        }
UNCOV
406
                        break;
19✔
UNCOV
407
                    case 'DELETE':
19✔
UNCOV
408
                        $successStatus = (string) $operation->getStatus() ?: 204;
19✔
UNCOV
409
                        $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, \sprintf('%s resource deleted', $resourceShortName), $openapiOperation);
19✔
UNCOV
410
                        break;
19✔
411
                }
412
            }
413

UNCOV
414
            if ($overrideResponses && !isset($existingResponses[403]) && $operation->getSecurity()) {
22✔
415
                $openapiOperation = $this->addOperationErrors($openapiOperation, [
12✔
416
                    $defaultError->withStatus(403)->withDescription('Forbidden'),
12✔
417
                ], $resourceMetadataCollection, $schema, $schemas, $operation);
12✔
418
            }
419

UNCOV
420
            if ($overrideResponses && !$operation instanceof CollectionOperationInterface && 'POST' !== $operation->getMethod() && !isset($existingResponses[404]) && null === $errors) {
22✔
UNCOV
421
                $openapiOperation = $this->addOperationErrors($openapiOperation, [
21✔
UNCOV
422
                    $defaultError->withStatus(404)->withDescription('Not found'),
21✔
UNCOV
423
                ], $resourceMetadataCollection, $schema, $schemas, $operation);
21✔
424
            }
425

UNCOV
426
            if (!$openapiOperation->getResponses()) {
22✔
427
                $openapiOperation = $openapiOperation->withResponse('default', new Response('Unexpected error'));
×
428
            }
429

430
            if (
UNCOV
431
                \in_array($method, ['PATCH', 'PUT', 'POST'], true)
22✔
UNCOV
432
                && !(false === ($input = $operation->getInput()) || (\is_array($input) && null === $input['class']))
22✔
433
            ) {
UNCOV
434
                $content = $openapiOperation->getRequestBody()?->getContent();
20✔
UNCOV
435
                if (null === $content) {
20✔
UNCOV
436
                    $operationInputSchemas = [];
20✔
UNCOV
437
                    foreach ($requestMimeTypes as $operationFormat) {
20✔
UNCOV
438
                        $operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operation, $schema, null, $forceSchemaCollection);
20✔
UNCOV
439
                        $operationInputSchemas[$operationFormat] = $operationInputSchema;
20✔
UNCOV
440
                        $this->appendSchemaDefinitions($schemas, $operationInputSchema->getDefinitions());
20✔
441
                    }
UNCOV
442
                    $content = $this->buildContent($requestMimeTypes, $operationInputSchemas);
20✔
443
                }
444

UNCOV
445
                $openapiOperation = $openapiOperation->withRequestBody(new RequestBody(
20✔
UNCOV
446
                    description: $openapiOperation->getRequestBody()?->getDescription() ?? \sprintf('The %s %s resource', 'POST' === $method ? 'new' : 'updated', $resourceShortName),
20✔
UNCOV
447
                    content: $content,
20✔
UNCOV
448
                    required: $openapiOperation->getRequestBody()?->getRequired() ?? true,
20✔
UNCOV
449
                ));
20✔
450
            }
451

UNCOV
452
            if ($openapiAttribute instanceof Webhook) {
22✔
453
                $webhooks[$openapiAttribute->getName()] = $pathItem->{'with'.ucfirst($method)}($openapiOperation);
12✔
454
                continue;
12✔
455
            }
456

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

462
                foreach ($errorResponse->getContent() as $mime => $content) {
×
463
                    $currentResponseContent[$mime] = $content;
×
464
                }
465

466
                $openapiOperation = $existingOperation->withResponse(200, $currentResponse->withContent($currentResponseContent));
×
467
            }
468

UNCOV
469
            $paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
22✔
470
        }
471
    }
472

473
    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
474
    {
UNCOV
475
        if (isset($existingResponses[$status])) {
22✔
476
            return $openapiOperation;
×
477
        }
UNCOV
478
        $responseLinks = $responseContent = null;
22✔
UNCOV
479
        if ($responseMimeTypes && $operationOutputSchemas) {
22✔
UNCOV
480
            $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas);
22✔
481
        }
UNCOV
482
        if ($resourceMetadataCollection && $operation) {
22✔
UNCOV
483
            $responseLinks = $this->getLinks($resourceMetadataCollection, $operation);
21✔
484
        }
485

UNCOV
486
        return $openapiOperation->withResponse($status, new Response($description, $responseContent, null, $responseLinks));
22✔
487
    }
488

489
    /**
490
     * @param array<string, string> $responseMimeTypes
491
     * @param array<string, Schema> $operationSchemas
492
     *
493
     * @return \ArrayObject<MediaType>
494
     */
495
    private function buildContent(array $responseMimeTypes, array $operationSchemas): \ArrayObject
496
    {
497
        /** @var \ArrayObject<MediaType> $content */
UNCOV
498
        $content = new \ArrayObject();
22✔
499

UNCOV
500
        foreach ($responseMimeTypes as $mimeType => $format) {
22✔
UNCOV
501
            $content[$mimeType] = new MediaType(new \ArrayObject($operationSchemas[$format]->getArrayCopy(false)));
22✔
502
        }
503

UNCOV
504
        return $content;
22✔
505
    }
506

507
    /**
508
     * @return array[array<string, string>, array<string, string>]
509
     */
510
    private function getMimeTypes(HttpOperation $operation): array
511
    {
UNCOV
512
        $requestFormats = $operation->getInputFormats() ?: [];
22✔
UNCOV
513
        $responseFormats = $operation->getOutputFormats() ?: [];
22✔
514

UNCOV
515
        $requestMimeTypes = $this->flattenMimeTypes($requestFormats);
22✔
UNCOV
516
        $responseMimeTypes = $this->flattenMimeTypes($responseFormats);
22✔
517

UNCOV
518
        return [$requestMimeTypes, $responseMimeTypes];
22✔
519
    }
520

521
    /**
522
     * @param array<string, string[]> $responseFormats
523
     *
524
     * @return array<string, string>
525
     */
526
    private function flattenMimeTypes(array $responseFormats): array
527
    {
UNCOV
528
        $responseMimeTypes = [];
22✔
UNCOV
529
        foreach ($responseFormats as $responseFormat => $mimeTypes) {
22✔
UNCOV
530
            foreach ($mimeTypes as $mimeType) {
22✔
UNCOV
531
                $responseMimeTypes[$mimeType] = $responseFormat;
22✔
532
            }
533
        }
534

UNCOV
535
        return $responseMimeTypes;
22✔
536
    }
537

538
    /**
539
     * Gets the path for an operation.
540
     *
541
     * If the path ends with the optional _format parameter, it is removed
542
     * as optional path parameters are not yet supported.
543
     *
544
     * @see https://github.com/OAI/OpenAPI-Specification/issues/93
545
     */
546
    private function getPath(string $path): string
547
    {
548
        // Handle either API Platform's URI Template (rfc6570) or Symfony's route
UNCOV
549
        if (str_ends_with($path, '{._format}') || str_ends_with($path, '.{_format}')) {
22✔
UNCOV
550
            $path = substr($path, 0, -10);
22✔
551
        }
552

UNCOV
553
        return str_starts_with($path, '/') ? $path : '/'.$path;
22✔
554
    }
555

556
    private function getPathDescription(string $resourceShortName, string $method, bool $isCollection): string
557
    {
558
        switch ($method) {
UNCOV
559
            case 'GET':
22✔
UNCOV
560
                $pathSummary = $isCollection ? 'Retrieves the collection of %s resources.' : 'Retrieves a %s resource.';
22✔
UNCOV
561
                break;
22✔
UNCOV
562
            case 'POST':
20✔
UNCOV
563
                $pathSummary = 'Creates a %s resource.';
20✔
UNCOV
564
                break;
20✔
UNCOV
565
            case 'PATCH':
19✔
UNCOV
566
                $pathSummary = 'Updates the %s resource.';
19✔
UNCOV
567
                break;
19✔
UNCOV
568
            case 'PUT':
19✔
UNCOV
569
                $pathSummary = 'Replaces the %s resource.';
18✔
UNCOV
570
                break;
18✔
UNCOV
571
            case 'DELETE':
19✔
UNCOV
572
                $pathSummary = 'Removes the %s resource.';
19✔
UNCOV
573
                break;
19✔
574
            default:
575
                return $resourceShortName;
×
576
        }
577

UNCOV
578
        return \sprintf($pathSummary, $resourceShortName);
22✔
579
    }
580

581
    /**
582
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
583
     *
584
     * @return \ArrayObject<Link>
585
     */
586
    private function getLinks(ResourceMetadataCollection $resourceMetadataCollection, HttpOperation $currentOperation): \ArrayObject
587
    {
588
        /** @var \ArrayObject<Link> $links */
UNCOV
589
        $links = new \ArrayObject();
21✔
590

591
        // Only compute get links for now
UNCOV
592
        foreach ($resourceMetadataCollection as $resource) {
21✔
UNCOV
593
            foreach ($resource->getOperations() as $operationName => $operation) {
21✔
UNCOV
594
                $parameters = [];
21✔
UNCOV
595
                $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET';
21✔
596
                if (
UNCOV
597
                    $operationName === $operation->getName()
21✔
UNCOV
598
                    || isset($links[$operationName])
21✔
UNCOV
599
                    || $operation instanceof CollectionOperationInterface
21✔
UNCOV
600
                    || 'GET' !== $method
21✔
601
                ) {
UNCOV
602
                    continue;
21✔
603
                }
604

605
                // Operation ignored from OpenApi
606
                if ($operation instanceof HttpOperation && (false === $operation->getOpenapi() || $operation->getOpenapi() instanceof Webhook)) {
×
607
                    continue;
×
608
                }
609

610
                $operationUriVariables = $operation->getUriVariables();
×
611
                foreach ($currentOperation->getUriVariables() ?? [] as $parameterName => $uriVariableDefinition) {
×
612
                    if (!isset($operationUriVariables[$parameterName])) {
×
613
                        continue;
×
614
                    }
615

616
                    if ($operationUriVariables[$parameterName]->getIdentifiers() === $uriVariableDefinition->getIdentifiers() && $operationUriVariables[$parameterName]->getFromClass() === $uriVariableDefinition->getFromClass()) {
×
617
                        $parameters[$parameterName] = '$request.path.'.($uriVariableDefinition->getIdentifiers()[0] ?? 'id');
×
618
                    }
619
                }
620

621
                foreach ($operationUriVariables ?? [] as $parameterName => $uriVariableDefinition) {
×
622
                    if (isset($parameters[$parameterName])) {
×
623
                        continue;
×
624
                    }
625

626
                    if ($uriVariableDefinition->getFromClass() === $currentOperation->getClass()) {
×
627
                        $parameters[$parameterName] = '$response.body#/'.($uriVariableDefinition->getIdentifiers()[0] ?? 'id');
×
628
                    }
629
                }
630

631
                $links[$operationName] = new Link(
×
632
                    $operationName,
×
633
                    new \ArrayObject($parameters),
×
634
                    null,
×
635
                    $operation->getDescription() ?? ''
×
636
                );
×
637
            }
638
        }
639

UNCOV
640
        return $links;
21✔
641
    }
642

643
    /**
644
     * Gets parameters corresponding to enabled filters.
645
     */
646
    private function getFiltersParameters(CollectionOperationInterface|HttpOperation $operation): array
647
    {
UNCOV
648
        $parameters = [];
22✔
UNCOV
649
        $resourceFilters = $operation->getFilters();
22✔
UNCOV
650
        $entityClass = $this->getStateOptionsClass($operation, $operation->getClass());
22✔
651

UNCOV
652
        foreach ($resourceFilters ?? [] as $filterId) {
22✔
UNCOV
653
            if (!$this->filterLocator->has($filterId)) {
14✔
654
                continue;
×
655
            }
656

UNCOV
657
            $filter = $this->filterLocator->get($filterId);
14✔
UNCOV
658
            foreach ($filter->getDescription($entityClass) as $name => $description) {
14✔
UNCOV
659
                $parameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $filterId);
14✔
660
            }
661
        }
662

UNCOV
663
        return $parameters;
22✔
664
    }
665

666
    /**
667
     * @param array<string, mixed> $description
668
     */
669
    private function getFilterParameter(string $name, array $description, string $shortName, string $filter): Parameter
670
    {
UNCOV
671
        if (isset($description['swagger'])) {
15✔
672
            trigger_deprecation('api-platform/core', '4.0', \sprintf('Using the "swagger" field of the %s::getDescription() (%s) is deprecated.', $filter, $shortName));
×
673
        }
674

UNCOV
675
        if (!isset($description['openapi']) || $description['openapi'] instanceof Parameter) {
15✔
UNCOV
676
            $schema = $description['schema'] ?? [];
15✔
677

UNCOV
678
            if (isset($description['type']) && \in_array($description['type'], Type::$builtinTypes, true) && !isset($schema['type'])) {
15✔
UNCOV
679
                $schema += $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false));
15✔
680
            }
681

UNCOV
682
            if (!isset($schema['type'])) {
15✔
UNCOV
683
                $schema['type'] = 'string';
14✔
684
            }
685

UNCOV
686
            $style = 'array' === ($schema['type'] ?? null) && \in_array(
15✔
UNCOV
687
                $description['type'],
15✔
UNCOV
688
                [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
15✔
UNCOV
689
                true
15✔
UNCOV
690
            ) ? 'deepObject' : 'form';
15✔
691

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

UNCOV
694
            if ('' === $parameter->getDescription() && ($str = $description['description'] ?? '')) {
15✔
695
                $parameter = $parameter->withDescription($str);
×
696
            }
697

UNCOV
698
            if (false === $parameter->getRequired() && false !== ($required = $description['required'] ?? false)) {
15✔
699
                $parameter = $parameter->withRequired($required);
12✔
700
            }
701

UNCOV
702
            return $parameter->withSchema($schema);
15✔
703
        }
704

705
        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));
×
706
        $schema = $description['schema'] ?? (\in_array($description['type'], Type::$builtinTypes, true) ? $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)) : ['type' => 'string']);
×
707

708
        return new Parameter(
×
709
            $name,
×
710
            'query',
×
711
            $description['description'] ?? '',
×
712
            $description['required'] ?? false,
×
713
            $description['openapi']['deprecated'] ?? false,
×
714
            $description['openapi']['allowEmptyValue'] ?? true,
×
715
            $schema,
×
716
            'array' === $schema['type'] && \in_array(
×
717
                $description['type'],
×
718
                [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
×
719
                true
×
720
            ) ? 'deepObject' : 'form',
×
721
            $description['openapi']['explode'] ?? ('array' === $schema['type']),
×
722
            $description['openapi']['allowReserved'] ?? false,
×
723
            $description['openapi']['example'] ?? null,
×
724
            isset(
×
725
                $description['openapi']['examples']
×
726
            ) ? new \ArrayObject($description['openapi']['examples']) : null
×
727
        );
×
728
    }
729

730
    private function getPaginationParameters(CollectionOperationInterface|HttpOperation $operation): array
731
    {
UNCOV
732
        if (!$this->paginationOptions->isPaginationEnabled()) {
22✔
733
            return [];
×
734
        }
735

UNCOV
736
        $parameters = [];
22✔
737

UNCOV
738
        if ($operation->getPaginationEnabled() ?? $this->paginationOptions->isPaginationEnabled()) {
22✔
UNCOV
739
            $parameters[] = new Parameter($this->paginationOptions->getPaginationPageParameterName(), 'query', 'The collection page number', false, false, true, ['type' => 'integer', 'default' => 1]);
22✔
740

UNCOV
741
            if ($operation->getPaginationClientItemsPerPage() ?? $this->paginationOptions->getClientItemsPerPage()) {
22✔
UNCOV
742
                $schema = [
22✔
UNCOV
743
                    'type' => 'integer',
22✔
UNCOV
744
                    'default' => $operation->getPaginationItemsPerPage() ?? $this->paginationOptions->getItemsPerPage(),
22✔
UNCOV
745
                    'minimum' => 0,
22✔
UNCOV
746
                ];
22✔
747

UNCOV
748
                if (null !== $maxItemsPerPage = ($operation->getPaginationMaximumItemsPerPage() ?? $this->paginationOptions->getMaximumItemsPerPage())) {
22✔
749
                    $schema['maximum'] = $maxItemsPerPage;
×
750
                }
751

UNCOV
752
                $parameters[] = new Parameter($this->paginationOptions->getItemsPerPageParameterName(), 'query', 'The number of items per page', false, false, true, $schema);
22✔
753
            }
754
        }
755

UNCOV
756
        if ($operation->getPaginationClientEnabled() ?? $this->paginationOptions->isPaginationClientEnabled()) {
22✔
UNCOV
757
            $parameters[] = new Parameter($this->paginationOptions->getPaginationClientEnabledParameterName(), 'query', 'Enable or disable pagination', false, false, true, ['type' => 'boolean']);
22✔
758
        }
759

UNCOV
760
        return $parameters;
22✔
761
    }
762

763
    private function getOauthSecurityScheme(): SecurityScheme
764
    {
UNCOV
765
        $oauthFlow = new OAuthFlow($this->openApiOptions->getOAuthAuthorizationUrl(), $this->openApiOptions->getOAuthTokenUrl() ?: null, $this->openApiOptions->getOAuthRefreshUrl() ?: null, new \ArrayObject($this->openApiOptions->getOAuthScopes()));
22✔
UNCOV
766
        $description = \sprintf(
22✔
UNCOV
767
            'OAuth 2.0 %s Grant',
22✔
UNCOV
768
            strtolower(preg_replace('/[A-Z]/', ' \\0', lcfirst($this->openApiOptions->getOAuthFlow())))
22✔
UNCOV
769
        );
22✔
UNCOV
770
        $implicit = $password = $clientCredentials = $authorizationCode = null;
22✔
771

UNCOV
772
        switch ($this->openApiOptions->getOAuthFlow()) {
22✔
UNCOV
773
            case 'implicit':
22✔
UNCOV
774
                $implicit = $oauthFlow;
22✔
UNCOV
775
                break;
22✔
776
            case 'password':
×
777
                $password = $oauthFlow;
×
778
                break;
×
779
            case 'application':
×
780
            case 'clientCredentials':
×
781
                $clientCredentials = $oauthFlow;
×
782
                break;
×
783
            case 'accessCode':
×
784
            case 'authorizationCode':
×
785
                $authorizationCode = $oauthFlow;
×
786
                break;
×
787
            default:
788
                throw new \LogicException('OAuth flow must be one of: implicit, password, clientCredentials, authorizationCode');
×
789
        }
790

UNCOV
791
        return new SecurityScheme($this->openApiOptions->getOAuthType(), $description, null, null, null, null, new OAuthFlows($implicit, $password, $clientCredentials, $authorizationCode), null);
22✔
792
    }
793

794
    private function getSecuritySchemes(): array
795
    {
UNCOV
796
        $securitySchemes = [];
22✔
797

UNCOV
798
        if ($this->openApiOptions->getOAuthEnabled()) {
22✔
UNCOV
799
            $securitySchemes['oauth'] = $this->getOauthSecurityScheme();
22✔
800
        }
801

UNCOV
802
        foreach ($this->openApiOptions->getApiKeys() as $key => $apiKey) {
22✔
UNCOV
803
            $description = \sprintf('Value for the %s %s parameter.', $apiKey['name'], $apiKey['type']);
22✔
UNCOV
804
            $securitySchemes[$key] = new SecurityScheme('apiKey', $description, $apiKey['name'], $apiKey['type']);
22✔
805
        }
806

UNCOV
807
        foreach ($this->openApiOptions->getHttpAuth() as $key => $httpAuth) {
22✔
808
            $description = \sprintf('Value for the http %s parameter.', $httpAuth['scheme']);
×
809
            $securitySchemes[$key] = new SecurityScheme('http', $description, null, null, $httpAuth['scheme'], $httpAuth['bearerFormat'] ?? null);
×
810
        }
811

UNCOV
812
        return $securitySchemes;
22✔
813
    }
814

815
    /**
816
     * @param \ArrayObject<string, mixed> $schemas
817
     * @param \ArrayObject<string, mixed> $definitions
818
     */
819
    private function appendSchemaDefinitions(\ArrayObject $schemas, \ArrayObject $definitions): void
820
    {
UNCOV
821
        foreach ($definitions as $key => $value) {
22✔
UNCOV
822
            $schemas[$key] = $value;
22✔
823
        }
824
    }
825

826
    /**
827
     * @return array{0: int, 1: Parameter}|null
828
     */
829
    private function hasParameter(Operation $operation, Parameter $parameter): ?array
830
    {
UNCOV
831
        foreach ($operation->getParameters() as $key => $existingParameter) {
22✔
UNCOV
832
            if ($existingParameter->getName() === $parameter->getName() && $existingParameter->getIn() === $parameter->getIn()) {
22✔
833
                return [$key, $existingParameter];
×
834
            }
835
        }
836

UNCOV
837
        return null;
22✔
838
    }
839

840
    private function mergeParameter(Parameter $actual, Parameter $defined): Parameter
841
    {
842
        foreach (
UNCOV
843
            [
13✔
UNCOV
844
                'name',
13✔
UNCOV
845
                'in',
13✔
UNCOV
846
                'description',
13✔
UNCOV
847
                'required',
13✔
UNCOV
848
                'deprecated',
13✔
UNCOV
849
                'allowEmptyValue',
13✔
UNCOV
850
                'style',
13✔
UNCOV
851
                'explode',
13✔
UNCOV
852
                'allowReserved',
13✔
UNCOV
853
                'example',
13✔
UNCOV
854
            ] as $method
13✔
855
        ) {
UNCOV
856
            $newValue = $defined->{"get$method"}();
13✔
UNCOV
857
            if (null !== $newValue && $actual->{"get$method"}() !== $newValue) {
13✔
UNCOV
858
                $actual = $actual->{"with$method"}($newValue);
13✔
859
            }
860
        }
861

UNCOV
862
        foreach (['examples', 'content', 'schema'] as $method) {
13✔
UNCOV
863
            $newValue = $defined->{"get$method"}();
13✔
UNCOV
864
            if ($newValue && \count($newValue) > 0 && $actual->{"get$method"}() !== $newValue) {
13✔
865
                $actual = $actual->{"with$method"}($newValue);
×
866
            }
867
        }
868

UNCOV
869
        return $actual;
13✔
870
    }
871

872
    /**
873
     * @param ErrorResource[]              $errors
874
     * @param \ArrayObject<string, Schema> $schemas
875
     */
876
    private function addOperationErrors(
877
        Operation $operation,
878
        array $errors,
879
        ResourceMetadataCollection $resourceMetadataCollection,
880
        Schema $schema,
881
        \ArrayObject $schemas,
882
        HttpOperation $originalOperation,
883
    ): Operation {
UNCOV
884
        foreach ($errors as $errorResource) {
21✔
UNCOV
885
            $responseMimeTypes = $this->flattenMimeTypes($errorResource->getOutputFormats() ?: $this->errorFormats);
21✔
UNCOV
886
            foreach ($errorResource->getOperations() as $errorOperation) {
21✔
UNCOV
887
                if (false === $errorOperation->getOpenApi()) {
21✔
UNCOV
888
                    continue;
21✔
889
                }
890

891
                $responseMimeTypes += $this->flattenMimeTypes($errorOperation->getOutputFormats() ?: $this->errorFormats);
12✔
892
            }
893

UNCOV
894
            foreach ($responseMimeTypes as $mime => $format) {
21✔
UNCOV
895
                if (!isset($this->errorFormats[$format])) {
21✔
896
                    unset($responseMimeTypes[$mime]);
×
897
                }
898
            }
899

UNCOV
900
            $operationErrorSchemas = [];
21✔
UNCOV
901
            foreach ($responseMimeTypes as $operationFormat) {
21✔
UNCOV
902
                $operationErrorSchema = $this->jsonSchemaFactory->buildSchema($errorResource->getClass(), $operationFormat, Schema::TYPE_OUTPUT, null, $schema);
21✔
UNCOV
903
                $operationErrorSchemas[$operationFormat] = $operationErrorSchema;
21✔
UNCOV
904
                $this->appendSchemaDefinitions($schemas, $operationErrorSchema->getDefinitions());
21✔
905
            }
906

UNCOV
907
            if (!$status = $errorResource->getStatus()) {
21✔
908
                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()));
×
909
            }
910

UNCOV
911
            $operation = $this->buildOpenApiResponse($operation->getResponses() ?: [], $status, $errorResource->getDescription() ?? '', $operation, $originalOperation, $responseMimeTypes, $operationErrorSchemas, $resourceMetadataCollection);
21✔
912
        }
913

UNCOV
914
        return $operation;
21✔
915
    }
916

917
    /**
918
     * @param string|class-string $error
919
     */
920
    private function getErrorResource(string $error, ?int $status = null, ?string $description = null): ErrorResource
921
    {
UNCOV
922
        if ($this->localErrorResourceCache[$error] ?? null) {
22✔
UNCOV
923
            return $this->localErrorResourceCache[$error];
22✔
924
        }
925

UNCOV
926
        if (is_a($error, ProblemExceptionInterface::class, true)) {
22✔
927
            try {
928
                /** @var ProblemExceptionInterface $exception */
UNCOV
929
                $exception = new $error();
22✔
UNCOV
930
                $status = $exception->getStatus();
22✔
UNCOV
931
                $description = $exception->getTitle();
22✔
UNCOV
932
            } catch (\TypeError) {
22✔
933
            }
934
        } elseif (class_exists($error)) {
×
935
            throw new RuntimeException(\sprintf('The error class "%s" does not implement "%s". Did you forget a use statement?', $error, ProblemExceptionInterface::class));
×
936
        }
937

UNCOV
938
        $defaultErrorResourceClass = $this->openApiOptions->getErrorResourceClass() ?? ApiResourceError::class;
22✔
939

940
        try {
UNCOV
941
            $errorResource = $this->resourceMetadataFactory->create($error)[0] ?? new ErrorResource(status: $status, description: $description, class: $defaultErrorResourceClass);
22✔
UNCOV
942
            if (!($errorResource instanceof ErrorResource)) {
22✔
943
                throw new RuntimeException(\sprintf('The error class %s is not an ErrorResource', $error));
×
944
            }
945

946
            // Here we want the exception status and expression to override the resource one when available
UNCOV
947
            if ($status) {
22✔
UNCOV
948
                $errorResource = $errorResource->withStatus($status);
22✔
949
            }
950

UNCOV
951
            if ($description) {
22✔
UNCOV
952
                $errorResource = $errorResource->withDescription($description);
22✔
953
            }
954
        } catch (ResourceClassNotFoundException|OperationNotFoundException) {
×
955
            $errorResource = new ErrorResource(status: $status, description: $description, class: $defaultErrorResourceClass);
×
956
        }
957

UNCOV
958
        if (!$errorResource->getClass()) {
22✔
959
            $errorResource = $errorResource->withClass($error);
×
960
        }
961

UNCOV
962
        return $this->localErrorResourceCache[$error] = $errorResource;
22✔
963
    }
964
}
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