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

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

0 of 2 new or added lines in 1 file covered. (0.0%)

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

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

100.0
/src/JsonApi/JsonSchema/SchemaFactory.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\JsonApi\JsonSchema;
15

16
use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface;
17
use ApiPlatform\JsonSchema\ResourceMetadataTrait;
18
use ApiPlatform\JsonSchema\Schema;
19
use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface;
20
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
21
use ApiPlatform\Metadata\Operation;
22
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
23
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
24
use ApiPlatform\Metadata\ResourceClassResolverInterface;
25
use ApiPlatform\State\ApiResource\Error;
26

27
/**
28
 * Decorator factory which adds JSON:API properties to the JSON Schema document.
29
 *
30
 * @author Gwendolen Lynch <gwendolen.lynch@gmail.com>
31
 */
32
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
33
{
34
    use ResourceMetadataTrait;
35

36
    /**
37
     * As JSON:API recommends using [includes](https://jsonapi.org/format/#fetching-includes) instead of groups
38
     * this flag allows to force using groups to generate the JSON:API JSONSchema. Defaults to true, use it in
39
     * a serializer context.
40
     */
41
    public const DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS = 'disable_json_schema_serializer_groups';
42

43
    private const LINKS_PROPS = [
44
        'type' => 'object',
45
        'properties' => [
46
            'self' => [
47
                'type' => 'string',
48
                'format' => 'iri-reference',
49
            ],
50
            'first' => [
51
                'type' => 'string',
52
                'format' => 'iri-reference',
53
            ],
54
            'prev' => [
55
                'type' => 'string',
56
                'format' => 'iri-reference',
57
            ],
58
            'next' => [
59
                'type' => 'string',
60
                'format' => 'iri-reference',
61
            ],
62
            'last' => [
63
                'type' => 'string',
64
                'format' => 'iri-reference',
65
            ],
66
        ],
67
        'example' => [
68
            'self' => 'string',
69
            'first' => 'string',
70
            'prev' => 'string',
71
            'next' => 'string',
72
            'last' => 'string',
73
        ],
74
    ];
75
    private const META_PROPS = [
76
        'type' => 'object',
77
        'properties' => [
78
            'totalItems' => [
79
                'type' => 'integer',
80
                'minimum' => 0,
81
            ],
82
            'itemsPerPage' => [
83
                'type' => 'integer',
84
                'minimum' => 0,
85
            ],
86
            'currentPage' => [
87
                'type' => 'integer',
88
                'minimum' => 0,
89
            ],
90
        ],
91
    ];
92
    private const RELATION_PROPS = [
93
        'type' => 'object',
94
        'properties' => [
95
            'type' => [
96
                'type' => 'string',
97
            ],
98
            'id' => [
99
                'type' => 'string',
100
                'format' => 'iri-reference',
101
            ],
102
        ],
103
    ];
104
    private const PROPERTY_PROPS = [
105
        'id' => [
106
            'type' => 'string',
107
        ],
108
        'type' => [
109
            'type' => 'string',
110
        ],
111
        'attributes' => [
112
            'type' => 'object',
113
            'properties' => [],
114
        ],
115
    ];
116

117
    public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly ?DefinitionNameFactoryInterface $definitionNameFactory = null)
118
    {
UNCOV
119
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
824✔
UNCOV
120
            $this->schemaFactory->setSchemaFactory($this);
824✔
121
        }
UNCOV
122
        $this->resourceClassResolver = $resourceClassResolver;
824✔
UNCOV
123
        $this->resourceMetadataFactory = $resourceMetadataFactory;
824✔
124
    }
125

126
    /**
127
     * {@inheritdoc}
128
     */
