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

api-platform / core / 18343625207

08 Oct 2025 11:48AM UTC coverage: 24.542% (-0.02%) from 24.561%
18343625207

push

github

web-flow
fix(state): object mapper on delete operation (#7447)

fixes #7434

1 of 49 new or added lines in 2 files covered. (2.04%)

7084 existing lines in 207 files now uncovered.

14004 of 57061 relevant lines covered (24.54%)

26.01 hits per line

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

96.62
/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 = 'HydraCollectionBaseSchema';
42

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

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

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

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

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

UNCOV
102
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
743✔
UNCOV
103
            $this->schemaFactory->setSchemaFactory($this);
743✔
104
        }
105
    }
106

107
    /**
108
     * {@inheritdoc}
109
     */
110
    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
111
    {
112
        // The input schema must not include `@id` or `@type` as required fields, so it should be a pure JSON schema.
113
        // Strictly speaking, it is possible to include `@id` or `@context` in the input,
114
        // but the generated JSON Schema does not include `"additionalProperties": false` by default,
115
        // so it is possible to include `@id` or `@context` in the input even if the input schema is a JSON schema.
UNCOV
116
        if (Schema::TYPE_INPUT === $type) {
154✔
UNCOV
117
            $format = 'json';
54✔
118
        }
119

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

UNCOV
124
        $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
102✔
UNCOV
125
        $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
102✔
UNCOV
126
        $serializerContext ??= $this->getSerializerContext($operation, $type);
102✔
127

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

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

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

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

UNCOV
149
            return $schema;
96✔
150
        }
151

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

UNCOV
156
        $hydraPrefix = $this->getHydraPrefix($serializerContext + $this->defaultContext);
66✔
157

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

UNCOV
171
            $definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [
66✔
UNCOV
172
                'type' => 'object',
66✔
UNCOV
173
                'required' => [
66✔
UNCOV
174
                    $hydraPrefix.'member',
66✔
UNCOV
175
                ],
66✔
UNCOV
176
                'properties' => [
66✔
UNCOV
177
                    $hydraPrefix.'member' => [
66✔
UNCOV
178
                        'type' => 'array',
66✔
UNCOV
179
                        'items' => ['type' => 'object'],
66✔
UNCOV
180
                    ],
66✔
UNCOV
181
                    $hydraPrefix.'totalItems' => [
66✔
UNCOV
182
                        'type' => 'integer',
66✔
UNCOV
183
                        'minimum' => 0,
66✔
UNCOV
184
                    ],
66✔
UNCOV
185
                    $hydraPrefix.'view' => [
66✔
UNCOV
186
                        'type' => 'object',
66✔
UNCOV
187
                        'properties' => [
66✔
UNCOV
188
                            '@id' => [
66✔
UNCOV
189
                                'type' => 'string',
66✔
UNCOV
190
                                'format' => 'iri-reference',
66✔
UNCOV
191
                            ],
66✔
UNCOV
192
                            '@type' => [
66✔
UNCOV
193
                                'type' => 'string',
66✔
UNCOV
194
                            ],
66✔
UNCOV
195
                            $hydraPrefix.'first' => [
66✔
UNCOV
196
                                'type' => 'string',
66✔
UNCOV
197
                                'format' => 'iri-reference',
66✔
UNCOV
198
                            ],
66✔
UNCOV
199
                            $hydraPrefix.'last' => [
66✔
UNCOV
200
                                'type' => 'string',
66✔
UNCOV
201
                                'format' => 'iri-reference',
66✔
UNCOV
202
                            ],
66✔
UNCOV
203
                            $hydraPrefix.'previous' => [
66✔
UNCOV
204
                                'type' => 'string',
66✔
UNCOV
205
                                'format' => 'iri-reference',
66✔
UNCOV
206
                            ],
66✔
UNCOV
207
                            $hydraPrefix.'next' => [
66✔
UNCOV
208
                                'type' => 'string',
66✔
UNCOV
209
                                'format' => 'iri-reference',
66✔
UNCOV
210
                            ],
66✔
UNCOV
211
                        ],
66✔
UNCOV
212
                        'example' => [
66✔
UNCOV
213
                            '@id' => 'string',
66✔
UNCOV
214
                            'type' => 'string',
66✔
UNCOV
215
                            $hydraPrefix.'first' => 'string',
66✔
UNCOV
216
                            $hydraPrefix.'last' => 'string',
66✔
UNCOV
217
                            $hydraPrefix.'previous' => 'string',
66✔
UNCOV
218
                            $hydraPrefix.'next' => 'string',
66✔
UNCOV
219
                        ],
66✔
UNCOV
220
                    ],
66✔
UNCOV
221
                    $hydraPrefix.'search' => [
66✔
UNCOV
222
                        'type' => 'object',
66✔
UNCOV
223
                        'properties' => [
66✔
UNCOV
224
                            '@type' => ['type' => 'string'],
66✔
UNCOV
225
                            $hydraPrefix.'template' => ['type' => 'string'],
66✔
UNCOV
226
                            $hydraPrefix.'variableRepresentation' => ['type' => 'string'],
66✔
UNCOV
227
                            $hydraPrefix.'mapping' => [
66✔
UNCOV
228
                                'type' => 'array',
66✔
UNCOV
229
                                'items' => [
66✔
UNCOV
230
                                    'type' => 'object',
66✔
UNCOV
231
                                    'properties' => [
66✔
UNCOV
232
                                        '@type' => ['type' => 'string'],
66✔
UNCOV
233
                                        'variable' => ['type' => 'string'],
66✔
UNCOV
234
                                        'property' => $nullableStringDefinition,
66✔
UNCOV
235
                                        'required' => ['type' => 'boolean'],
66✔
UNCOV
236
                                    ],
66✔
UNCOV
237
                                ],
66✔
UNCOV
238
                            ],
66✔
UNCOV
239
                        ],
66✔
UNCOV
240
                    ],
66✔
UNCOV
241
                ],
66✔
UNCOV
242
            ];
