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

api-platform / core / 19337755357

13 Nov 2025 04:03PM UTC coverage: 0.0%. Remained the same
19337755357

push

github

soyuka
Merge 4.2

0 of 298 new or added lines in 18 files covered. (0.0%)

61 existing lines in 11 files now uncovered.

0 of 56962 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/src/Hydra/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\Hydra\JsonSchema;
15

16
use ApiPlatform\JsonLd\ContextBuilder;
17
use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait;
18
use ApiPlatform\JsonSchema\DefinitionNameFactory;
19
use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface;
20
use ApiPlatform\JsonSchema\ResourceMetadataTrait;
21
use ApiPlatform\JsonSchema\Schema;
22
use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface;
23
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
24
use ApiPlatform\JsonSchema\SchemaUriPrefixTrait;
25
use ApiPlatform\Metadata\Operation;
26
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
27

28
/**
29
 * Decorator factory which adds Hydra properties to the JSON Schema document.
30
 *
31
 * @author Kévin Dunglas <dunglas@gmail.com>
32
 */
33
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
34
{
35
    use HydraPrefixTrait;
36
    use ResourceMetadataTrait;
37
    use SchemaUriPrefixTrait;
38

39
    private const ITEM_BASE_SCHEMA_NAME = 'HydraItemBaseSchema';
40
    private const ITEM_WITHOUT_ID_BASE_SCHEMA_NAME = 'HydraItemBaseSchemaWithoutId';
41
    private const COLLECTION_BASE_SCHEMA_NAME_NO_PAGINATION = 'HydraCollectionBaseSchemaNoPagination';
42
    private const COLLECTION_BASE_SCHEMA_NAME = 'HydraCollectionBaseSchema';
43

44
    private const BASE_PROP = [
45
        'type' => 'string',
46
    ];
47
    private const BASE_PROPS = [
48
        '@id' => self::BASE_PROP,
49
        '@type' => self::BASE_PROP,
50
    ];
51
    private const ITEM_BASE_SCHEMA = [
52
        'type' => 'object',
53
        'properties' => [
54
            '@context' => [
55
                'oneOf' => [
56
                    ['type' => 'string'],
57
                    [
58
                        'type' => 'object',
59
                        'properties' => [
60
                            '@vocab' => [
61
                                'type' => 'string',
62
                            ],
63
                            'hydra' => [
64
                                'type' => 'string',
65
                                'enum' => [ContextBuilder::HYDRA_NS],
66
                            ],
67
                        ],
68
                        'required' => ['@vocab', 'hydra'],
69
                        'additionalProperties' => true,
70
                    ],
71
                ],
72
            ],
73
        ] + self::BASE_PROPS,
74
    ];
75

76
    private const ITEM_BASE_SCHEMA_WITH_ID = self::ITEM_BASE_SCHEMA + [
77
        'required' => ['@id', '@type'],
78
    ];
79

80
    private const ITEM_BASE_SCHEMA_WITHOUT_ID = self::ITEM_BASE_SCHEMA + [
81
        'required' => ['@type'],
82
    ];
83

84
    /**
85
     * @var array<string, true>
86
     */
87
    private array $transformed = [];
88

89
    /**
90
     * @param array<string, mixed> $defaultContext
91
     */
92
    public function __construct(
93
        private readonly SchemaFactoryInterface $schemaFactory,
94
        private readonly array $defaultContext = [],
95
        private ?DefinitionNameFactoryInterface $definitionNameFactory = null,
96
        ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null,
97
    ) {
98
        if (!$definitionNameFactory) {
×
99
            $this->definitionNameFactory = new DefinitionNameFactory();
×
100
        }
101
        $this->resourceMetadataFactory = $resourceMetadataFactory;
×
102

103
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
×
104
            $this->schemaFactory->setSchemaFactory($this);
×
105
        }
106
    }
107