129
    public function buildSchema(string $className, string $format = 'jsonapi', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
130
    {
UNCOV
131
        if ('jsonapi' !== $format) {
61✔
UNCOV
132
            return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
53✔
133
        }
134
        // We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources.
135
        // That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes
UNCOV
136
        $serializerContext ??= $this->getSerializerContext($operation ?? $this->findOperation($className, $type, $operation, $serializerContext, $format), $type);
30✔
UNCOV
137
        $jsonApiSerializerContext = !($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) ? $serializerContext : [];
30✔
UNCOV
138
        $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection);
30✔
139

UNCOV
140
        if (($key = $schema->getRootDefinitionKey()) || ($key = $schema->getItemsDefinitionKey())) {
30✔
UNCOV
141
            $definitions = $schema->getDefinitions();
30✔
UNCOV
142
            $properties = $definitions[$key]['properties'] ?? [];
30✔
143

UNCOV
144
            if (Error::class === $className && !isset($properties['errors'])) {
30✔
UNCOV
145
                $definitions[$key]['properties'] = [
21✔
UNCOV
146
                    'errors' => [
21✔
UNCOV
147
                        'type' => 'object',
21✔
UNCOV
148
                        'properties' => $properties,
21✔
UNCOV
149
                    ],
21✔
UNCOV
150
                ];
21✔
151

UNCOV
152
                return $schema;
21✔
153
            }
154

155
            // Prevent reapplying
UNCOV
156
            if (isset($properties['id'], $properties['type']) || isset($properties['data']) || isset($properties['errors'])) {
30✔
UNCOV
157
                return $schema;
22✔
158
            }
159

UNCOV
160
            $definitions[$key]['properties'] = $this->buildDefinitionPropertiesSchema($key, $className, $format, $type, $operation, $schema, []);
30✔
161

UNCOV
162
            if ($schema->getRootDefinitionKey()) {
30✔
UNCOV
163
                return $schema;
28✔
164
            }
165
        }
166

UNCOV
167
        if (($schema['type'] ?? '') === 'array') {
22✔
168
            // data
UNCOV
169
            $items = $schema['items'];
22✔
UNCOV
170
            unset($schema['items']);
22✔
171

UNCOV
172
            $schema['type'] = 'object';
22✔
UNCOV
173
            $schema['properties'] = [
22✔
UNCOV
174
                'links' => self::LINKS_PROPS,
22✔
UNCOV
175
                'meta' => self::META_PROPS,
22✔
UNCOV
176
                'data' => [
22✔
UNCOV
177
                    'type' => 'array',
22✔
UNCOV
178
                    'items' => $items,
22✔
UNCOV
179
                ],
22✔
UNCOV
180
            ];
22✔
UNCOV
181
            $schema['required'] = [
22✔
UNCOV
182
                'data',
22✔
UNCOV
183
            ];
22✔
184

UNCOV
185
            return $schema;
22✔
186
        }
187

188
        return $schema;
12✔
189
    }
190

191
    public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
192
    {
UNCOV
193
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
824✔
UNCOV
194
            $this->schemaFactory->setSchemaFactory($schemaFactory);
824✔
195
        }
196
    }
197

198
    private function buildDefinitionPropertiesSchema(string $key, string $className, string $format, string $type, ?Operation $operation, Schema $schema, ?array $serializerContext): array
