• 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

92.08
/src/Metadata/Extractor/YamlResourceExtractor.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\RequestBody;
27
use Symfony\Component\Yaml\Exception\ParseException;
28
use Symfony\Component\Yaml\Yaml;
29

30
/**
31
 * Extracts an array of metadata from a list of YAML files.
32
 *
33
 * @author Antoine Bluchet <soyuka@gmail.com>
34
 * @author Baptiste Meyer <baptiste.meyer@gmail.com>
35
 * @author Kévin Dunglas <dunglas@gmail.com>
36
 * @author Vincent Chalamon <vincentchalamon@gmail.com>
37
 */
38
final class YamlResourceExtractor extends AbstractResourceExtractor
39
{
40
    use ResourceExtractorTrait;
41

42
    /**
43
     * {@inheritdoc}
44
     */
45
    protected function extractPath(string $path): void
46
    {
47
        try {
48
            $resourcesYaml = Yaml::parse((string) file_get_contents($path), Yaml::PARSE_CONSTANT);
8✔
49
        } catch (ParseException $e) {
×
50
            $e->setParsedFile($path);
×
51

52
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
×
53
        }
54

55
        if (null === $resourcesYaml = $resourcesYaml['resources'] ?? $resourcesYaml) {
8✔
56
            return;
×
57
        }
58

59
        if (!\is_array($resourcesYaml)) {
8✔
60
            throw new InvalidArgumentException(sprintf('"resources" setting is expected to be null or an array, %s given in "%s".', \gettype($resourcesYaml), $path));
1✔
61
        }
62

63
        $this->buildResources($resourcesYaml, $path);
7✔
64
    }
65

66
    private function buildResources(array $resourcesYaml, string $path): void
67
    {
68
        foreach ($resourcesYaml as $resourceName => $resourceYaml) {
7✔
69
            $resourceName = $this->resolve($resourceName);
7✔
70

71
            if (null === $resourceYaml) {
7✔
72
                $resourceYaml = [[]];
4✔
73
            }
74

75
            if (!\array_key_exists(0, $resourceYaml)) {
7✔
76
                $resourceYaml = [$resourceYaml];
4✔
77
            }
78

79
            foreach ($resourceYaml as $key => $resourceYamlDatum) {
7✔
80
                if (null === $resourceYamlDatum) {
7✔
81
                    $resourceYamlDatum = [];
5✔
82
                }
83

84
                try {
85
                    $base = $this->buildExtendedBase($resourceYamlDatum);
7✔
86
                    $this->resources[$resourceName][$key] = array_merge($base, [
7✔
87
                        'operations' => $this->buildOperations($resourceYamlDatum, $base),
7✔
88
                        'graphQlOperations' => $this->buildGraphQlOperations($resourceYamlDatum, $base),
7✔
89
                    ]);
7✔
90
                } catch (InvalidArgumentException $exception) {
×
91
                    throw new InvalidArgumentException(sprintf('%s in "%s" (%s).', $exception->getMessage(), $resourceName, $path));
×
92
                }
93
            }
94
        }
95
    }
96

97
    private function buildExtendedBase(array $resource): array
98
    {
99
        return array_merge($this->buildBase($resource), [
7✔
100
            'uriTemplate' => $this->phpize($resource, 'uriTemplate', 'string'),
7✔
101
            'routePrefix' => $this->phpize($resource, 'routePrefix', 'string'),
7✔
102
            'stateless' => $this->phpize($resource, 'stateless', 'bool'),
7✔
103
            'sunset' => $this->phpize($resource, 'sunset', 'string'),
7✔
104
            'acceptPatch' => $this->phpize($resource, 'acceptPatch', 'string'),
7✔
105
            'host' => $this->phpize($resource, 'host', 'string'),
7✔
106
            'condition' => $this->phpize($resource, 'condition', 'string'),
7✔
107
            'controller' => $this->phpize($resource, 'controller', 'string'),
7✔
108
            'queryParameterValidationEnabled' => $this->phpize($resource, 'queryParameterValidationEnabled', 'bool'),
7✔
109
            'types' => $this->buildArrayValue($resource, 'types'),
7✔
110
            'cacheHeaders' => $this->buildArrayValue($resource, 'cacheHeaders'),
7✔
111
            'hydraContext' => $this->buildArrayValue($resource, 'hydraContext'),
7✔
112
            'openapiContext' => $this->buildArrayValue($resource, 'openapiContext'), // TODO Remove in 4.0
7✔
113
            'openapi' => $this->buildOpenapi($resource),
7✔
114
            'paginationViaCursor' => $this->buildArrayValue($resource, 'paginationViaCursor'),
7✔
115
            'exceptionToStatus' => $this->buildArrayValue($resource, 'exceptionToStatus'),
7✔
116
            'defaults' => $this->buildArrayValue($resource, 'defaults'),
7✔
117
            'requirements' => $this->buildArrayValue($resource, 'requirements'),
7✔
118
            'options' => $this->buildArrayValue($resource, 'options'),
7✔
119
            'status' => $this->phpize($resource, 'status', 'integer'),
7✔
120
            'schemes' => $this->buildArrayValue($resource, 'schemes'),
7✔
121
            'formats' => $this->buildArrayValue($resource, 'formats'),
7✔
122
            'uriVariables' => $this->buildUriVariables($resource),
7✔
123
            'inputFormats' => $this->buildArrayValue($resource, 'inputFormats'),
7✔
124
            'outputFormats' => $this->buildArrayValue($resource, 'outputFormats'),
7✔
125
        ]);
7✔
126
    }
127

128
    private function buildBase(array $resource): array
129
    {
130
        return [
7✔
131
            'shortName' => $this->phpize($resource, 'shortName', 'string'),
7✔
132
            'description' => $this->phpize($resource, 'description', 'string'),
7✔
133
            'urlGenerationStrategy' => $this->phpize($resource, 'urlGenerationStrategy', 'integer'),
7✔
134
            'deprecationReason' => $this->phpize($resource, 'deprecationReason', 'string'),
7✔
135
            'elasticsearch' => $this->phpize($resource, 'elasticsearch', 'bool'),
7✔
136
            'fetchPartial' => $this->phpize($resource, 'fetchPartial', 'bool'),
7✔
137
            'forceEager' => $this->phpize($resource, 'forceEager', 'bool'),
7✔
138
            'paginationClientEnabled' => $this->phpize($resource, 'paginationClientEnabled', 'bool'),
7✔
139
            'paginationClientItemsPerPage' => $this->phpize($resource, 'paginationClientItemsPerPage', 'bool'),
7✔
140
            'paginationClientPartial' => $this->phpize($resource, 'paginationClientPartial', 'bool'),
7✔
141
            'paginationEnabled' => $this->phpize($resource, 'paginationEnabled', 'bool'),
7✔
142
            'paginationFetchJoinCollection' => $this->phpize($resource, 'paginationFetchJoinCollection', 'bool'),
7✔
143
            'paginationUseOutputWalkers' => $this->phpize($resource, 'paginationUseOutputWalkers', 'bool'),
7✔
144
            'paginationItemsPerPage' => $this->phpize($resource, 'paginationItemsPerPage', 'integer'),
7✔
145
            'paginationMaximumItemsPerPage' => $this->phpize($resource, 'paginationMaximumItemsPerPage', 'integer'),
7✔
146
            'paginationPartial' => $this->phpize($resource, 'paginationPartial', 'bool'),
7✔
147
            'paginationType' => $this->phpize($resource, 'paginationType', 'string'),
7✔
148
            'processor' => $this->phpize($resource, 'processor', 'string'),
7✔
149
            'provider' => $this->phpize($resource, 'provider', 'string'),
7✔
150
            'security' => $this->phpize($resource, 'security', 'string'),
7✔
151
            'securityMessage' => $this->phpize($resource, 'securityMessage', 'string'),
7✔
152
            'securityPostDenormalize' => $this->phpize($resource, 'securityPostDenormalize', 'string'),
7✔
153
            'securityPostDenormalizeMessage' => $this->phpize($resource, 'securityPostDenormalizeMessage', 'string'),
7✔
154
            'securityPostValidation' => $this->phpize($resource, 'securityPostValidation', 'string'),
7✔
155
            'securityPostValidationMessage' => $this->phpize($resource, 'securityPostValidationMessage', 'string'),
7✔
156
            'input' => $this->phpize($resource, 'input', 'bool|string'),
7✔
157
            'output' => $this->phpize($resource, 'output', 'bool|string'),
7✔
158
            'normalizationContext' => $this->buildArrayValue($resource, 'normalizationContext'),
7✔
159
            'denormalizationContext' => $this->buildArrayValue($resource, 'denormalizationContext'),
7✔
160
            'validationContext' => $this->buildArrayValue($resource, 'validationContext'),
7✔
161
            'filters' => $this->buildArrayValue($resource, 'filters'),
7✔
162
            'order' => $this->buildArrayValue($resource, 'order'),
7✔
163
            'extraProperties' => $this->buildArrayValue($resource, 'extraProperties'),
7✔
164
            'mercure' => $this->buildMercure($resource),
7✔
165
            'messenger' => $this->buildMessenger($resource),
7✔
166
            'read' => $this->phpize($resource, 'read', 'bool'),
7✔
167
            'write' => $this->phpize($resource, 'write', 'bool'),
7✔
168
        ];
7✔
169
    }
170

171
    private function buildUriVariables(array $resource): ?array
172
    {
173
        if (!\array_key_exists('uriVariables', $resource)) {
7✔
174
            return null;
7✔
175
        }
176

177
        $uriVariables = [];
7✔
178
        foreach ($resource['uriVariables'] as $parameterName => $data) {
7✔
179
            if (\is_string($data)) {
7✔
180
                $uriVariables[$data] = $data;
3✔
181
                continue;
3✔
182
            }
183

184
            if (2 === (is_countable($data) ? \count($data) : 0) && isset($data[0]) && isset($data[1])) {
5✔
185
                $data['fromClass'] = $data[0];
5✔
186
                $data['fromProperty'] = $data[1];
5✔
187
                unset($data[0], $data[1]);
5✔
188
            }
189
            if (isset($data['fromClass'])) {
5✔
190
                $uriVariables[$parameterName]['from_class'] = $data['fromClass'];
5✔
191
            }
192
            if (isset($data['fromProperty'])) {
5✔
193
                $uriVariables[$parameterName]['from_property'] = $data['fromProperty'];
5✔
194
            }
195
            if (isset($data['toClass'])) {
5✔
196
                $uriVariables[$parameterName]['to_class'] = $data['toClass'];
×
197
            }
198
            if (isset($data['toProperty'])) {
5✔
199
                $uriVariables[$parameterName]['to_property'] = $data['toProperty'];
3✔
200
            }
201
            if (isset($data['identifiers'])) {
5✔
202
                $uriVariables[$parameterName]['identifiers'] = $data['identifiers'];
×
203
            }
204
            if (isset($data['compositeIdentifier'])) {
5✔
205
                $uriVariables[$parameterName]['composite_identifier'] = $data['compositeIdentifier'];
1✔
206
            }
207
        }
208

209
        return $uriVariables;
7✔
210
    }
211

212
    private function buildOpenapi(array $resource): bool|OpenApiOperation|null
213
    {
214
        if (!\array_key_exists('openapi', $resource)) {
7✔
215
            return null;
7✔
216
        }
217

218
        if (!\is_array($resource['openapi'])) {
1✔
219
            return $this->phpize($resource, 'openapi', 'bool');
×
220
        }
221

222
        $allowedProperties = array_map(fn (\ReflectionProperty $reflProperty): string => $reflProperty->getName(), (new \ReflectionClass(OpenApiOperation::class))->getProperties());
1✔
223
        foreach ($resource['openapi'] as $key => $value) {
1✔
224
            $resource['openapi'][$key] = match ($key) {
1✔
225
                'externalDocs' => new ExternalDocumentation(description: $value['description'] ?? '', url: $value['url'] ?? ''),
1✔
226
                'requestBody' => new RequestBody(description: $value['description'] ?? '', content: isset($value['content']) ? new \ArrayObject($value['content'] ?? []) : null, required: $value['required'] ?? false),
1✔
227
                'callbacks' => new \ArrayObject($value ?? []),
1✔
228
                default => $value,
1✔
229
            };
1✔
230

231
            if (\in_array($key, $allowedProperties, true)) {
1✔
232
                continue;
1✔
233
            }
234

235
            $resource['openapi']['extensionProperties'][$key] = $value;
×
236
            unset($resource['openapi'][$key]);
×
237
        }
238

239
        return new OpenApiOperation(...$resource['openapi']);
1✔
240
    }
241

242
    /**
243
     * @return bool|string|string[]|null
244
     */
245
    private function buildMercure(array $resource): array|bool|string|null
246
    {
247
        if (!\array_key_exists('mercure', $resource)) {
7✔
248
            return null;
7✔
249
        }
250

251
        if (\is_string($resource['mercure'])) {
1✔
252
            return $this->phpize($resource, 'mercure', 'bool|string');
×
253
        }
254

255
        return $resource['mercure'];
1✔
256
    }
257

258
    private function buildMessenger(array $resource): bool|array|string|null
259
    {
260
        if (!\array_key_exists('messenger', $resource)) {
7✔
261
            return null;
7✔
262
        }
263

264
        return $this->phpize($resource, 'messenger', 'bool|string');
1✔
265
    }
266

267
    private function buildOperations(array $resource, array $root): ?array
268
    {
269
        if (!\array_key_exists('operations', $resource)) {
7✔
270
            return null;
5✔
271
        }
272

273
        $data = [];
7✔
274
        foreach ($resource['operations'] as $class => $operation) {
7✔
275
            if (null === $operation) {
7✔
276
                $operation = [];
4✔
277
            }
278

279
            if (\array_key_exists('class', $operation)) {
7✔
280
                if (!\array_key_exists('name', $operation) && \is_string($class)) {
1✔
281
                    $operation['name'] = $class;
×
282
                }
283
                $class = $operation['class'];
1✔
284
            }
285

286
            if (empty($class)) {
7✔
287
                throw new InvalidArgumentException('Missing "class" attribute');
×
288
            }
289

290
            if (!class_exists($class)) {
7✔
291
                throw new InvalidArgumentException(sprintf('Operation class "%s" does not exist', $class));
×
292
            }
293

294
            $datum = $this->buildExtendedBase($operation);
7✔
295
            foreach ($datum as $key => $value) {
7✔
296
                if (null === $value) {
7✔
297
                    $datum[$key] = $root[$key];
7✔
298
                }
299
            }
300

301
            if (\in_array((string) $class, [GetCollection::class, Post::class], true)) {
7✔
302
                $datum['itemUriTemplate'] = $this->phpize($operation, 'itemUriTemplate', 'string');
7✔
303
            }
304

305
            $data[] = array_merge($datum, [
7✔
306
                'read' => $this->phpize($operation, 'read', 'bool'),
7✔
307
                'deserialize' => $this->phpize($operation, 'deserialize', 'bool'),
7✔
308
                'validate' => $this->phpize($operation, 'validate', 'bool'),
7✔
309
                'write' => $this->phpize($operation, 'write', 'bool'),
7✔
310
                'serialize' => $this->phpize($operation, 'serialize', 'bool'),
7✔
311
                'queryParameterValidate' => $this->phpize($operation, 'queryParameterValidate', 'bool'),
7✔
312
                'priority' => $this->phpize($operation, 'priority', 'integer'),
7✔
313
                'name' => $this->phpize($operation, 'name', 'string'),
7✔
314
                'class' => (string) $class,
7✔
315
            ]);
7✔
316
        }
317

318
        return $data;
7✔
319
    }
320

321
    private function buildGraphQlOperations(array $resource, array $root): ?array
322
    {
323
        if (!\array_key_exists('graphQlOperations', $resource) || !\is_array($resource['graphQlOperations'])) {
7✔
324
            return null;
7✔
325
        }
326

327
        $data = [];
4✔
328
        foreach (['mutations' => Mutation::class, 'queries' => Query::class, 'subscriptions' => Subscription::class] as $type => $class) {
4✔
329
            if (!\array_key_exists($type, $resource['graphQlOperations'])) {
4✔
330
                continue;
3✔
331
            }
332

333
            foreach ($resource['graphQlOperations'][$type] as $operation) {
1✔
334
                $datum = $this->buildBase($operation);
1✔
335
                foreach ($datum as $key => $value) {
1✔
336
                    if (null === $value) {
1✔
337
                        $datum[$key] = $root[$key];
1✔
338
                    }
339
                }
340

341
                $collection = $this->phpize($operation, 'collection', 'bool', false);
1✔
342
                if (Query::class === $class && $collection) {
1✔
343
                    $class = QueryCollection::class;
1✔
344
                }
345

346
                $delete = $this->phpize($operation, 'delete', 'bool', false);
1✔
347
                if (Mutation::class === $class && $delete) {
1✔
348
                    $class = DeleteMutation::class;
×
349
                }
350

351
                $data[] = array_merge($datum, [
1✔
352
                    'graphql_operation_class' => $class,
1✔
353
                    'resolver' => $this->phpize($operation, 'resolver', 'string'),
1✔
354
                    'args' => $operation['args'] ?? null,
1✔
355
                    'class' => $this->phpize($operation, 'class', 'string'),
1✔
356
                    'read' => $this->phpize($operation, 'read', 'bool'),
1✔
357
                    'deserialize' => $this->phpize($operation, 'deserialize', 'bool'),
1✔
358
                    'validate' => $this->phpize($operation, 'validate', 'bool'),
1✔
359
                    'write' => $this->phpize($operation, 'write', 'bool'),
1✔
360
                    'serialize' => $this->phpize($operation, 'serialize', 'bool'),
1✔
361
                    'priority' => $this->phpize($operation, 'priority', 'integer'),
1✔
362
                ]);
1✔
363
            }
364
        }
365

366
        return $data ?: null;
4✔
367
    }
368
}
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