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

api-platform / core / 16705318661

03 Aug 2025 01:05PM UTC coverage: 0.0% (-21.9%) from 21.944%
16705318661

Pull #7317

github

web-flow
Merge 1ca8642ff into d06b1a0a0
Pull Request #7317: Fix/4372 skip null values in hal

0 of 14 new or added lines in 3 files covered. (0.0%)

11680 existing lines in 376 files now uncovered.

0 of 51817 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/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
    ) {
UNCOV
103
        $this->filterLocator = $filterLocator;
×
UNCOV
104
        $this->openApiOptions = $openApiOptions ?: new Options('API Platform');
×
UNCOV
105
        $this->paginationOptions = $paginationOptions ?: new PaginationOptions();
×
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
    {
UNCOV
117
        $baseUrl = $context[self::BASE_URL] ?? '/';
×
UNCOV
118
        $contact = null === $this->openApiOptions->getContactUrl() || null === $this->openApiOptions->getContactEmail() ? null : new Contact($this->openApiOptions->getContactName(), $this->openApiOptions->getContactUrl(), $this->openApiOptions->getContactEmail());
×
UNCOV
119
        $license = null === $this->openApiOptions->getLicenseName() ? null : new License($this->openApiOptions->getLicenseName(), $this->openApiOptions->getLicenseUrl(), $this->openApiOptions->getLicenseIdentifier());
×
UNCOV
120
        $info = new Info($this->openApiOptions->getTitle(), $this->openApiOptions->getVersion(), trim($this->openApiOptions->getDescription()), $this->openApiOptions->getTermsOfService(), $contact, $license);
×
UNCOV
121
        $servers = '/' === $baseUrl || '' === $baseUrl ? [new Server('/')] : [new Server($baseUrl)];
×
UNCOV
122
        $paths = new Paths();
×
UNCOV
123
        $schemas = new \ArrayObject();
×
UNCOV
124
        $webhooks = new \ArrayObject();
×
UNCOV
125
        $tags = [];
×
126

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

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

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

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

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

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

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

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

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

UNCOV
186
            $openapiAttribute = $operation->getOpenapi();
×
187

188
            // Operation ignored from OpenApi
UNCOV
189
            if (false === $openapiAttribute) {
×
UNCOV
190
                continue;
×
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
UNCOV
194
            $operationTag = !\is_object($openapiAttribute) ? [] : ($openapiAttribute->getExtensionProperties()[self::API_PLATFORM_TAG] ?? []);
×
UNCOV
195
            if (!\is_array($operationTag)) {
×
UNCOV
196
                $operationTag = [$operationTag];
×
197
            }
198

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

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

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

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

UNCOV
216
            $path = $this->getPath($path);
×
UNCOV
217
            $method = $operation->getMethod();
×
218

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

UNCOV
223
            $pathItem = null;
×
224

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

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

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

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

UNCOV
257
            if (null === $pathItem) {
×
UNCOV
258
                $pathItem = $paths->getPath($path) ?? new PathItem();
×
259
            }
260

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

UNCOV
265
            $operationOutputSchemas = [];
×
266

UNCOV
267
            foreach ($responseMimeTypes as $operationFormat) {
×
UNCOV
268
                $operationOutputSchema = null;
×
269
                // Having JSONSchema for non-json schema makes no sense
UNCOV
270
                if (str_starts_with($operationFormat, 'json')) {
×
UNCOV
271
                    $operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operation, $schema, null, $forceSchemaCollection);
×
UNCOV
272
                    $this->appendSchemaDefinitions($schemas, $operationOutputSchema->getDefinitions());
×
273
                }
274

UNCOV
275
                $operationOutputSchemas[$operationFormat] = $operationOutputSchema;
×
276
            }
277

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

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

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

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

UNCOV
304
                $openapiParameters[] = $parameter;
×
305
            }
306

UNCOV
307
            $openapiOperation = $openapiOperation->withParameters($openapiParameters);
×
308

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

UNCOV
315
                    $openapiOperation = $openapiOperation->withParameter($parameter);
×
316
                }
317
            }
318

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

