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

api-platform / core / 21006071786

14 Jan 2026 06:53PM UTC coverage: 21.366% (-7.7%) from 29.097%
21006071786

Pull #7675

github

web-flow
Merge 5c98a7330 into 73402fc61
Pull Request #7675: feat(doctrine): Add caseInsensitive option to PartialSearchFilter

0 of 11 new or added lines in 2 files covered. (0.0%)

15054 existing lines in 491 files now uncovered.

12306 of 57596 relevant lines covered (21.37%)

49.15 hits per line

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

49.46
/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.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\JsonSchema\Metadata\Property\Factory;
15

16
use ApiPlatform\JsonSchema\Schema;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\Exception\PropertyNotFoundException;
19
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
20
use ApiPlatform\Metadata\ResourceClassResolverInterface;
21
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Ramsey\Uuid\UuidInterface;
24
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
25
use Symfony\Component\PropertyInfo\Type as LegacyType;
26
use Symfony\Component\TypeInfo\Type;
27
use Symfony\Component\TypeInfo\Type\BuiltinType;
28
use Symfony\Component\TypeInfo\Type\CollectionType;
29
use Symfony\Component\TypeInfo\Type\IntersectionType;
30
use Symfony\Component\TypeInfo\Type\ObjectType;
31
use Symfony\Component\TypeInfo\Type\UnionType;
32
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
33
use Symfony\Component\TypeInfo\TypeIdentifier;
34
use Symfony\Component\Uid\Ulid;
35
use Symfony\Component\Uid\Uuid;
36

37
/**
38
 * Build ApiProperty::schema.
39
 */
40
final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInterface
41
{
42
    use ResourceClassInfoTrait;
43

44
    public const JSON_SCHEMA_USER_DEFINED = 'user_defined_schema';
45

46
    public function __construct(
47
        ResourceClassResolverInterface $resourceClassResolver,
48
        private readonly ?PropertyMetadataFactoryInterface $decorated = null,
49
    ) {
UNCOV
50
        $this->resourceClassResolver = $resourceClassResolver;
1,520✔
51
    }
52

53
    public function create(string $resourceClass, string $property, array $options = []): ApiProperty
54
    {
UNCOV
55
        if (null === $this->decorated) {
573✔
56
            $propertyMetadata = new ApiProperty();
×
57
        } else {
58
            try {
UNCOV
59
                $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
573✔
60
            } catch (PropertyNotFoundException) {
×
61
                $propertyMetadata = new ApiProperty();
×
62
            }
63
        }
64

UNCOV
65
        $extraProperties = $propertyMetadata->getExtraProperties();
573✔
66
        // see AttributePropertyMetadataFactory
UNCOV
67
        if (true === ($extraProperties[self::JSON_SCHEMA_USER_DEFINED] ?? false)) {
573✔
68
            // schema seems to have been declared by the user: do not override nor complete user value
UNCOV
69
            return $propertyMetadata;
16✔
70
        }
71

UNCOV
72
        $link = (($options['schema_type'] ?? null) === Schema::TYPE_INPUT) ? $propertyMetadata->isWritableLink() : $propertyMetadata->isReadableLink();
573✔
UNCOV
73
        $propertySchema = $propertyMetadata->getSchema() ?? [];
573✔
74

UNCOV
75
        if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable())) {
573✔
UNCOV
76
            $propertySchema['readOnly'] = true;
401✔
77
        }
78

UNCOV
79
        if (!\array_key_exists('writeOnly', $propertySchema) && false === $propertyMetadata->isReadable()) {
573✔
UNCOV
80
            $propertySchema['writeOnly'] = true;
136✔
81
        }
82

UNCOV
83
        if (!\array_key_exists('description', $propertySchema) && null !== ($description = $propertyMetadata->getDescription())) {
573✔
UNCOV
84
            $propertySchema['description'] = $description;
220✔
85
        }
86

87
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
UNCOV
88
        if (!\array_key_exists('deprecated', $propertySchema) && null !== $propertyMetadata->getDeprecationReason()) {
573✔
UNCOV
89
            $propertySchema['deprecated'] = true;
25✔
90
        }
91

92
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
93
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
UNCOV
94
        if (!\array_key_exists('externalDocs', $propertySchema) && null !== ($iri = $propertyMetadata->getTypes()[0] ?? null)) {
573✔
UNCOV
95
            $propertySchema['externalDocs'] = ['url' => $iri];
60✔
96
        }
