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

api-platform / core / 14246153067

03 Apr 2025 02:56PM UTC coverage: 7.286% (-0.002%) from 7.288%
14246153067

push

github

web-flow
Merge commit from fork

12 of 160 new or added lines in 7 files covered. (7.5%)

2343 existing lines in 152 files now uncovered.

12450 of 170870 relevant lines covered (7.29%)

12.06 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

26
/**
27
 * Decorator factory which adds JSON:API properties to the JSON Schema document.
28
 *
29
 * @author Gwendolen Lynch <gwendolen.lynch@gmail.com>
30
 */
31
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
32
{
33
    use ResourceMetadataTrait;
34
    private const LINKS_PROPS = [
35
        'type' => 'object',
36
        'properties' => [
37
            'self' => [
38
                'type' => 'string',
39
                'format' => 'iri-reference',
40
            ],
41
            'first' => [
42
                'type' => 'string',
43
                'format' => 'iri-reference',
44
            ],
45
            'prev' => [
46
                'type' => 'string',
47
                'format' => 'iri-reference',
48
            ],
49
            'next' => [
50
                'type' => 'string',
51
                'format' => 'iri-reference',
52
            ],
53
            'last' => [
54
                'type' => 'string',
55
                'format' => 'iri-reference',
56
            ],
57
        ],
58
        'example' => [
59
            'self' => 'string',
60
            'first' => 'string',
61
            'prev' => 'string',
62
            'next' => 'string',
63
            'last' => 'string',
64
        ],
65
    ];
66
    private const META_PROPS = [
67
        'type' => 'object',
68
        'properties' => [
69
            'totalItems' => [
70
                'type' => 'integer',
71
                'minimum' => 0,
72
            ],
73
            'itemsPerPage' => [
74
                'type' => 'integer',
75
                'minimum' => 0,
76
            ],
77
            'currentPage' => [
78
                'type' => 'integer',
79
                'minimum' => 0,
80
            ],
81
        ],
82
    ];
83
    private const RELATION_PROPS = [
84
        'type' => 'object',
85
        'properties' => [
86
            'type' => [
87
                'type' => 'string',
88
            ],
89
            'id' => [
90
                'type' => 'string',
91
                'format' => 'iri-reference',
92
            ],
93
        ],
94
    ];
95
    private const PROPERTY_PROPS = [
96
        'id' => [
97
            'type' => 'string',
98
        ],
99
        'type' => [
100
            'type' => 'string',
101
        ],
102
        'attributes' => [
103
            'type' => 'object',
104
            'properties' => [],
105
        ],
106
    ];
107

108
    public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly ?DefinitionNameFactoryInterface $definitionNameFactory = null)
109
    {
110
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
1,011✔
111
            $this->schemaFactory->setSchemaFactory($this);
1,011✔
112
        }
113
        $this->resourceClassResolver = $resourceClassResolver;
1,011✔
114
        $this->resourceMetadataFactory = $resourceMetadataFactory;
1,011✔
115
    }
116

117
    /**
118
     * {@inheritdoc}
119
     */
120
    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
121
    {
122
        if ('jsonapi' !== $format) {
150✔
123
            return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
126✔
124
        }
125
        // We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources.
126
        // That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes
127
        $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, [], $forceCollection);
57✔
128

129
        if (($key = $schema->getRootDefinitionKey()) || ($key = $schema->getItemsDefinitionKey())) {
57✔
130
            $definitions = $schema->getDefinitions();
57✔
131
            $properties = $definitions[$key]['properties'] ?? [];
57✔
132

133
            // Prevent reapplying
134
            if (isset($properties['id'], $properties['type']) || isset($properties['data'])) {
57✔
135
                return $schema;
36✔
136
            }
137

138
            $definitions[$key]['properties'] = $this->buildDefinitionPropertiesSchema($key, $className, $format, $type, $operation, $schema, []);
57✔
139

140
            if ($schema->getRootDefinitionKey()) {
57✔
141
                return $schema;
54✔
142
            }
143
        }
144

145
        if (($schema['type'] ?? '') === 'array') {
33✔
146
            // data
147
            $items = $schema['items'];
33✔
148
            unset($schema['items']);
33✔
149

150
            $schema['type'] = 'object';
33✔
151
            $schema['properties'] = [
33✔
152
                'links' => self::LINKS_PROPS,
33✔
153
                'meta' => self::META_PROPS,
33✔
154
                'data' => [
33✔
155
                    'type' => 'array',
33✔
156
                    'items' => $items,
33✔
157
                ],
33✔
158
            ];
33✔
159
            $schema['required'] = [
33✔
160
                'data',
33✔
161
            ];
33✔
162

163
            return $schema;
33✔
164
        }
165

UNCOV
166
        return $schema;
12✔
167
    }
168

169
    public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
170
    {
171
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
1,011✔
172
            $this->schemaFactory->setSchemaFactory($schemaFactory);
1,011✔
173
        }
174
    }
175

176
    private function buildDefinitionPropertiesSchema(string $key, string $className, string $format, string $type, ?Operation $operation, Schema $schema, ?array $serializerContext): array