UNCOV
326
                if (($f = $p->getFilter()) && \is_string($f) && $this->filterLocator && $this->filterLocator->has($f)) {
×
UNCOV
327
                    $filter = $this->filterLocator->get($f);
×
328

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

UNCOV
335
                            $openapiParameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $f);
×
336
                        }
337

UNCOV
338
                        continue;
×
339
                    }
340
                }
341

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

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

UNCOV
361
                    continue;
×
362
                }
363

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

372
                        $openapiParameters[] = $parameter;
×
373
                    }
374
                    continue;
×
375
                }
376

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

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

396
                $openapiOperation = $this->addOperationErrors($openapiOperation, $errorOperations, $resourceMetadataCollection, $schema, $schemas, $operation);
×
397
            }
398

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

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

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

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

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

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

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

UNCOV
466
                        $operationInputSchemas[$operationFormat] = $operationInputSchema;
×
467
                    }
UNCOV
468
                    $content = $this->buildContent($requestMimeTypes, $operationInputSchemas);
×
469
                }
470

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

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

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

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

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

UNCOV
495
            $paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
×
496
        }
497
    }
498

499
    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
500
    {
UNCOV
501
        if (isset($existingResponses[$status])) {
×
502
            return $openapiOperation;
×
503
        }
UNCOV
504
        $responseLinks = $responseContent = null;
×
UNCOV
505
        if ($responseMimeTypes && $operationOutputSchemas) {
×
UNCOV
506
            $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas);
×
507
        }
UNCOV
508
        if ($resourceMetadataCollection && $operation) {
×
UNCOV
509
            $responseLinks = $this->getLinks($resourceMetadataCollection, $operation);
×
510
        }
511

UNCOV
512
        return $openapiOperation->withResponse($status, new Response($description, $responseContent, null, $responseLinks));
×
513
    }
514

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

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

UNCOV
530
        return $content;
×
531
    }
532

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

UNCOV
541
        $requestMimeTypes = $this->flattenMimeTypes($requestFormats);
×
UNCOV
542
        $responseMimeTypes = $this->flattenMimeTypes($responseFormats);
×
543

UNCOV
544
        return [$requestMimeTypes, $responseMimeTypes];
×
545
    }
546

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

UNCOV
561
        return $responseMimeTypes;
×
562
    }
563

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

UNCOV
579
        return str_starts_with($path, '/') ? $path : '/'.$path;
×
580
    }
581

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

UNCOV
604
        return \sprintf($pathSummary, $resourceShortName);
×
605
    }
606

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

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

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

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

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

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

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

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

UNCOV
666
        return $links;
×
667
    }
668

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

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

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

UNCOV
689
        return $parameters;
×
690
    }
691

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

UNCOV
701
        if (!isset($description['openapi']) || $description['openapi'] instanceof Parameter) {
×
UNCOV
702
            $schema = $description['schema'] ?? [];
×
703

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

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

UNCOV
720
            if (!isset($schema['type'])) {
×
UNCOV
721
                $schema['type'] = 'string';
×
722
            }
723

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

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

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

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

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

UNCOV
743
            return $parameter->withSchema($schema);
×
744
        }
745

746
        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));
×
747

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

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

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

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

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

UNCOV
800
        $parameters = [];
×
801

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

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

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

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

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

UNCOV
848
        if ($operation->getPaginationClientPartial() ?? $this->paginationOptions->isClientPartialPaginationEnabled()) {
×
UNCOV
849
            $parameters[] = new Parameter($this->paginationOptions->getPartialPaginationParameterName(), 'query', 'Enable or disable partial pagination', false, false, true, ['type' => 'boolean']);
×
850
        }
851

UNCOV
852
        return $parameters;
×
853
    }
854

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

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

UNCOV
883
        return new SecurityScheme($this->openApiOptions->getOAuthType(), $description, null, null, null, null, new OAuthFlows($implicit, $password, $clientCredentials, $authorizationCode), null);
×
884
    }
885

886
    private function getSecuritySchemes(): array