97

UNCOV
98
        if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
573✔
99
            return $propertyMetadata->withSchema($this->getLegacyTypeSchema($propertyMetadata, $propertySchema, $resourceClass, $property, $link));
×
100
        }
101

UNCOV
102
        return $propertyMetadata->withSchema($this->getTypeSchema($propertyMetadata, $propertySchema, $link));
573✔
103
    }
104

105
    private function getTypeSchema(ApiProperty $propertyMetadata, array $propertySchema, ?bool $link): array
106
    {
UNCOV
107
        $type = $propertyMetadata->getNativeType();
573✔
108

UNCOV
109
        $className = null;
573✔
UNCOV
110
        $typeIsResourceClass = function (Type $type) use (&$className): bool {
573✔
UNCOV
111
            return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
529✔
UNCOV
112
        };
573✔
UNCOV
113
        $isResourceClass = $type?->isSatisfiedBy($typeIsResourceClass);
573✔
114

UNCOV
115
        if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) && !$className) {
573✔
116
            $propertySchema['readOnly'] = true;
10✔
117
        }
118

UNCOV
119
        if (!\array_key_exists('default', $propertySchema) && null !== ($default = $propertyMetadata->getDefault()) && false === (\is_array($default) && empty($default)) && !$isResourceClass) {
573✔
UNCOV
120
            if ($default instanceof \BackedEnum) {
111✔
UNCOV
121
                $default = $default->value;
10✔
122
            }
UNCOV
123
            $propertySchema['default'] = $default;
111✔
124
        }
125

UNCOV
126
        if (!\array_key_exists('example', $propertySchema) && null !== ($example = $propertyMetadata->getExample()) && false === (\is_array($example) && empty($example))) {
573✔
UNCOV
127
            $propertySchema['example'] = $example;
6✔
128
        }
129

UNCOV
130
        $hasType = $this->getSchemaValue($propertySchema, 'type') || $this->getSchemaValue($propertyMetadata->getJsonSchemaContext() ?? [], 'type') || $this->getSchemaValue($propertyMetadata->getOpenapiContext() ?? [], 'type');
573✔
UNCOV
131
        $hasRef = $this->getSchemaValue($propertySchema, '$ref') || $this->getSchemaValue($propertyMetadata->getJsonSchemaContext() ?? [], '$ref') || $this->getSchemaValue($propertyMetadata->getOpenapiContext() ?? [], '$ref');
573✔
132

133
        // never override the following keys if at least one is already set or if there's a custom openapi context
UNCOV
134
        if ($hasType || $hasRef || !$type) {
573✔
UNCOV
135
            return $propertySchema;
90✔
136
        }
137

UNCOV
138
        if ($type instanceof CollectionType && null !== $propertyMetadata->getUriTemplate()) {
529✔
139
            $type = $type->getCollectionValueType();
10✔
140
        }
141

UNCOV
142
        return $propertySchema + $this->getJsonSchemaFromType($type, $link);
529✔
143
    }
144

145
    /**
146
     * Applies nullability rules to a generated JSON schema based on the original type's nullability.
147
     *
148
     * @param array<string, mixed> $schema     the base JSON schema generated for the non-null type
149
     * @param bool                 $isNullable whether the original type allows null
150
     *
151
     * @return array<string, mixed> the JSON schema with nullability applied
152
     */
153
    private function applyNullability(array $schema, bool $isNullable): array
154
    {
UNCOV
155
        if (!$isNullable) {
529✔
UNCOV
156
            return $schema;
529✔
157
        }
158

UNCOV
159
        if (isset($schema['type']) && 'null' === $schema['type'] && 1 === \count($schema)) {
278✔
160
            return $schema;
×
161
        }
162

UNCOV
163
        if (isset($schema['anyOf']) && \is_array($schema['anyOf'])) {
278✔
UNCOV
164
            $hasNull = false;
10✔
UNCOV
165
            foreach ($schema['anyOf'] as $anyOfSchema) {
10✔
UNCOV
166
                if (isset($anyOfSchema['type']) && 'null' === $anyOfSchema['type']) {
10✔
167
                    $hasNull = true;
×
168
                    break;
×
169
                }
170
            }
UNCOV
171
            if (!$hasNull) {
10✔
UNCOV
172
                $schema['anyOf'][] = ['type' => 'null'];
10✔
173
            }
174

UNCOV
175
            return $schema;
10✔
176
        }
177

UNCOV
178
        if (isset($schema['type'])) {
278✔
UNCOV
179
            $currentType = $schema['type'];
278✔
UNCOV
180
            $schema['type'] = \is_array($currentType) ? array_merge($currentType, ['null']) : [$currentType, 'null'];
278✔
181

UNCOV
182
            if (isset($schema['enum'])) {
278✔
UNCOV
183
                $schema['enum'][] = null;
11✔
184

UNCOV
185
                return $schema;
11✔
186
            }
187

UNCOV
188
            return $schema;
276✔
189
        }
190

191
        return ['anyOf' => [$schema, ['type' => 'null']]];
×
192
    }
