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

api-platform / core / 3713134090

pending completion
3713134090

Pull #5254

github

GitHub
Merge b2ec54b3c into ac711530f
Pull Request #5254: [OpenApi] Add ApiResource::openapi and deprecate openapiContext

197 of 197 new or added lines in 5 files covered. (100.0%)

10372 of 12438 relevant lines covered (83.39%)

11.97 hits per line

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

86.72
/src/Metadata/Extractor/XmlResourceExtractor.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\Metadata\Extractor;
15

16
use ApiPlatform\Exception\InvalidArgumentException;
17
use ApiPlatform\Metadata\GetCollection;
18
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
19
use ApiPlatform\Metadata\GraphQl\Mutation;
20
use ApiPlatform\Metadata\GraphQl\Query;
21
use ApiPlatform\Metadata\GraphQl\QueryCollection;
22
use ApiPlatform\Metadata\GraphQl\Subscription;
23
use ApiPlatform\Metadata\Post;
24
use ApiPlatform\OpenApi\Model\ExternalDocumentation;
25
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
26
use ApiPlatform\OpenApi\Model\Parameter;
27
use ApiPlatform\OpenApi\Model\RequestBody;
28
use Symfony\Component\Config\Util\XmlUtils;
29

30
/**
31
 * Extracts an array of metadata from a list of XML files.
32
 *
33
 * @author Vincent Chalamon <vincentchalamon@gmail.com>
34
 */