887
    {
UNCOV
888
        $securitySchemes = [];
×
889

UNCOV
890
        if ($this->openApiOptions->getOAuthEnabled()) {
×
UNCOV
891
            $securitySchemes['oauth'] = $this->getOauthSecurityScheme();
×
892
        }
893

UNCOV
894
        foreach ($this->openApiOptions->getApiKeys() as $key => $apiKey) {
×
UNCOV
895
            $description = \sprintf('Value for the %s %s parameter.', $apiKey['name'], $apiKey['type']);
×
UNCOV
896
            $securitySchemes[$key] = new SecurityScheme('apiKey', $description, $apiKey['name'], $apiKey['type']);
×
897
        }
898

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

UNCOV
904
        return $securitySchemes;
×
905
    }
906

907
    /**
908
     * @param \ArrayObject<string, mixed> $schemas
909
     * @param \ArrayObject<string, mixed> $definitions
910
     */
911
    private function appendSchemaDefinitions(\ArrayObject $schemas, \ArrayObject $definitions): void
912
    {
UNCOV
913
        foreach ($definitions as $key => $value) {
×
UNCOV
914
            $schemas[$key] = $value;
×
915
        }
916
    }
917

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

UNCOV
929
        return null;
×
930
    }
931

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

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

UNCOV
961
        return $actual;
×
962
    }
963

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

983
                $responseMimeTypes += $this->flattenMimeTypes($errorOperation->getOutputFormats() ?: $this->errorFormats);
×
984
            }
985

UNCOV
986
            foreach ($responseMimeTypes as $mime => $format) {
×
UNCOV
987
                if (!isset($this->errorFormats[$format])) {
×
988
                    unset($responseMimeTypes[$mime]);
×
989
                }
990
            }
991

UNCOV
992
            $operationErrorSchemas = [];
×
UNCOV
993
            foreach ($responseMimeTypes as $operationFormat) {
×
UNCOV
994
                $operationErrorSchema = null;
×
995
                // Having JSONSchema for non-json schema makes no sense
UNCOV
996
                if (str_starts_with($operationFormat, 'json')) {
×
UNCOV
997
                    $operationErrorSchema = $this->jsonSchemaFactory->buildSchema($errorResource->getClass(), $operationFormat, Schema::TYPE_OUTPUT, null, $schema);
×
UNCOV
998
                    $this->appendSchemaDefinitions($schemas, $operationErrorSchema->getDefinitions());
×
999
                }
UNCOV
1000
                $operationErrorSchemas[$operationFormat] = $operationErrorSchema;
×
1001
            }
1002

UNCOV
1003
            if (!$status = $errorResource->getStatus()) {
×
1004
                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()));
×
1005
            }
1006

UNCOV
1007
            $operation = $this->buildOpenApiResponse($operation->getResponses() ?: [], $status, $errorResource->getDescription() ?? '', $operation, $originalOperation, $responseMimeTypes, $operationErrorSchemas, $resourceMetadataCollection);
×
1008
        }
1009

UNCOV
1010
        return $operation;
×
1011
    }
1012

1013
    /**
1014
     * @param string|class-string $error
1015
     */
1016
    private function getErrorResource(string $error, ?int $status = null, ?string $description = null): ErrorResource
1017
    {
UNCOV
1018
        if ($this->localErrorResourceCache[$error] ?? null) {
×
UNCOV
1019
            return $this->localErrorResourceCache[$error];
×
1020
        }
1021

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

UNCOV
1034
        $defaultErrorResourceClass = $this->openApiOptions->getErrorResourceClass() ?? ApiResourceError::class;
×
1035

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

1042
            // Here we want the exception status and expression to override the resource one when available
UNCOV
1043
            if ($status) {
×
UNCOV
1044
                $errorResource = $errorResource->withStatus($status);
×
1045
            }
1046

UNCOV
1047
            if ($description) {
×
UNCOV
1048
                $errorResource = $errorResource->withDescription($description);
×
1049
            }
1050
        } catch (ResourceClassNotFoundException|OperationNotFoundException) {
×
1051
            $errorResource = new ErrorResource(status: $status, description: $description, class: $defaultErrorResourceClass);
×
1052
        }
1053

UNCOV
1054
        if (!$errorResource->getClass()) {
×
1055
            $errorResource = $errorResource->withClass($error);
×
1056
        }
1057

UNCOV
1058
        return $this->localErrorResourceCache[$error] = $errorResource;
×
1059
    }
1060
}
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