199
    {
UNCOV
200
        $definitions = $schema->getDefinitions();
30✔
UNCOV
201
        $properties = $definitions[$key]['properties'] ?? [];
30✔
202

UNCOV
203
        $attributes = [];
30✔
UNCOV
204
        $relationships = [];
30✔
UNCOV
205
        $relatedDefinitions = [];
30✔
UNCOV
206
        foreach ($properties as $propertyName => $property) {
30✔
UNCOV
207
            if ($relation = $this->getRelationship($className, $propertyName, $serializerContext)) {
30✔
UNCOV
208
                [$isOne, $relatedClasses] = $relation;
17✔
UNCOV
209
                $refs = [];
17✔
UNCOV
210
                foreach ($relatedClasses as $relatedClassName => $hasOperations) {
17✔
UNCOV
211
                    if (false === $hasOperations) {
17✔
212
                        continue;
12✔
213
                    }
214

UNCOV
215
                    $operation = $this->findOperation($relatedClassName, $type, $operation, $serializerContext);
17✔
UNCOV
216
                    $inputOrOutputClass = $this->findOutputClass($relatedClassName, $type, $operation, $serializerContext);
17✔
UNCOV
217
                    $serializerContext ??= $this->getSerializerContext($operation, $type);
17✔
UNCOV
218
                    $definitionName = $this->definitionNameFactory->create($relatedClassName, $format, $inputOrOutputClass, $operation, $serializerContext);
17✔
UNCOV
219
                    $ref = Schema::VERSION_OPENAPI === $schema->getVersion() ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
17✔
UNCOV
220
                    $refs[$ref] = '$ref';
17✔
221
                }
UNCOV
222
                $relatedDefinitions[$propertyName] = array_flip($refs);
17✔
UNCOV
223
                if ($isOne) {
17✔
UNCOV
224
                    $relationships[$propertyName]['properties']['data'] = self::RELATION_PROPS;
16✔
UNCOV
225
                    continue;
16✔
226
                }
UNCOV
227
                $relationships[$propertyName]['properties']['data'] = [
17✔
UNCOV
228
                    'type' => 'array',
17✔
UNCOV
229
                    'items' => self::RELATION_PROPS,
17✔
UNCOV
230
                ];
17✔
UNCOV
231
                continue;
17✔
232
            }
UNCOV
233
            if ('id' === $propertyName) {
30✔
UNCOV
234
                $attributes['_id'] = $property;
27✔
UNCOV
235
                continue;
27✔
236
            }
UNCOV
237
            $attributes[$propertyName] = $property;
28✔
238
        }
239

UNCOV
240
        $replacement = self::PROPERTY_PROPS;
30✔
UNCOV
241
        $replacement['attributes']['properties'] = $attributes;
30✔
242

UNCOV
243
        $included = [];
30✔
UNCOV
244
        if (\count($relationships) > 0) {
30✔
UNCOV
245
            $replacement['relationships'] = [
17✔
UNCOV
246
                'type' => 'object',
17✔
UNCOV
247
                'properties' => $relationships,
17✔
UNCOV
248
            ];
17✔
UNCOV
249
            $included = [
17✔
UNCOV
250
                'included' => [
17✔
UNCOV
251
                    'description' => 'Related resources requested via the "include" query parameter.',
17✔
UNCOV
252
                    'type' => 'array',
17✔
UNCOV
253
                    'items' => [
17✔
UNCOV
254
                        'anyOf' => array_values($relatedDefinitions),
17✔
UNCOV
255
                    ],
17✔
UNCOV
256
                    'readOnly' => true,
17✔
UNCOV
257
                    'externalDocs' => [
17✔
UNCOV
258
                        'url' => 'https://jsonapi.org/format/#fetching-includes',
17✔
UNCOV
259
                    ],
17✔
UNCOV
260
                ],
17✔
UNCOV
261
            ];
17✔
262
        }
263

UNCOV
264
        if ($required = $definitions[$key]['required'] ?? null) {
30✔
UNCOV
265
            foreach ($required as $require) {
16✔
UNCOV
266
                if (isset($replacement['attributes']['properties'][$require])) {
16✔
UNCOV
267
                    $replacement['attributes']['required'][] = $require;
15✔
UNCOV
268
                    continue;
15✔
269
                }
UNCOV
270
                if (isset($relationships[$require])) {
16✔
UNCOV
271
                    $replacement['relationships']['required'][] = $require;
16✔
272
                }
273
            }
UNCOV
274
            unset($definitions[$key]['required']);
16✔
275
        }
276

UNCOV
277
        return [
30✔
UNCOV
278
            'data' => [
30✔
UNCOV
279
                'type' => 'object',
30✔
UNCOV
280
                'properties' => $replacement,
30✔
UNCOV
281
                'required' => ['type', 'id'],
30✔
UNCOV
282
            ],
30✔
UNCOV
283
        ] + $included;
30✔
284
    }
285

286
    private function getRelationship(string $resourceClass, string $property, ?array $serializerContext): ?array
287
    {
UNCOV
288
        $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $serializerContext ?? []);
30✔
UNCOV
289
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
30✔
UNCOV
290
        $isRelationship = false;
30✔
UNCOV
291
        $isOne = $isMany = false;
30✔
UNCOV
292
        $relatedClasses = [];
30✔
293

UNCOV
294
        foreach ($types as $type) {
30✔
UNCOV
295
            if ($type->isCollection()) {
30✔
UNCOV
296
                $collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
18✔
UNCOV
297
                $isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
18✔
298
            } else {
UNCOV
299
                $isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
30✔
300
            }
UNCOV
301
            if (!isset($className) || (!$isOne && !$isMany)) {
30✔
UNCOV
302
                continue;
30✔
303
            }
UNCOV
304
            $isRelationship = true;
17✔
UNCOV
305
            $resourceMetadata = $this->resourceMetadataFactory->create($className);
17✔
UNCOV
306
            $operation = $resourceMetadata->getOperation();
17✔
307
            // @see https://github.com/api-platform/core/issues/5501
308
            // @see https://github.com/api-platform/core/pull/5722
UNCOV
309
            $relatedClasses[$className] = $operation->canRead();
17✔
310
        }
311

UNCOV
312
        return $isRelationship ? [$isOne, $relatedClasses] : null;
30✔
313
    }
314
}
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