108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function buildSchema(string $className, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
112
    {
113
        // The input schema must not include `@id` or `@type` as required fields, so it should be a pure JSON schema.
114
        // Strictly speaking, it is possible to include `@id` or `@context` in the input,
115
        // but the generated JSON Schema does not include `"additionalProperties": false` by default,
116
        // so it is possible to include `@id` or `@context` in the input even if the input schema is a JSON schema.
117
        if (Schema::TYPE_INPUT === $type) {
×
118
            $format = 'json';
×
119
        }
120

121
        if ('jsonld' !== $format || !$this->isResourceClass($className)) {
×
122
            return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
×
123
        }
124

125
        $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
×
126
        $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
×
127
        $serializerContext ??= $this->getSerializerContext($operation, $type);
×
128

129
        if (null === $inputOrOutputClass) {
×
130
            // input or output disabled
131
            return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
×
132
        }
133

134
        $schema = $this->schemaFactory->buildSchema($className, 'jsonld', $type, $operation, $schema, $serializerContext, $forceCollection);
×
135
        $definitions = $schema->getDefinitions();
×
136
        $prefix = $this->getSchemaUriPrefix($schema->getVersion());
×
137
        $collectionKey = $schema->getItemsDefinitionKey();
×
138

139
        if (!$collectionKey) {
×
140
            $definitionName = $schema->getRootDefinitionKey() ?? $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
×
141
            $this->decorateItemDefinition($definitionName, $definitions, $prefix, $type, $serializerContext);
×
142

143
            if (isset($definitions[$definitionName])) {
×
144
                $currentDefinitions = $schema->getDefinitions();
×
145
                $schema->exchangeArray([]); // Clear the schema
×
146
                $schema['$ref'] = $prefix.$definitionName;
×
147
                $schema->setDefinitions($currentDefinitions);
×
148
            }
149

150
            return $schema;
×
151
        }
152

153
        if (($schema['type'] ?? '') !== 'array') {
×
154
            return $schema;
×
155
        }
156

157
        $hydraPrefix = $this->getHydraPrefix($serializerContext + $this->defaultContext);
×
158

NEW
159
        if (!isset($definitions[self::COLLECTION_BASE_SCHEMA_NAME_NO_PAGINATION])) {
×
160
            switch ($schema->getVersion()) {
×
161
                // JSON Schema + OpenAPI 3.1
162
                case Schema::VERSION_OPENAPI:
×
163
                case Schema::VERSION_JSON_SCHEMA:
×
164
                    $nullableStringDefinition = ['type' => ['string', 'null']];
×
165
                    break;
×
166
                    // Swagger
167
                default:
168
                    $nullableStringDefinition = ['type' => 'string'];
×
169
                    break;
×
170
            }
171

NEW
172
            $definitions[self::COLLECTION_BASE_SCHEMA_NAME_NO_PAGINATION] = [
×
173
                'type' => 'object',
×
174
                'properties' => [
×
175
                    $hydraPrefix.'totalItems' => [
×
176
                        'type' => 'integer',
×
177
                        'minimum' => 0,
×
178
                    ],
×
179
                    $hydraPrefix.'search' => [
×
180
                        'type' => 'object',
×
181
                        'properties' => [
×
182
                            '@type' => ['type' => 'string'],
×
183
                            $hydraPrefix.'template' => ['type' => 'string'],
×
184
                            $hydraPrefix.'variableRepresentation' => ['type' => 'string'],
×
185
                            $hydraPrefix.'mapping' => [
×
186
                                'type' => 'array',
×
187
                                'items' => [
×
188
                                    'type' => 'object',
×
189
                                    'properties' => [
×
190
                                        '@type' => ['type' => 'string'],
×
191
                                        'variable' => ['type' => 'string'],
×
192
                                        'property' => $nullableStringDefinition,
×
193
                                        'required' => ['type' => 'boolean'],
×
194
                                    ],
×
195
                                ],
×
196
                            ],
×
197
                        ],
×
198
                    ],
×
199
                ],
×
200
            ];
×
201

NEW
202
            $definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [
×
NEW
203
                'allOf' => [
×
NEW
204
                    ['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME_NO_PAGINATION],
×
NEW
205
                    [
×
NEW
206
                        'type' => 'object',
×
NEW
207
                        'properties' => [
×
NEW
208
                            $hydraPrefix.'view' => [
×
NEW
209
                                'type' => 'object',
×
NEW
210
                                'properties' => [
×
NEW
211
                                    '@id' => [
×
NEW
212
                                        'type' => 'string',
×
NEW
213
                                        'format' => 'iri-reference',
×
NEW
214
                                    ],
×
NEW
215
                                    '@type' => [
×
NEW
216
                                        'type' => 'string',
×
NEW
217
                                    ],
×
NEW
218
                                    $hydraPrefix.'first' => [
×
NEW
219
                                        'type' => 'string',
×
NEW
220
                                        'format' => 'iri-reference',
×
NEW
221
                                    ],
×
NEW
222
                                    $hydraPrefix.'last' => [
×
NEW
223
                                        'type' => 'string',
×
NEW
224
                                        'format' => 'iri-reference',
×
NEW
225
                                    ],
×
NEW
226
                                    $hydraPrefix.'previous' => [
×
NEW
227
                                        'type' => 'string',
×
NEW
228
                                        'format' => 'iri-reference',
×
NEW
229
                                    ],
×
NEW
230
                                    $hydraPrefix.'next' => [
×
NEW
231
                                        'type' => 'string',
×
NEW
232
                                        'format' => 'iri-reference',
×
NEW
233
                                    ],
×
NEW
234
                                ],
×
NEW
235
                                'example' => [
×
NEW
236
                                    '@id' => 'string',
×
NEW
237
                                    'type' => 'string',
×
NEW
238
                                    $hydraPrefix.'first' => 'string',
×
NEW
239
                                    $hydraPrefix.'last' => 'string',
×
NEW
240
                                    $hydraPrefix.'previous' => 'string',
×
NEW
241
                                    $hydraPrefix.'next' => 'string',
×
NEW
242
                                ],
×
NEW
243
                            ],
×
NEW
244
                        ],
×
NEW
245
                    ],
×
NEW
246
                ],
×
NEW
247
            ];
×
248
        }
249

250
        $definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
×
251
        $schema['type'] = 'object';
×
252
        $schema['description'] = "$definitionName collection.";
×
253
        $schema['allOf'] = [
×
NEW
254
            ['$ref' => $prefix.(false === $operation->getPaginationEnabled() ? self::COLLECTION_BASE_SCHEMA_NAME_NO_PAGINATION : self::COLLECTION_BASE_SCHEMA_NAME)],
×
255
            [
×
256
                'type' => 'object',
×
257
                'required' => [
×
258
                    $hydraPrefix.'member',
×
259
                ],
×
260
                'properties' => [
×
261
                    $hydraPrefix.'member' => [
×
262
                        'type' => 'array',
×
263
                        'items' => $schema['items'],
×
264
                    ],
×
265
                ],
×
266
            ],
×
267
        ];
×
268

269
        unset($schema['items']);
×
270

271
        if (isset($definitions[$collectionKey])) {
×
272
            $this->decorateItemDefinition($collectionKey, $definitions, $prefix, $type, $serializerContext);
×
273
        }
274

275
        return $schema;
×
276
    }
277

278
    public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
279
    {
280
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
×
281
            $this->schemaFactory->setSchemaFactory($schemaFactory);
×
282
        }
283
    }
284

285
    private function decorateItemDefinition(string $definitionName, \ArrayObject $definitions, string $prefix, string $type, ?array $serializerContext): void
286
    {
287
        if (!isset($definitions[$definitionName]) || ($this->transformed[$definitionName] ?? false)) {
×
288
            return;
×
289
        }
290

291
        $hasNoId = Schema::TYPE_OUTPUT === $type && false === ($serializerContext['gen_id'] ?? true);
×
292
        $baseName = self::ITEM_BASE_SCHEMA_NAME;
×
293
        if ($hasNoId) {
×
294
            $baseName = self::ITEM_WITHOUT_ID_BASE_SCHEMA_NAME;
×
295
        }
296

297
        if (!isset($definitions[$baseName])) {
×
298
            $definitions[$baseName] = $hasNoId ? self::ITEM_BASE_SCHEMA_WITHOUT_ID : self::ITEM_BASE_SCHEMA_WITH_ID;
×
299
        }
300

301
        $allOf = new \ArrayObject(['allOf' => [
×
302
            ['$ref' => $prefix.$baseName],
×
303
            $definitions[$definitionName],
×
304
        ]]);
×
305

306
        if (isset($definitions[$definitionName]['description'])) {
×
307
            $allOf['description'] = $definitions[$definitionName]['description'];
×
308
        }
309

310
        $definitions[$definitionName] = $allOf;
×
311
        unset($definitions[$definitionName]['allOf'][1]['description']);
×
312

313
        $this->transformed[$definitionName] = true;
×
314
    }
315
}
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