193

194
    /**
195
     * Converts a TypeInfo Type into a JSON Schema definition array.
196
     *
197
     * @return array<string, mixed>
198
     */
199
    private function getJsonSchemaFromType(Type $type, ?bool $readableLink = null): array
200
    {
UNCOV
201
        $isNullable = $type->isNullable();
529✔
202

UNCOV
203
        if ($type instanceof UnionType) {
529✔
UNCOV
204
            $subTypes = array_filter($type->getTypes(), fn ($t) => !($t instanceof BuiltinType && $t->isIdentifiedBy(TypeIdentifier::NULL)));
292✔
205

UNCOV
206
            foreach ($subTypes as $t) {
292✔
UNCOV
207
                $s = $this->getJsonSchemaFromType($t, $readableLink);
292✔
208
                // We can not find what type this is, let it be computed at runtime by the SchemaFactory
UNCOV
209
                if (($s['type'] ?? null) === Schema::UNKNOWN_TYPE) {
292✔
UNCOV
210
                    return $s;
57✔
211
                }
212
            }
213

UNCOV
214
            $schemas = array_map(fn ($t) => $this->getJsonSchemaFromType($t, $readableLink), $subTypes);
271✔
215

UNCOV
216
            if (0 === \count($schemas)) {
271✔
217
                $schema = [];
×
UNCOV
218
            } elseif (1 === \count($schemas)) {
271✔
UNCOV
219
                $schema = current($schemas);
270✔
220
            } else {
UNCOV
221
                $schema = ['anyOf' => array_values($schemas)];
13✔
222
            }
223

UNCOV
224
            return $this->applyNullability($schema, $isNullable);
271✔
225
        }
226

UNCOV
227
        if ($type instanceof IntersectionType) {
529✔
UNCOV
228
            $schemas = [];
8✔
UNCOV
229
            foreach ($type->getTypes() as $t) {
8✔
UNCOV
230
                while ($t instanceof WrappingTypeInterface) {
8✔
231
                    $t = $t->getWrappedType();
×
232
                }
233

UNCOV
234
                $subSchema = $this->getJsonSchemaFromType($t, $readableLink);
8✔
UNCOV
235
                if (!empty($subSchema)) {
8✔
UNCOV
236
                    $schemas[] = $subSchema;
8✔
237
                }
238
            }
239

UNCOV
240
            return $this->applyNullability(['allOf' => $schemas], $isNullable);
8✔
241
        }
242

UNCOV
243
        if ($type instanceof CollectionType) {
529✔
UNCOV
244
            $valueType = $type->getCollectionValueType();
147✔
UNCOV
245
            $valueSchema = $this->getJsonSchemaFromType($valueType, $readableLink);
147✔
UNCOV
246
            $keyType = $type->getCollectionKeyType();
147✔
247

248
            // Associative array (string keys)
UNCOV
249
            if ($keyType->isSatisfiedBy(fn (Type $t) => $t instanceof BuiltinType && $t->isIdentifiedBy(TypeIdentifier::INT))) {
147✔
UNCOV
250
                $schema = [
146✔
UNCOV
251
                    'type' => 'array',
146✔
UNCOV
252
                    'items' => $valueSchema,
146✔
UNCOV
253
                ];
146✔
254
            } else { // List (int keys)
UNCOV
255
                $schema = [
12✔
UNCOV
256
                    'type' => 'object',
12✔
UNCOV
257
                    'additionalProperties' => $valueSchema,
12✔
UNCOV
258
                ];
12✔
259
            }
260

UNCOV
261
            return $this->applyNullability($schema, $isNullable);
147✔
262
        }
263

UNCOV
264
        if ($type instanceof ObjectType) {
529✔
UNCOV
265
            $schema = $this->getClassSchemaDefinition($type->getClassName(), $readableLink);
274✔
266

UNCOV
267
            return $this->applyNullability($schema, $isNullable);
274✔
268
        }
269

UNCOV
270
        if ($type instanceof BuiltinType) {
515✔
UNCOV
271
            $schema = match ($type->getTypeIdentifier()) {
515✔
UNCOV
272
                TypeIdentifier::INT => ['type' => 'integer'],
515✔
UNCOV
273
                TypeIdentifier::FLOAT => ['type' => 'number'],
445✔
UNCOV
274
                TypeIdentifier::BOOL => ['type' => 'boolean'],
443✔
UNCOV
275
                TypeIdentifier::TRUE => ['type' => 'boolean', 'const' => true],
429✔
UNCOV
276
                TypeIdentifier::FALSE => ['type' => 'boolean', 'const' => false],
429✔
UNCOV
277
                TypeIdentifier::STRING => ['type' => 'string'],
429✔
UNCOV
278
                TypeIdentifier::ARRAY => ['type' => 'array', 'items' => []],
40✔
UNCOV
279
                TypeIdentifier::ITERABLE => ['type' => 'array', 'items' => []],
40✔
UNCOV
280
                TypeIdentifier::OBJECT => ['type' => 'object'],
40✔
UNCOV
281
                TypeIdentifier::RESOURCE => ['type' => 'string'],
40✔
UNCOV
282
                TypeIdentifier::CALLABLE => ['type' => 'string'],
40✔
UNCOV
283
                TypeIdentifier::MIXED => ['type' => 'string'],
40✔
284
                default => ['type' => 'null'],
×
UNCOV
285
            };
515✔
286

UNCOV
287
            return $this->applyNullability($schema, $isNullable);
515✔
288
        }
289

UNCOV
290
        return ['type' => Schema::UNKNOWN_TYPE];
×
291
    }