177
    {
178
        $definitions = $schema->getDefinitions();
57✔
179
        $properties = $definitions[$key]['properties'] ?? [];
57✔
180

181
        $attributes = [];
57✔
182
        $relationships = [];
57✔
183
        $relatedDefinitions = [];
57✔
184
        foreach ($properties as $propertyName => $property) {
57✔
185
            if ($relation = $this->getRelationship($className, $propertyName, $serializerContext)) {
57✔
186
                [$isOne, $relatedClasses] = $relation;
27✔
187
                $refs = [];
27✔
188
                foreach ($relatedClasses as $relatedClassName => $hasOperations) {
27✔
189
                    if (false === $hasOperations) {
27✔
UNCOV
190
                        continue;
12✔
191
                    }
192

193
                    $operation = $this->findOperation($relatedClassName, $type, $operation, $serializerContext);
27✔
194
                    $inputOrOutputClass = $this->findOutputClass($relatedClassName, $type, $operation, $serializerContext);
27✔
195
                    $serializerContext ??= $this->getSerializerContext($operation, $type);
27✔
196
                    $definitionName = $this->definitionNameFactory->create($relatedClassName, $format, $inputOrOutputClass, $operation, $serializerContext);
27✔
197
                    $ref = Schema::VERSION_OPENAPI === $schema->getVersion() ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
27✔
198
                    $refs[$ref] = '$ref';
27✔
199
                }
200
                $relatedDefinitions[$propertyName] = array_flip($refs);
27✔
201
                if ($isOne) {
27✔
202
                    $relationships[$propertyName]['properties']['data'] = self::RELATION_PROPS;
24✔
203
                    continue;
24✔
204
                }
205
                $relationships[$propertyName]['properties']['data'] = [
27✔
206
                    'type' => 'array',
27✔
207
                    'items' => self::RELATION_PROPS,
27✔
208
                ];
27✔
209
                continue;
27✔
210
            }
211
            if ('id' === $propertyName) {
57✔
212
                $attributes['_id'] = $property;
48✔
213
                continue;
48✔
214
            }
215
            $attributes[$propertyName] = $property;
54✔
216
        }
217

218
        $replacement = self::PROPERTY_PROPS;
57✔
219
        $replacement['attributes']['properties'] = $attributes;
57✔
220

221
        $included = [];
57✔
222
        if (\count($relationships) > 0) {
57✔
223
            $replacement['relationships'] = [
27✔
224
                'type' => 'object',
27✔
225
                'properties' => $relationships,
27✔
226
            ];
27✔
227
            $included = [
27✔
228
                'included' => [
27✔
229
                    'description' => 'Related resources requested via the "include" query parameter.',
27✔
230
                    'type' => 'array',
27✔
231
                    'items' => [
27✔
232
                        'anyOf' => array_values($relatedDefinitions),
27✔
233
                    ],
27✔
234
                    'readOnly' => true,
27✔
235
                    'externalDocs' => [
27✔
236
                        'url' => 'https://jsonapi.org/format/#fetching-includes',
27✔
237
                    ],
27✔
238
                ],
27✔
239
            ];
27✔
240
        }
241

242
        if ($required = $definitions[$key]['required'] ?? null) {
57✔
243
            foreach ($required as $require) {
24✔
244
                if (isset($replacement['attributes']['properties'][$require])) {
24✔
245
                    $replacement['attributes']['required'][] = $require;
21✔
246
                    continue;
21✔
247
                }
248
                if (isset($relationships[$require])) {
24✔
249
                    $replacement['relationships']['required'][] = $require;
24✔
250
                }
251
            }
252
            unset($definitions[$key]['required']);
24✔
253
        }
254

255
        return [
57✔
256
            'data' => [
57✔
257
                'type' => 'object',
57✔
258
                'properties' => $replacement,
57✔
259
                'required' => ['type', 'id'],
57✔
260
            ],
57✔
261
        ] + $included;
57✔
262
    }
263

264
    private function getRelationship(string $resourceClass, string $property, ?array $serializerContext): ?array
265
    {
266
        $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $serializerContext ?? []);
57✔
267
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
57✔
268
        $isRelationship = false;
57✔
269
        $isOne = $isMany = false;
57✔
270
        $relatedClasses = [];
57✔
271

272
        foreach ($types as $type) {
57✔
273
            if ($type->isCollection()) {
57✔
274
                $collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
30✔
275
                $isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
30✔
276
            } else {
277
                $isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
57✔
278
            }
279
            if (!isset($className) || (!$isOne && !$isMany)) {
57✔
280
                continue;
57✔
281
            }
282
            $isRelationship = true;
27✔
283
            $resourceMetadata = $this->resourceMetadataFactory->create($className);
27✔
284
            $operation = $resourceMetadata->getOperation();
27✔
285
            // @see https://github.com/api-platform/core/issues/5501
286
            // @see https://github.com/api-platform/core/pull/5722
287
            $relatedClasses[$className] = $operation->canRead();
27✔
288
        }
289

290
        return $isRelationship ? [$isOne, $relatedClasses] : null;
57✔
291
    }
292
}
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