66✔
243
        }
244

UNCOV
245
        $definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
66✔
UNCOV
246
        $schema['type'] = 'object';
66✔
UNCOV
247
        $schema['description'] = "$definitionName collection.";
66✔
UNCOV
248
        $schema['allOf'] = [
66✔
UNCOV
249
            ['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME],
66✔
UNCOV
250
            [
66✔
UNCOV
251
                'type' => 'object',
66✔
UNCOV
252
                'properties' => [
66✔
UNCOV
253
                    $hydraPrefix.'member' => [
66✔
UNCOV
254
                        'type' => 'array',
66✔
UNCOV
255
                        'items' => $schema['items'],
66✔
UNCOV
256
                    ],
66✔
UNCOV
257
                ],
66✔
UNCOV
258
            ],
66✔
UNCOV
259
        ];
66✔
260

UNCOV
261
        unset($schema['items']);
66✔
262

UNCOV
263
        if (isset($definitions[$collectionKey])) {
66✔
UNCOV
264
            $this->decorateItemDefinition($collectionKey, $definitions, $prefix, $type, $serializerContext);
66✔
265
        }
266

UNCOV
267
        return $schema;
66✔
268
    }
269

270
    public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
271
    {
UNCOV
272
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
743✔
UNCOV
273
            $this->schemaFactory->setSchemaFactory($schemaFactory);
743✔
274
        }
275
    }
276

277
    private function decorateItemDefinition(string $definitionName, \ArrayObject $definitions, string $prefix, string $type, ?array $serializerContext): void
278
    {
UNCOV
279
        if (!isset($definitions[$definitionName]) || ($this->transformed[$definitionName] ?? false)) {
102✔
UNCOV
280
            return;
52✔
281
        }
282

UNCOV
283
        $hasNoId = Schema::TYPE_OUTPUT === $type && false === ($serializerContext['gen_id'] ?? true);
102✔
UNCOV
284
        $baseName = self::ITEM_BASE_SCHEMA_NAME;
102✔
UNCOV
285
        if ($hasNoId) {
102✔
UNCOV
286
            $baseName = self::ITEM_WITHOUT_ID_BASE_SCHEMA_NAME;
2✔
287
        }
288

UNCOV
289
        if (!isset($definitions[$baseName])) {
102✔
UNCOV
290
            $definitions[$baseName] = $hasNoId ? self::ITEM_BASE_SCHEMA_WITHOUT_ID : self::ITEM_BASE_SCHEMA_WITH_ID;
102✔
291
        }
292

UNCOV
293
        $allOf = new \ArrayObject(['allOf' => [
102✔
UNCOV
294
            ['$ref' => $prefix.$baseName],
102✔
UNCOV
295
            $definitions[$definitionName],
102✔
UNCOV
296
        ]]);
102✔
297

UNCOV
298
        if (isset($definitions[$definitionName]['description'])) {
102✔
UNCOV
299
            $allOf['description'] = $definitions[$definitionName]['description'];
60✔
300
        }
301

UNCOV
302
        $definitions[$definitionName] = $allOf;
102✔
UNCOV
303
        unset($definitions[$definitionName]['allOf'][1]['description']);
102✔
304

UNCOV
305
        $this->transformed[$definitionName] = true;
102✔
306
    }
307
}
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