292

293
    /**
294
     * Gets the JSON Schema definition for a class.
295
     */
296
    private function getClassSchemaDefinition(?string $className, ?bool $readableLink): array
297
    {
UNCOV
298
        if (null === $className) {
274✔
299
            return ['type' => 'string'];
×
300
        }
301

UNCOV
302
        if (is_a($className, \DateTimeInterface::class, true)) {
274✔
UNCOV
303
            return ['type' => 'string', 'format' => 'date-time'];
66✔
304
        }
305

UNCOV
306
        if (is_a($className, \DateInterval::class, true)) {
265✔
307
            return ['type' => 'string', 'format' => 'duration'];
×
308
        }
309

UNCOV
310
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
265✔
UNCOV
311
            return ['type' => 'string', 'format' => 'uuid'];
10✔
312
        }
313

UNCOV
314
        if (is_a($className, Ulid::class, true)) {
261✔
UNCOV
315
            return ['type' => 'string', 'format' => 'ulid'];
3✔
316
        }
317

UNCOV
318
        if (is_a($className, \SplFileInfo::class, true)) {
261✔
319
            return ['type' => 'string', 'format' => 'binary'];
×
320
        }
321

UNCOV
322
        if (is_a($className, \BcMath\Number::class, true)) {
261✔
UNCOV
323
            return ['type' => 'string', 'format' => 'string'];
6✔
324
        }
325

UNCOV
326
        $isResourceClass = $this->isResourceClass($className);
261✔
UNCOV
327
        if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) {
261✔
UNCOV
328
            $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
11✔
UNCOV
329
            $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer';
11✔
330

UNCOV
331
            return ['type' => $type, 'enum' => $enumCases];
11✔
332
        }
333

UNCOV
334
        if (false === $readableLink && $isResourceClass) {
261✔
UNCOV
335
            return [
185✔
UNCOV
336
                'type' => 'string',
185✔
UNCOV
337
                'format' => 'iri-reference',
185✔
UNCOV
338
                'example' => 'https://example.com/',
185✔
UNCOV
339
            ];
185✔
340
        }
341

UNCOV
342
        return ['type' => Schema::UNKNOWN_TYPE];
121✔
343
    }
344

345
    private function getLegacyTypeSchema(ApiProperty $propertyMetadata, array $propertySchema, string $resourceClass, string $property, ?bool $link): array
