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

api-platform / core / 7275126587

20 Dec 2023 12:09PM UTC coverage: 37.298% (+0.04%) from 37.262%
7275126587

push

github

soyuka
Merge 3.2

24 of 134 new or added lines in 24 files covered. (17.91%)

4 existing lines in 2 files now uncovered.

10338 of 27717 relevant lines covered (37.3%)

28.58 hits per line

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

61.89
/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\Metadata\Exception\InvalidArgumentException;
17
use ApiPlatform\Metadata\GetCollection;
18
use ApiPlatform\Metadata\Post;
19
use ApiPlatform\Metadata\Tests\Fixtures\StateOptions;
20
use ApiPlatform\OpenApi\Model\ExternalDocumentation;
21
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
22
use ApiPlatform\OpenApi\Model\Parameter;
23
use ApiPlatform\OpenApi\Model\RequestBody;
24
use ApiPlatform\State\OptionsInterface;
25
use Symfony\Component\WebLink\Link;
26
use Symfony\Component\Yaml\Exception\ParseException;
27
use Symfony\Component\Yaml\Yaml;
28

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

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

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

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

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

62
        $this->buildResources($resourcesYaml, $path);
32✔
63
    }
64

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

70
            if (null === $resourceYaml) {
32✔
71
                $resourceYaml = [[]];
32✔
72
            }
73

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

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

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

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

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

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

179
        $uriVariables = [];
32✔
180
        foreach ($resource['uriVariables'] as $parameterName => $data) {
32✔
181
            if (\is_string($data)) {
32✔
182
                $uriVariables[$data] = $data;
×
183
                continue;
×
184
            }
185

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

211
        return $uriVariables;
32✔
212
    }
213

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

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

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

233
            if (\in_array($key, $allowedProperties, true)) {
×
234
                continue;
×
235
            }
236

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

241
        if (\array_key_exists('parameters', $resource['openapi']) && \is_array($openapiParameters = $resource['openapi']['parameters'] ?? [])) {
×
242
            $parameters = [];
×
243
            foreach ($openapiParameters as $parameter) {
×
244
                $parameters[] = new Parameter(
×
245
                    name: $parameter['name'],
×
246
                    in: $parameter['in'],
×
247
                    description: $parameter['description'] ?? '',
×
248
                    required: $parameter['required'] ?? false,
×
249
                    deprecated: $parameter['deprecated'] ?? false,
×
250
                    allowEmptyValue: $parameter['allowEmptyValue'] ?? false,
×
251
                    schema: $parameter['schema'] ?? [],
×
252
                    style: $parameter['style'] ?? null,
×
253
                    explode: $parameter['explode'] ?? false,
×
254
                    allowReserved: $parameter['allowReserved '] ?? false,
×
255
                    example: $parameter['example'] ?? null,
×
256
                    examples: isset($parameter['examples']) ? new \ArrayObject($parameter['examples']) : null,
×
257
                    content: isset($parameter['content']) ? new \ArrayObject($parameter['content']) : null
×
258
                );
×
259
            }
260
            $resource['openapi']['parameters'] = $parameters;
×
261
        }
262

263
        return new OpenApiOperation(...$resource['openapi']);
×
264
    }
265

266
    /**
267
     * @return bool|string|string[]|null
268
     */
269
    private function buildMercure(array $resource): array|bool|string|null
270
    {
271
        if (!\array_key_exists('mercure', $resource)) {
32✔
272
            return null;
32✔
273
        }
274

275
        if (\is_string($resource['mercure'])) {
×
276
            return $this->phpize($resource, 'mercure', 'bool|string');
×
277
        }
278

279
        return $resource['mercure'];
×
280
    }
281

282
    private function buildMessenger(array $resource): bool|array|string|null
283
    {
284
        if (!\array_key_exists('messenger', $resource)) {
32✔
285
            return null;
32✔
286
        }
287

288
        return $this->phpize($resource, 'messenger', 'bool|string');
×
289
    }
290

291
    private function buildOperations(array $resource, array $root): ?array