35
final class XmlResourceExtractor extends AbstractResourceExtractor
36
{
37
    use ResourceExtractorTrait;
38

39
    public const SCHEMA = __DIR__.'/schema/resources.xsd';
40

41
    /**
42
     * {@inheritdoc}
43
     */
44
    protected function extractPath(string $path): void
45
    {
46
        try {
47
            /** @var \SimpleXMLElement $xml */
48
            $xml = simplexml_import_dom(XmlUtils::loadFile($path, self::SCHEMA));
6✔
49
        } catch (\InvalidArgumentException $e) {
4✔
50
            // Ensure it's not a resource
51
            try {
52
                simplexml_import_dom(XmlUtils::loadFile($path, XmlPropertyExtractor::SCHEMA));
4✔
53
            } catch (\InvalidArgumentException) {
1✔
54
                throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
1✔
55
            }
56

57
            // It's a property: ignore error
58
            return;
3✔
59
        }
60

61
        foreach ($xml->resource as $resource) {
5✔
62
            $base = $this->buildExtendedBase($resource);
5✔
63
            $this->resources[$this->resolve((string) $resource['class'])][] = array_merge($base, [
5✔
64
                'class' => $this->phpize($resource, 'class', 'string'),
5✔
65
                'operations' => $this->buildOperations($resource, $base),
5✔
66
                'graphQlOperations' => $this->buildGraphQlOperations($resource, $base),
5✔
67
            ]);
5✔
68
        }
69
    }
70

71
    private function buildExtendedBase(\SimpleXMLElement $resource): array
72
    {
73
        return array_merge($this->buildBase($resource), [
5✔
74
            'uriTemplate' => $this->phpize($resource, 'uriTemplate', 'string'),
5✔
75
            'routePrefix' => $this->phpize($resource, 'routePrefix', 'string'),
5✔
76
            'stateless' => $this->phpize($resource, 'stateless', 'bool'),
5✔
77
            'sunset' => $this->phpize($resource, 'sunset', 'string'),
5✔
78
            'acceptPatch' => $this->phpize($resource, 'acceptPatch', 'string'),
5✔
79
            'status' => $this->phpize($resource, 'status', 'integer'),
5✔
80
            'host' => $this->phpize($resource, 'host', 'string'),
5✔
81
            'condition' => $this->phpize($resource, 'condition', 'string'),
5✔
82
            'controller' => $this->phpize($resource, 'controller', 'string'),
5✔
83
            'types' => $this->buildArrayValue($resource, 'type'),
5✔
84
            'formats' => $this->buildFormats($resource, 'formats'),
5✔
85
            'inputFormats' => $this->buildFormats($resource, 'inputFormats'),
5✔
86
            'outputFormats' => $this->buildFormats($resource, 'outputFormats'),
5✔
87
            'uriVariables' => $this->buildUriVariables($resource),
5✔
88
            'defaults' => isset($resource->defaults->values) ? $this->buildValues($resource->defaults->values) : null,
5✔
89
            'requirements' => $this->buildRequirements($resource),
5✔
90
            'options' => isset($resource->options->values) ? $this->buildValues($resource->options->values) : null,
5✔
91
            'schemes' => $this->buildArrayValue($resource, 'scheme'),
5✔
92
            'cacheHeaders' => $this->buildCacheHeaders($resource),
5✔
93
            'hydraContext' => isset($resource->hydraContext->values) ? $this->buildValues($resource->hydraContext->values) : null,
5✔
94
            'openapiContext' => isset($resource->openapiContext->values) ? $this->buildValues($resource->openapiContext->values) : null, // TODO Remove in 4.0
5✔
95
            'openapi' => $this->buildOpenapi($resource),
5✔
96
            'paginationViaCursor' => $this->buildPaginationViaCursor($resource),
5✔
97
            'exceptionToStatus' => $this->buildExceptionToStatus($resource),
5✔
98
            'queryParameterValidationEnabled' => $this->phpize($resource, 'queryParameterValidationEnabled', 'bool'),
5✔
99
        ]);
5✔
100
    }
101

102
    private function buildBase(\SimpleXMLElement $resource): array
103
    {
104
        return [
5✔
105
            'shortName' => $this->phpize($resource, 'shortName', 'string'),
5✔
106
            'description' => $this->phpize($resource, 'description', 'string'),
5✔
107
            'urlGenerationStrategy' => $this->phpize($resource, 'urlGenerationStrategy', 'integer'),
5✔
108
            'deprecationReason' => $this->phpize($resource, 'deprecationReason', 'string'),
5✔
109
            'elasticsearch' => $this->phpize($resource, 'elasticsearch', 'bool'),
5✔
110
            'messenger' => $this->phpize($resource, 'messenger', 'bool|string'),
5✔
111
            'mercure' => $this->buildMercure($resource),
5✔
112
            'input' => $this->phpize($resource, 'input', 'bool|string'),
5✔
113
            'output' => $this->phpize($resource, 'output', 'bool|string'),
5✔
114
            'fetchPartial' => $this->phpize($resource, 'fetchPartial', 'bool'),
5✔
115
            'forceEager' => $this->phpize($resource, 'forceEager', 'bool'),
5✔
116
            'paginationClientEnabled' => $this->phpize($resource, 'paginationClientEnabled', 'bool'),
5✔
117
            'paginationClientItemsPerPage' => $this->phpize($resource, 'paginationClientItemsPerPage', 'bool'),
5✔
118
            'paginationClientPartial' => $this->phpize($resource, 'paginationClientPartial', 'bool'),
5✔
119
            'paginationEnabled' => $this->phpize($resource, 'paginationEnabled', 'bool'),
5✔
120
            'paginationFetchJoinCollection' => $this->phpize($resource, 'paginationFetchJoinCollection', 'bool'),
5✔
121
            'paginationUseOutputWalkers' => $this->phpize($resource, 'paginationUseOutputWalkers', 'bool'),
5✔
122
            'paginationItemsPerPage' => $this->phpize($resource, 'paginationItemsPerPage', 'integer'),
5✔
123
            'paginationMaximumItemsPerPage' => $this->phpize($resource, 'paginationMaximumItemsPerPage', 'integer'),
5✔
124
            'paginationPartial' => $this->phpize($resource, 'paginationPartial', 'bool'),
5✔
125
            'paginationType' => $this->phpize($resource, 'paginationType', 'string'),
5✔
126
            'processor' => $this->phpize($resource, 'processor', 'string'),
5✔
127
            'provider' => $this->phpize($resource, 'provider', 'string'),
5✔
128
            'security' => $this->phpize($resource, 'security', 'string'),
5✔
129
            'securityMessage' => $this->phpize($resource, 'securityMessage', 'string'),
5✔
130
            'securityPostDenormalize' => $this->phpize($resource, 'securityPostDenormalize', 'string'),
5✔
131
            'securityPostDenormalizeMessage' => $this->phpize($resource, 'securityPostDenormalizeMessage', 'string'),
5✔
132
            'securityPostValidation' => $this->phpize($resource, 'securityPostValidation', 'string'),
5✔
133
            'securityPostValidationMessage' => $this->phpize($resource, 'securityPostValidationMessage', 'string'),
5✔
134
            'normalizationContext' => isset($resource->normalizationContext->values) ? $this->buildValues($resource->normalizationContext->values) : null,
5✔
135
            'denormalizationContext' => isset($resource->denormalizationContext->values) ? $this->buildValues($resource->denormalizationContext->values) : null,
5✔
136
            'validationContext' => isset($resource->validationContext->values) ? $this->buildValues($resource->validationContext->values) : null,
5✔
137
            'filters' => $this->buildArrayValue($resource, 'filter'),
5✔
138
            'order' => isset($resource->order->values) ? $this->buildValues($resource->order->values) : null,
5✔
139
            'extraProperties' => $this->buildExtraProperties($resource, 'extraProperties'),
5✔
140
            'read' => $this->phpize($resource, 'read', 'bool'),
5✔
141
            'write' => $this->phpize($resource, 'write', 'bool'),
5✔
142
        ];
5✔
143
    }
144

145
    private function buildFormats(\SimpleXMLElement $resource, string $key): ?array
146
    {
147
        if (!isset($resource->{$key}->format)) {
5✔
148
            return null;
5✔
149
        }
150

151
        $data = [];
2✔
152
        foreach ($resource->{$key}->format as $format) {
2✔
153
            if (isset($format['name'])) {
2✔
154
                $data[(string) $format['name']] = (string) $format;
2✔
155
                continue;
2✔
156
            }
157

158
            $data[] = (string) $format;
1✔
159
        }
160

161
        return $data;
2✔
162
    }
163

164
    private function buildOpenapi(\SimpleXMLElement $resource): bool|OpenApiOperation|null
165
    {
166
        if (!isset($resource->openapi) && !isset($resource['openapi'])) {
5✔
167
            return null;
5✔
168
        }
169

170
        if (isset($resource['openapi']) && (\is_bool($resource['openapi']) || \in_array((string) $resource['openapi'], ['1', '0', 'true', 'false'], true))) {
1✔
171
            return $this->phpize($resource, 'openapi', 'bool');
×
172
        }
173

174
        $openapi = $resource->openapi;
1✔
175
        $data = [];
1✔
176
        $attributes = $openapi->attributes();
1✔
177
        foreach ($attributes as $attribute) {
1✔
178
            $data[$attribute->getName()] = $this->phpize($attributes, 'deprecated', 'deprecated' === $attribute->getName() ? 'bool' : 'string');
×
179
        }
180

181
        $data['tags'] = $this->buildArrayValue($resource, 'tag');
1✔
182

183
        if (isset($openapi->responses->response)) {
1✔
184
            foreach ($openapi->responses->response as $response) {
×
185
                $data['responses'][(string) $response->attributes()->status] = [
×
186
                    'description' => $this->phpize($response, 'description', 'string'),
×
187
                    'content' => isset($response->content->values) ? $this->buildValues($response->content->values) : null,
×
188
                    'headers' => isset($response->headers->values) ? $this->buildValues($response->headers->values) : null,
×
189
                    'links' => isset($response->links->values) ? $this->buildValues($response->links->values) : null,
×
190
                ];
×
191
            }
192
        }
193

194
        $data['externalDocs'] = isset($openapi->externalDocs) ? new ExternalDocumentation(
1✔
195
            description: $this->phpize($resource, 'description', 'string'),
1✔
196
            url: $this->phpize($resource, 'url', 'string'),
1✔
197
        ) : null;
1✔
198

199
        if (isset($openapi->parameters->parameter)) {
1✔
200
            foreach ($openapi->parameters->parameter as $parameter) {
×
201
                $data['parameters'][(string) $parameter->attributes()->name] = new Parameter(
×
202
                    name: $this->phpize($parameter, 'name', 'string'),
×
203
                    in: $this->phpize($parameter, 'in', 'string'),
×
204
                    description: $this->phpize($parameter, 'description', 'string'),
×
205
                    required: $this->phpize($parameter, 'required', 'bool'),
×
206
                    deprecated: $this->phpize($parameter, 'deprecated', 'bool'),
×
207
                    allowEmptyValue: $this->phpize($parameter, 'allowEmptyValue', 'bool'),
×
208
                    schema: isset($parameter->schema->values) ? $this->buildValues($parameter->schema->values) : null,
×
209
                    style: $this->phpize($parameter, 'style', 'string'),
×
210
                    explode: $this->phpize($parameter, 'explode', 'bool'),
×
211
                    allowReserved: $this->phpize($parameter, 'allowReserved', 'bool'),
×
212
                    example: $this->phpize($parameter, 'example', 'string'),
×
213
                    examples: isset($parameter->examples->values) ? new \ArrayObject($this->buildValues($parameter->examples->values)) : null,
×
214
                    content: isset($parameter->content->values) ? new \ArrayObject($this->buildValues($parameter->content->values)) : null,
×
215
                );
×
216
            }
217
        }
218
        $data['requestBody'] = isset($openapi->requestBody) ? new RequestBody(
1✔
219
            description: $this->phpize($openapi->requestBody, 'description', 'string'),
1✔
220
            content: isset($openapi->requestBody->content->values) ? new \ArrayObject($this->buildValues($openapi->requestBody->values)) : null,
1✔
221
            required: $this->phpize($openapi->requestBody, 'required', 'bool'),
1✔
222
        ) : null;
1✔
223

224
        $data['callbacks'] = isset($openapi->callbacks->values) ? new \ArrayObject($this->buildValues($openapi->callbacks->values)) : null;
1✔
225

226
        $data['security'] = isset($openapi->security->values) ? $this->buildValues($openapi->security->values) : null;
1✔
227

228
        if (isset($openapi->servers->server)) {
1✔
229
            foreach ($openapi->servers->server as $server) {
×
230
                $data['servers'][] = [
×
231
                    'description' => $this->phpize($server, 'description', 'string'),
×
232
                    'url' => $this->phpize($server, 'url', 'string'),
×
233
                    'variables' => isset($server->variables->values) ? $this->buildValues($server->variables->values) : null,
×
234
                ];
×
235
            }
236
        }
237

238
        $data['extensionProperties'] = isset($openapi->extensionProperties->values) ? $this->buildValues($openapi->extensionProperties->values) : null;
1✔
239

240
        foreach ($data as $key => $value) {
1✔
241
            if (null === $value) {
1✔
242
                unset($data[$key]);
1✔
243
            }
244
        }
245

246
        return new OpenApiOperation(...$data);
1✔
247
    }
248

249
    private function buildUriVariables(\SimpleXMLElement $resource): ?array
250
    {
251
        if (!isset($resource->uriVariables->uriVariable)) {
5✔
252
            return null;
5✔
253
        }
254

255
        $uriVariables = [];
5✔
256
        foreach ($resource->uriVariables->uriVariable as $data) {
5✔
257
            $parameterName = (string) $data['parameterName'];
5✔
258
            if (1 === (null === $data->attributes() ? 0 : \count($data->attributes()))) {
5✔
259
                $uriVariables[$parameterName] = $parameterName;
4✔
260
                continue;
4✔
261
            }
262

263
            if ($fromProperty = $this->phpize($data, 'fromProperty', 'string')) {
5✔
264
                $uriVariables[$parameterName]['from_property'] = $fromProperty;
2✔
265
            }
266
            if ($toProperty = $this->phpize($data, 'toProperty', 'string')) {
5✔
267
                $uriVariables[$parameterName]['to_property'] = $toProperty;
3✔
268
            }
269
            if ($fromClass = $this->phpize($data, 'fromClass', 'string')) {
5✔
270
                $uriVariables[$parameterName]['from_class'] = $fromClass;
5✔
271
            }
272
            if ($toClass = $this->phpize($data, 'toClass', 'string')) {
5✔
273
                $uriVariables[$parameterName]['to_class'] = $toClass;
×
274
            }
275
            if (isset($data->identifiers->values)) {
5✔
276
                $uriVariables[$parameterName]['identifiers'] = $this->buildValues($data->identifiers->values);
×
277
            }
278
            if (null !== ($compositeIdentifier = $this->phpize($data, 'compositeIdentifier', 'bool'))) {
5✔
279
                $uriVariables[$parameterName]['composite_identifier'] = $compositeIdentifier;
1✔
280
            }
281
        }
282

283
        return $uriVariables;
5✔
284
    }
285

286
    private function buildCacheHeaders(\SimpleXMLElement $resource): ?array
287
    {
288
        if (!isset($resource->cacheHeaders->cacheHeader)) {
5✔
289
            return null;
5✔
290
        }
291

292
        $data = [];
2✔
293
        foreach ($resource->cacheHeaders->cacheHeader as $cacheHeader) {
2✔
294
            if (isset($cacheHeader->values->value)) {
2✔
295
                $data[(string) $cacheHeader['name']] = $this->buildValues($cacheHeader->values);
2✔
296
                continue;
2✔
297
            }
298

299
            $data[(string) $cacheHeader['name']] = (string) $cacheHeader;
2✔
300
        }
301

302
        return $data;
2✔
303
    }
304

305
    private function buildRequirements(\SimpleXMLElement $resource): ?array
306
    {
307
        if (!isset($resource->requirements->requirement)) {
5✔
308
            return null;
5✔
309
        }
310

311
        $data = [];
1✔
312
        foreach ($resource->requirements->requirement as $requirement) {
1✔
313
            $data[(string) $requirement->attributes()->property] = (string) $requirement;
1✔
314
        }
315

316
        return $data;
1✔
317
    }
318

319
    private function buildMercure(\SimpleXMLElement $resource): array|bool|null
320
    {
321
        if (!isset($resource->mercure)) {
5✔
322
            return null;
5✔
323
        }
324

325
        if (null !== $resource->mercure->attributes()->private) {
2✔
326
            return ['private' => $this->phpize($resource->mercure->attributes(), 'private', 'bool')];
2✔
327
        }
328

329
        return true;
1✔
330
    }
331

332
    private function buildPaginationViaCursor(\SimpleXMLElement $resource): ?array
333
    {
334
        if (!isset($resource->paginationViaCursor->paginationField)) {
5✔
335
            return null;
5✔
336
        }
337

338
        $data = [];
2✔
339
        foreach ($resource->paginationViaCursor->paginationField as $paginationField) {
2✔
340
            $data[(string) $paginationField['field']] = (string) $paginationField['direction'];
2✔
341
        }
342

343
        return $data;
2✔
344
    }
345

346
    private function buildExceptionToStatus(\SimpleXMLElement $resource): ?array
347
    {
348
        if (!isset($resource->exceptionToStatus->exception)) {
5✔
349
            return null;
5✔
350
        }
351

352
        $data = [];
2✔
353
        foreach ($resource->exceptionToStatus->exception as $exception) {
2✔
354
            $data[(string) $exception['class']] = (int) $exception['statusCode'];
2✔
355
        }
356

357
        return $data;
2✔
358
    }
359

360
    private function buildExtraProperties(\SimpleXMLElement $resource, string $key = null): ?array
361
    {
362
        if (null !== $key) {
5✔
363
            if (!isset($resource->{$key})) {
5✔
364
                return null;
5✔
365
            }
366

367
            $resource = $resource->{$key};
2✔
368
        }
369

370
        return $this->buildValues($resource->values);
2✔
371
    }
372

373
    private function buildOperations(\SimpleXMLElement $resource, array $root): ?array
374
    {
375
        if (!isset($resource->operations->operation)) {
5✔
376
            return null;
5✔
377
        }
378

379
        $data = [];
5✔
380
        foreach ($resource->operations->operation as $operation) {
5✔
381
            $datum = $this->buildExtendedBase($operation);
5✔
382
            foreach ($datum as $key => $value) {
5✔
383
                if (null === $value) {
5✔
384
                    $datum[$key] = $root[$key];
5✔
385
                }
386
            }
387

388
            if (\in_array((string) $operation['class'], [GetCollection::class, Post::class], true)) {
5✔
389
                $datum['itemUriTemplate'] = $this->phpize($operation, 'itemUriTemplate', 'string');
5✔
390
            }
391

392
            $data[] = array_merge($datum, [
5✔
393
                'collection' => $this->phpize($operation, 'collection', 'bool'),
5✔
394
                'class' => (string) $operation['class'],
5✔
395
                'method' => $this->phpize($operation, 'method', 'string'),
5✔
396
                'read' => $this->phpize($operation, 'read', 'bool'),
5✔
397
                'deserialize' => $this->phpize($operation, 'deserialize', 'bool'),
5✔
398
                'validate' => $this->phpize($operation, 'validate', 'bool'),
5✔
399
                'write' => $this->phpize($operation, 'write', 'bool'),
5✔
400
                'serialize' => $this->phpize($operation, 'serialize', 'bool'),
5✔
401
                'queryParameterValidate' => $this->phpize($operation, 'queryParameterValidate', 'bool'),
5✔
402
                'priority' => $this->phpize($operation, 'priority', 'integer'),
5✔
403
                'name' => $this->phpize($operation, 'name', 'string'),
5✔
404
            ]);
5✔
405
        }
406

407
        return $data;
5✔
408
    }
409

410
    private function buildGraphQlOperations(\SimpleXMLElement $resource, array $root): ?array
411
    {
412
        if (!isset($resource->graphQlOperations->mutation) && !isset($resource->graphQlOperations->query) && !isset($resource->graphQlOperations->subscription)) {
5✔
413
            return null;
5✔
414
        }
415

416
        $data = [];
1✔
417
        foreach (['mutation' => Mutation::class, 'query' => Query::class, 'subscription' => Subscription::class] as $type => $class) {
1✔
418
            foreach ($resource->graphQlOperations->{$type} as $operation) {
1✔
419
                $datum = $this->buildBase($operation);
1✔
420
                foreach ($datum as $key => $value) {
1✔
421
                    if (null === $value) {
1✔
422
                        $datum[$key] = $root[$key];
1✔
423
                    }
424
                }
425

426
                $collection = $this->phpize($operation, 'collection', 'bool', false);
1✔
427
                if (Query::class === $class && $collection) {
1✔
428
                    $class = QueryCollection::class;
1✔
429
                }
430

431
                $delete = $this->phpize($operation, 'delete', 'bool', false);
1✔
432
                if (Mutation::class === $class && $delete) {
1✔
433
                    $class = DeleteMutation::class;
×
434
                }
435

436
                $data[] = array_merge($datum, [
1✔
437
                    'graphql_operation_class' => $class,
1✔
438
                    'resolver' => $this->phpize($operation, 'resolver', 'string'),
1✔
439
                    'args' => $this->buildArgs($operation),
1✔
440
                    'class' => $this->phpize($operation, 'class', 'string'),
1✔
441
                    'read' => $this->phpize($operation, 'read', 'bool'),
1✔
442
                    'deserialize' => $this->phpize($operation, 'deserialize', 'bool'),
1✔
443
                    'validate' => $this->phpize($operation, 'validate', 'bool'),
1✔
444
                    'write' => $this->phpize($operation, 'write', 'bool'),
1✔
445
                    'serialize' => $this->phpize($operation, 'serialize', 'bool'),
1✔
446
                    'priority' => $this->phpize($operation, 'priority', 'integer'),
1✔
447
                ]);
1✔
448
            }
449
        }
450

451
        return $data;
1✔
452
    }
453
}
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