346
    {
347
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
×
348
        $className = ($types[0] ?? null)?->getClassName() ?? null;
×
349

350
        if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) && !$className) {
×
351
            $propertySchema['readOnly'] = true;
×
352
        }
353

354
        if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!$className || !$this->isResourceClass($className))) {
×
355
            if ($default instanceof \BackedEnum) {
×
356
                $default = $default->value;
×
357
            }
358
            $propertySchema['default'] = $default;
×
359
        }
360

361
        if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
×
362
            $propertySchema['example'] = $example;
×
363
        }
364

365
        // never override the following keys if at least one is already set or if there's a custom openapi context
366
        if (
367
            [] === $types
×
368
            || ($propertySchema['type'] ?? $propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
×
369
            || \array_key_exists('type', $propertyMetadata->getOpenapiContext() ?? [])
×
370
        ) {
371
            return $propertySchema;
×
372
        }
373

374
        if ($propertyMetadata->getUriTemplate()) {
×
375
            return $propertySchema + [
×
376
                'type' => 'string',
×
377
                'format' => 'iri-reference',
×
378
                'example' => 'https://example.com/',
×
379
            ];
×
380
        }
381

382
        $valueSchema = [];
×
383
        foreach ($types as $type) {
×
384
            // Temp fix for https://github.com/symfony/symfony/pull/52699
385
            if (ArrayCollection::class === $type->getClassName()) {
×
386
                $type = new LegacyType($type->getBuiltinType(), $type->isNullable(), $type->getClassName(), true, $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
×
387
            }
388

389
            if ($isCollection = $type->isCollection()) {
×
390
                $keyType = $type->getCollectionKeyTypes()[0] ?? null;
×
391
                $valueType = $type->getCollectionValueTypes()[0] ?? null;
×
392
            } else {
393
                $keyType = null;
×
394
                $valueType = $type;
×
395
            }
396

397
            if (null === $valueType) {
×
398
                $builtinType = 'string';
×
399
                $className = null;
×
400
            } else {
401
                $builtinType = $valueType->getBuiltinType();
×
402
                $className = $valueType->getClassName();
×
403
            }
404

405
            if ($isCollection && null !== $propertyMetadata->getUriTemplate()) {
×
406
                $keyType = null;
×
407
                $isCollection = false;
×
408
            }
409

410
            $propertyType = $this->getLegacyType(new LegacyType($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $link);
×
411
            if (!\in_array($propertyType, $valueSchema, true)) {
×
412
                $valueSchema[] = $propertyType;
×
413
            }
414
        }
415

416
        if (1 === \count($valueSchema)) {
×
417
            return $propertySchema + $valueSchema[0];
×
418
        }
419

420
        // multiple builtInTypes detected: determine oneOf/allOf if union vs intersect types
421
        try {
422
            $reflectionClass = new \ReflectionClass($resourceClass);
×
423
            $reflectionProperty = $reflectionClass->getProperty($property);
×
424
            $composition = $reflectionProperty->getType() instanceof \ReflectionUnionType ? 'oneOf' : 'allOf';
×
425
        } catch (\ReflectionException) {
×
426
            // cannot detect types
427
            $composition = 'anyOf';
×
428
        }
429

430
        return $propertySchema + [$composition => $valueSchema];
×
431
    }
432

433
    private function getLegacyType(LegacyType $type, ?bool $readableLink = null): array
434
    {
435
        if (!$type->isCollection()) {
×
436
            return $this->addNullabilityToTypeDefinition($this->legacyTypeToArray($type, $readableLink), $type);
×
437
        }
438

439
        $keyType = $type->getCollectionKeyTypes()[0] ?? null;
×
440
        $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new LegacyType($type->getBuiltinType(), false, $type->getClassName(), false);
×
441

442
        if (null !== $keyType && LegacyType::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
×
443
            return $this->addNullabilityToTypeDefinition([
×
444
                'type' => 'object',
×
445
                'additionalProperties' => $this->getLegacyType($subType, $readableLink),
×
446
            ], $type);
×
447
        }
448

449
        return $this->addNullabilityToTypeDefinition([
×
450
            'type' => 'array',
×
451
            'items' => $this->getLegacyType($subType, $readableLink),
×
452
        ], $type);
×
453
    }
454

455
    private function legacyTypeToArray(LegacyType $type, ?bool $readableLink = null): array
456
    {
457
        return match ($type->getBuiltinType()) {
×
458
            LegacyType::BUILTIN_TYPE_INT => ['type' => 'integer'],
×
459
            LegacyType::BUILTIN_TYPE_FLOAT => ['type' => 'number'],
×
460
            LegacyType::BUILTIN_TYPE_BOOL => ['type' => 'boolean'],
×
461
            LegacyType::BUILTIN_TYPE_OBJECT => $this->getLegacyClassType($type->getClassName(), $type->isNullable(), $readableLink),
×
462
            default => ['type' => 'string'],
×
463
        };
×
464
    }
465

466
    /**
467
     * Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
468
     *
469
     * Note: if the class is not part of exceptions listed above, any class is considered as a resource.
470
     *
471
     * @throws PropertyNotFoundException
472
     *
473
     * @return array<string, mixed>
474
     */
475
    private function getLegacyClassType(?string $className, bool $nullable, ?bool $readableLink): array
476
    {
477
        if (null === $className) {
×
478
            return ['type' => 'string'];
×
479
        }
480

481
        if (is_a($className, \DateTimeInterface::class, true)) {
×
482
            return [
×
483
                'type' => 'string',
×
484
                'format' => 'date-time',
×
485
            ];
×
486
        }
487

488
        if (is_a($className, \DateInterval::class, true)) {
×
489
            return [
×
490
                'type' => 'string',
×
491
                'format' => 'duration',
×
492
            ];
×
493
        }
494

495
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
×
496
            return [
×
497
                'type' => 'string',
×
498
                'format' => 'uuid',
×
499
            ];
×
500
        }
501

502
        if (is_a($className, Ulid::class, true)) {
×
503
            return [
×
504
                'type' => 'string',
×
505
                'format' => 'ulid',
×
506
            ];
×
507
        }
508

509
        if (is_a($className, \SplFileInfo::class, true)) {
×
510
            return [
×
511
                'type' => 'string',
×
512
                'format' => 'binary',
×
513
            ];
×
514
        }
515

516
        if (is_a($className, \BcMath\Number::class, true)) {
×
517
            return [
×
518
                'type' => 'string',
×
519
                'format' => 'string',
×
520
            ];
×
521
        }
522

523
        $isResourceClass = $this->isResourceClass($className);
×
524
        if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) {
×
525
            $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
×
526

527
            $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer';
×
528

529
            if ($nullable) {
×
530
                $enumCases[] = null;
×
531
            }
532

533
            return [
×
534
                'type' => $type,
×
535
                'enum' => $enumCases,
×
536
            ];
×
537
        }
538

539
        if (false === $readableLink && $isResourceClass) {
×
540
            return [
×
541
                'type' => 'string',
×
542
                'format' => 'iri-reference',
×
543
                'example' => 'https://example.com/',
×
544
            ];
×
545
        }
546

547
        // When this is set, we compute the schema at SchemaFactory::buildPropertySchema as it
548
        // will end up being a $ref to another class schema, we don't have enough informations here
549
        return ['type' => Schema::UNKNOWN_TYPE];
×
550
    }
551

552
    /**
553
     * @param array<string, mixed> $jsonSchema
554
     *
555
     * @return array<string, mixed>
556
     */
557
    private function addNullabilityToTypeDefinition(array $jsonSchema, LegacyType $type): array
558
    {
559
        if (!$type->isNullable()) {
×
560
            return $jsonSchema;
×
561
        }
562

563
        if (\array_key_exists('$ref', $jsonSchema)) {
×
564
            return ['anyOf' => [$jsonSchema, ['type' => 'null']]];
×
565
        }
566

567
        return [...$jsonSchema, ...[
×
568
            'type' => \is_array($jsonSchema['type'])
×
569
                ? array_merge($jsonSchema['type'], ['null'])
×
570
                : [$jsonSchema['type'], 'null'],
×
571
        ]];
×
572
    }
573

574
    private function getSchemaValue(array $schema, string $key): array|string|null
575
    {
UNCOV
576
        if (isset($schema['items'])) {
573✔
UNCOV
577
            $schema = $schema['items'];
3✔
578
        }
579

UNCOV
580
        return $schema[$key] ?? $schema['allOf'][0][$key] ?? $schema['anyOf'][0][$key] ?? $schema['oneOf'][0][$key] ?? null;
573✔
581
    }
582
}
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