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

api-platform / core / 4498909317

pending completion
4498909317

push

github

soyuka
Merge 3.1

364 of 364 new or added lines in 61 files covered. (100.0%)

10781 of 17989 relevant lines covered (59.93%)

11.36 hits per line

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

68.06
/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\RequestBody;
23
use ApiPlatform\State\OptionsInterface;
24
use Symfony\Component\Yaml\Exception\ParseException;
25
use Symfony\Component\Yaml\Yaml;
26

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

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

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

52
        if (null === $resourcesYaml = $resourcesYaml['resources'] ?? $resourcesYaml) {
3✔
53
            return;
×
54
        }
55

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

60
        $this->buildResources($resourcesYaml, $path);
3✔
61
    }
62

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

68
            if (null === $resourceYaml) {
3✔
69
                $resourceYaml = [[]];
3✔
70
            }
71

72
            if (!\array_key_exists(0, $resourceYaml)) {
3✔
73
                $resourceYaml = [$resourceYaml];
3✔
74
            }
75

76
            foreach ($resourceYaml as $key => $resourceYamlDatum) {
3✔
77
                if (null === $resourceYamlDatum) {
3✔
78
                    $resourceYamlDatum = [];
3✔
79
                }
80

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

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

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

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

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

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

208
        return $uriVariables;
3✔
209
    }
210

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

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

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

230
            if (\in_array($key, $allowedProperties, true)) {
×
231
                continue;
×
232
            }
233

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

238
        return new OpenApiOperation(...$resource['openapi']);
×
239
    }
240

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

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

254
        return $resource['mercure'];
×
255
    }
256

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

263
        return $this->phpize($resource, 'messenger', 'bool|string');
×
264
    }
265

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

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

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

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

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

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

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

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

317
        return $data;
3✔
318
    }
319

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

326
        $data = [];
3✔
327
        foreach ($resource['graphQlOperations'] as $class => $operation) {
3✔
328
            if (null === $operation) {
×
329
                $operation = [];
×
330
            }
331

332
            if (\array_key_exists('class', $operation)) {
×
333
                if (!\array_key_exists('name', $operation) && \is_string($class)) {
×
334
                    $operation['name'] = $class;
×
335
                }
336
                $class = $operation['class'];
×
337
            }
338

339
            if (empty($class)) {
×
340
                throw new InvalidArgumentException('Missing "class" attribute');
×
341
            }
342

343
            if (!class_exists($class)) {
×
344
                throw new InvalidArgumentException(sprintf('Operation class "%s" does not exist', $class));
×
345
            }
346

347
            $datum = $this->buildBase($operation);
×
348
            foreach ($datum as $key => $value) {
×
349
                if (null === $value) {
×
350
                    $datum[$key] = $root[$key];
×
351
                }
352
            }
353

354
            $data[] = array_merge($datum, [
×
355
                'resolver' => $this->phpize($operation, 'resolver', 'string'),
×
356
                'args' => $operation['args'] ?? null,
×
357
                'extraArgs' => $operation['extraArgs'] ?? null,
×
358
                'class' => (string) $class,
×
359
                'read' => $this->phpize($operation, 'read', 'bool'),
×
360
                'deserialize' => $this->phpize($operation, 'deserialize', 'bool'),
×
361
                'validate' => $this->phpize($operation, 'validate', 'bool'),
×
362
                'write' => $this->phpize($operation, 'write', 'bool'),
×
363
                'serialize' => $this->phpize($operation, 'serialize', 'bool'),
×
364
                'priority' => $this->phpize($operation, 'priority', 'integer'),
×
365
                'name' => $this->phpize($operation, 'name', 'string'),
×
366
            ]);
×
367
        }
368

369
        return $data ?: null;
3✔
370
    }
371

372
    private function buildStateOptions(array $resource): ?OptionsInterface
373
    {
374
        $stateOptions = $resource['stateOptions'] ?? [];
3✔
375
        if (!\is_array($stateOptions)) {
3✔
376
            return null;
×
377
        }
378

379
        if (!$stateOptions) {
3✔
380
            return null;
3✔
381
        }
382

383
        $configuration = reset($stateOptions);
×
384
        switch (key($stateOptions)) {
×
385
            case 'elasticsearchOptions':
×
386
                return new StateOptions($configuration['index'] ?? null, $configuration['type'] ?? null);
×
387
        }
388

389
        return null;
×
390
    }
391
}
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

© 2026 Coveralls, Inc