292
    {
293
        if (!\array_key_exists('operations', $resource)) {
32✔
294
            return null;
32✔
295
        }
296

297
        $data = [];
32✔
298
        foreach ($resource['operations'] as $class => $operation) {
32✔
299
            if (null === $operation) {
32✔
300
                $operation = [];
32✔
301
            }
302

303
            if (\array_key_exists('class', $operation)) {
32✔
304
                if (!\array_key_exists('name', $operation) && \is_string($class)) {
×
305
                    $operation['name'] = $class;
×
306
                }
307
                $class = $operation['class'];
×
308
            }
309

310
            if (empty($class)) {
32✔
311
                throw new InvalidArgumentException('Missing "class" attribute');
×
312
            }
313

314
            if (!class_exists($class)) {
32✔
315
                throw new InvalidArgumentException(sprintf('Operation class "%s" does not exist', $class));
×
316
            }
317

318
            $datum = $this->buildExtendedBase($operation);
32✔
319
            foreach ($datum as $key => $value) {
32✔
320
                if (null === $value) {
32✔
321
                    $datum[$key] = $root[$key];
32✔
322
                }
323
            }
324

325
            if (\in_array((string) $class, [GetCollection::class, Post::class], true)) {
32✔
326
                $datum['itemUriTemplate'] = $this->phpize($operation, 'itemUriTemplate', 'string');
32✔
327
            } elseif (isset($operation['itemUriTemplate'])) {
32✔
NEW
328
                throw new InvalidArgumentException(sprintf('"itemUriTemplate" option is not allowed on a %s operation.', $class));
×
329
            }
330

331
            $data[] = array_merge($datum, [
32✔
332
                'read' => $this->phpize($operation, 'read', 'bool'),
32✔
333
                'deserialize' => $this->phpize($operation, 'deserialize', 'bool'),
32✔
334
                'validate' => $this->phpize($operation, 'validate', 'bool'),
32✔
335
                'write' => $this->phpize($operation, 'write', 'bool'),
32✔
336
                'serialize' => $this->phpize($operation, 'serialize', 'bool'),
32✔
337
                'queryParameterValidate' => $this->phpize($operation, 'queryParameterValidate', 'bool'),
32✔
338
                'priority' => $this->phpize($operation, 'priority', 'integer'),
32✔
339
                'name' => $this->phpize($operation, 'name', 'string'),
32✔
340
                'class' => (string) $class,
32✔
341
            ]);
32✔
342
        }
343

344
        return $data;
32✔
345
    }
346

347
    private function buildGraphQlOperations(array $resource, array $root): ?array
348
    {
349
        if (!\array_key_exists('graphQlOperations', $resource) || !\is_array($resource['graphQlOperations'])) {
32✔
350
            return null;
32✔
351
        }
352

353
        $data = [];
32✔
354
        foreach ($resource['graphQlOperations'] as $class => $operation) {
32✔
355
            if (null === $operation) {
×
356
                $operation = [];
×
357
            }
358

359
            if (\array_key_exists('class', $operation)) {
×
360
                if (!\array_key_exists('name', $operation) && \is_string($class)) {
×
361
                    $operation['name'] = $class;
×
362
                }
363
                $class = $operation['class'];
×
364
            }
365

366
            if (empty($class)) {
×
367
                throw new InvalidArgumentException('Missing "class" attribute');
×
368
            }
369

370
            if (!class_exists($class)) {
×
371
                throw new InvalidArgumentException(sprintf('Operation class "%s" does not exist', $class));
×
372
            }
373

374
            $datum = $this->buildBase($operation);
×
375
            foreach ($datum as $key => $value) {
×
376
                if (null === $value) {
×
377
                    $datum[$key] = $root[$key];
×
378
                }
379
            }
380

381
            $data[] = array_merge($datum, [
×
382
                'resolver' => $this->phpize($operation, 'resolver', 'string'),
×
383
                'args' => $operation['args'] ?? null,
×
384
                'extraArgs' => $operation['extraArgs'] ?? null,
×
385
                'class' => (string) $class,
×
386
                'read' => $this->phpize($operation, 'read', 'bool'),
×
387
                'deserialize' => $this->phpize($operation, 'deserialize', 'bool'),
×
388
                'validate' => $this->phpize($operation, 'validate', 'bool'),
×
389
                'write' => $this->phpize($operation, 'write', 'bool'),
×
390
                'serialize' => $this->phpize($operation, 'serialize', 'bool'),
×
391
                'priority' => $this->phpize($operation, 'priority', 'integer'),
×
392
                'name' => $this->phpize($operation, 'name', 'string'),
×
393
            ]);
×
394
        }
395

396
        return $data ?: null;
32✔
397
    }
398

399
    private function buildStateOptions(array $resource): ?OptionsInterface
400
    {
401
        $stateOptions = $resource['stateOptions'] ?? [];
32✔
402
        if (!\is_array($stateOptions)) {
32✔
403
            return null;
×
404
        }
405

406
        if (!$stateOptions) {
32✔
407
            return null;
32✔
408
        }
409

410
        $configuration = reset($stateOptions);
×
411
        switch (key($stateOptions)) {
×
412
            case 'elasticsearchOptions':
×
413
                return new StateOptions($configuration['index'] ?? null, $configuration['type'] ?? null);
×
414
        }
415

416
        return null;
×
417
    }
418

419
    /**
420
     * @return Link[]
421
     */
422
    private function buildLinks(array $resource): ?array
423
    {
424
        if (!isset($resource['links']) || !\is_array($resource['links'])) {
32✔
425
            return null;
32✔
426
        }
427

428
        $links = [];
×
429
        foreach ($resource['links'] as $link) {
×
430
            $links[] = new Link(rel: $link['rel'], href: $link['href']);
×
431
        }
432

433
        return $links;
×
434
    }
435
}
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