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

api-platform / core / 14635100171

24 Apr 2025 06:39AM UTC coverage: 8.271% (+0.02%) from 8.252%
14635100171

Pull #6904

github

web-flow
Merge c9cefd82e into a3e5e53ea
Pull Request #6904: feat(graphql): added support for graphql subscriptions to work for actions

0 of 73 new or added lines in 3 files covered. (0.0%)

1999 existing lines in 144 files now uncovered.

13129 of 158728 relevant lines covered (8.27%)

13.6 hits per line

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

51.15
/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\CompositeTypeInterface;
30
use Symfony\Component\TypeInfo\Type\IntersectionType;
31
use Symfony\Component\TypeInfo\Type\ObjectType;
32
use Symfony\Component\TypeInfo\Type\UnionType;
33
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
34
use Symfony\Component\TypeInfo\TypeIdentifier;
35
use Symfony\Component\Uid\Ulid;
36
use Symfony\Component\Uid\Uuid;
37

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

45
    public const JSON_SCHEMA_USER_DEFINED = 'user_defined_schema';
46

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

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

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

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

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

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

84
        if (!\array_key_exists('description', $propertySchema) && null !== ($description = $propertyMetadata->getDescription())) {
304✔
85
            $propertySchema['description'] = $description;
105✔
86
        }
87

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

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

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

103
        return $propertyMetadata->withSchema($this->getTypeSchema($propertyMetadata, $propertySchema, $link));
304✔
104
    }
105

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

110
        $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
304✔
111
            return match (true) {
112
                $type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
76✔
113
                $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
76✔
114
                $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
76✔
115
                default => $type instanceof ObjectType && $this->isResourceClass($type->getClassName()),
76✔
116
            };
117
        };
304✔
118

119
        if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && !$type?->isSatisfiedBy($typeIsResourceClass)) {
304✔
120
            if ($default instanceof \BackedEnum) {
75✔
121
                $default = $default->value;
9✔
122
            }
123
            $propertySchema['default'] = $default;
75✔
124
        }
125

126
        if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
304✔
127
            $propertySchema['example'] = $example;
5✔
128
        }
129

130
        if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
304✔
131
            $propertySchema['example'] = $propertySchema['default'];
75✔
132
        }
133

134
        // never override the following keys if at least one is already set or if there's a custom openapi context
135
        if (
136
            null === $type
304✔
137
            || ($propertySchema['type'] ?? $propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
304✔
138
            || \array_key_exists('type', $propertyMetadata->getOpenapiContext() ?? [])
304✔
139
        ) {
140
            return $propertySchema;
46✔
141
        }
142

143
        if ($type instanceof CollectionType && null !== $propertyMetadata->getUriTemplate()) {
289✔
UNCOV
144
            $type = $type->getCollectionValueType();
5✔
145
        }
146

147
        return $propertySchema + $this->getJsonSchemaFromType($type, $link);
289✔
148
    }
149

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

164
        if (isset($schema['type']) && 'null' === $schema['type'] && 1 === \count($schema)) {
148✔
165
            return $schema;
41✔
166
        }
167

168
        if (isset($schema['anyOf']) && \is_array($schema['anyOf'])) {
144✔
169
            $hasNull = false;
8✔
170
            foreach ($schema['anyOf'] as $anyOfSchema) {
8✔
171
                if (isset($anyOfSchema['type']) && 'null' === $anyOfSchema['type']) {
8✔
172
                    $hasNull = true;
×
173
                    break;
×
174
                }
175
            }
176
            if (!$hasNull) {
8✔
177
                $schema['anyOf'][] = ['type' => 'null'];
8✔
178
            }
179

180
            return $schema;
8✔
181
        }
182

183
        if (isset($schema['type'])) {
144✔
184
            $currentType = $schema['type'];
144✔
185
            $schema['type'] = \is_array($currentType) ? array_merge($currentType, ['null']) : [$currentType, 'null'];
144✔
186

187
            if (isset($schema['enum'])) {
144✔
188
                $schema['enum'][] = null;
11✔
189

190
                return $schema;
11✔
191
            }
192

193
            return $schema;
140✔
194
        }
195

196
        return ['anyOf' => [$schema, ['type' => 'null']]];
×
197
    }
198

199
    /**
200
     * Converts a TypeInfo Type into a JSON Schema definition array.
201
     *
202
     * @return array<string, mixed>
203
     */
204
    private function getJsonSchemaFromType(Type $type, ?bool $readableLink = null): array
205
    {
206
        $isNullable = $type->isNullable();
289✔
207

208
        if ($type instanceof UnionType) {
289✔
209
            $subTypes = array_filter($type->getTypes(), fn ($t) => !($t instanceof BuiltinType && $t->isIdentifiedBy(TypeIdentifier::NULL)));
149✔
210

211
            foreach ($subTypes as $t) {
149✔
212
                $s = $this->getJsonSchemaFromType($t, $readableLink);
149✔
213
                // We can not find what type this is, let it be computed at runtime by the SchemaFactory
214
                if (($s['type'] ?? null) === Schema::UNKNOWN_TYPE) {
149✔
215
                    return $s;
42✔
216
                }
217
            }
218

219
            $schemas = array_map(fn ($t) => $this->getJsonSchemaFromType($t, $readableLink), $subTypes);
144✔
220

221
            if (0 === \count($schemas)) {
144✔
222
                $schema = [];
×
223
            } elseif (1 === \count($schemas)) {
144✔
224
                $schema = current($schemas);
144✔
225
            } else {
226
                $schema = ['anyOf' => $schemas];
9✔
227
            }
228

229
            return $this->applyNullability($schema, $isNullable);
144✔
230
        }
231

232
        if ($type instanceof IntersectionType) {
289✔
UNCOV
233
            $schemas = [];
3✔
UNCOV
234
            foreach ($type->getTypes() as $t) {
3✔
UNCOV
235
                while ($t instanceof WrappingTypeInterface) {
3✔
236
                    $t = $t->getWrappedType();
×
237
                }
238

UNCOV
239
                $subSchema = $this->getJsonSchemaFromType($t, $readableLink);
3✔
UNCOV
240
                if (!empty($subSchema)) {
3✔
UNCOV
241
                    $schemas[] = $subSchema;
3✔
242
                }
243
            }
244

UNCOV
245
            return $this->applyNullability(['allOf' => $schemas], $isNullable);
3✔
246
        }
247

248
        if ($type instanceof CollectionType) {
289✔
249
            $valueType = $type->getCollectionValueType();
104✔
250
            $valueSchema = $this->getJsonSchemaFromType($valueType, $readableLink);
104✔
251
            $keyType = $type->getCollectionKeyType();
104✔
252

253
            // Associative array (string keys)
254
            if ($keyType->isSatisfiedBy(fn (Type $t) => $t instanceof BuiltinType && $t->isIdentifiedBy(TypeIdentifier::INT))) {
104✔
255
                $schema = [
104✔
256
                    'type' => 'array',
104✔
257
                    'items' => $valueSchema,
104✔
258
                ];
104✔
259
            } else { // List (int keys)
260
                $schema = [
5✔
261
                    'type' => 'object',
5✔
262
                    'additionalProperties' => $valueSchema,
5✔
263
                ];
5✔
264
            }
265

266
            return $this->applyNullability($schema, $isNullable);
104✔
267
        }
268

269
        if ($type instanceof ObjectType) {
289✔
270
            $schema = $this->getClassSchemaDefinition($type->getClassName(), $readableLink);
147✔
271

272
            return $this->applyNullability($schema, $isNullable);
147✔
273
        }
274

275
        if ($type instanceof BuiltinType) {
282✔
276
            $schema = match ($type->getTypeIdentifier()) {
282✔
277
                TypeIdentifier::INT => ['type' => 'integer'],
282✔
278
                TypeIdentifier::FLOAT => ['type' => 'number'],
253✔
279
                TypeIdentifier::BOOL => ['type' => 'boolean'],
249✔
280
                TypeIdentifier::TRUE => ['type' => 'boolean', 'const' => true],
243✔
281
                TypeIdentifier::FALSE => ['type' => 'boolean', 'const' => false],
243✔
282
                TypeIdentifier::STRING => ['type' => 'string'],
243✔
283
                TypeIdentifier::ARRAY => ['type' => 'array', 'items' => []],
41✔
284
                TypeIdentifier::ITERABLE => ['type' => 'array', 'items' => []],
41✔
285
                TypeIdentifier::OBJECT => ['type' => 'object'],
41✔
286
                TypeIdentifier::RESOURCE => ['type' => 'string'],
41✔
287
                TypeIdentifier::CALLABLE => ['type' => 'string'],
41✔
288
                default => ['type' => 'null'],
41✔
289
            };
282✔
290

291
            return $this->applyNullability($schema, $isNullable);
282✔
292
        }
293

294
        return ['type' => Schema::UNKNOWN_TYPE];
×
295
    }
296

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

306
        if (is_a($className, \DateTimeInterface::class, true)) {
147✔
307
            return ['type' => 'string', 'format' => 'date-time'];
54✔
308
        }
309

310
        if (is_a($className, \DateInterval::class, true)) {
129✔
311
            return ['type' => 'string', 'format' => 'duration'];
×
312
        }
313

314
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
129✔
UNCOV
315
            return ['type' => 'string', 'format' => 'uuid'];
2✔
316
        }
317

318
        if (is_a($className, Ulid::class, true)) {
129✔
319
            return ['type' => 'string', 'format' => 'ulid'];
×
320
        }
321

322
        if (is_a($className, \SplFileInfo::class, true)) {
129✔
323
            return ['type' => 'string', 'format' => 'binary'];
×
324
        }
325

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

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

334
        // If it's a resource and links are not readable, represent as IRI string.
335
        if ($isResourceClass && true !== $readableLink) {
129✔
336
            return [
70✔
337
                'type' => 'string',
70✔
338
                'format' => 'iri-reference',
70✔
339
                'example' => 'https://example.com/', // Add a generic example
70✔
340
            ];
70✔
341
        }
342

343
        return ['type' => Schema::UNKNOWN_TYPE];
90✔
344
    }
345

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

350
        if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!\count($types) || null === ($className = $types[0]->getClassName()) || !$this->isResourceClass($className))) {
×
351
            if ($default instanceof \BackedEnum) {
×
352
                $default = $default->value;
×
353
            }
354
            $propertySchema['default'] = $default;
×
355
        }
356

357
        if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
×
358
            $propertySchema['example'] = $example;
×
359
        }
360

361
        if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
×
362
            $propertySchema['example'] = $propertySchema['default'];
×
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
        $valueSchema = [];
×
375
        foreach ($types as $type) {
×
376
            // Temp fix for https://github.com/symfony/symfony/pull/52699
377
            if (ArrayCollection::class === $type->getClassName()) {
×
378
                $type = new LegacyType($type->getBuiltinType(), $type->isNullable(), $type->getClassName(), true, $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
×
379
            }
380

381
            if ($isCollection = $type->isCollection()) {
×
382
                $keyType = $type->getCollectionKeyTypes()[0] ?? null;
×
383
                $valueType = $type->getCollectionValueTypes()[0] ?? null;
×
384
            } else {
385
                $keyType = null;
×
386
                $valueType = $type;
×
387
            }
388

389
            if (null === $valueType) {
×
390
                $builtinType = 'string';
×
391
                $className = null;
×
392
            } else {
393
                $builtinType = $valueType->getBuiltinType();
×
394
                $className = $valueType->getClassName();
×
395
            }
396

397
            if ($isCollection && null !== $propertyMetadata->getUriTemplate()) {
×
398
                $keyType = null;
×
399
                $isCollection = false;
×
400
            }
401

402
            $propertyType = $this->getLegacyType(new LegacyType($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $link);
×
403
            if (!\in_array($propertyType, $valueSchema, true)) {
×
404
                $valueSchema[] = $propertyType;
×
405
            }
406
        }
407

408
        if (1 === \count($valueSchema)) {
×
409
            return $propertySchema + $valueSchema[0];
×
410
        }
411

412
        // multiple builtInTypes detected: determine oneOf/allOf if union vs intersect types
413
        try {
414
            $reflectionClass = new \ReflectionClass($resourceClass);
×
415
            $reflectionProperty = $reflectionClass->getProperty($property);
×
416
            $composition = $reflectionProperty->getType() instanceof \ReflectionUnionType ? 'oneOf' : 'allOf';
×
417
        } catch (\ReflectionException) {
×
418
            // cannot detect types
419
            $composition = 'anyOf';
×
420
        }
421

422
        return $propertySchema + [$composition => $valueSchema];
×
423
    }
424

425
    private function getLegacyType(LegacyType $type, ?bool $readableLink = null): array
426
    {
427
        if (!$type->isCollection()) {
×
428
            return $this->addNullabilityToTypeDefinition($this->legacyTypeToArray($type, $readableLink), $type);
×
429
        }
430

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

434
        if (null !== $keyType && LegacyType::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
×
435
            return $this->addNullabilityToTypeDefinition([
×
436
                'type' => 'object',
×
437
                'additionalProperties' => $this->getLegacyType($subType, $readableLink),
×
438
            ], $type);
×
439
        }
440

441
        return $this->addNullabilityToTypeDefinition([
×
442
            'type' => 'array',
×
443
            'items' => $this->getLegacyType($subType, $readableLink),
×
444
        ], $type);
×
445
    }
446

447
    private function legacyTypeToArray(LegacyType $type, ?bool $readableLink = null): array
448
    {
449
        return match ($type->getBuiltinType()) {
×
450
            LegacyType::BUILTIN_TYPE_INT => ['type' => 'integer'],
×
451
            LegacyType::BUILTIN_TYPE_FLOAT => ['type' => 'number'],
×
452
            LegacyType::BUILTIN_TYPE_BOOL => ['type' => 'boolean'],
×
453
            LegacyType::BUILTIN_TYPE_OBJECT => $this->getLegacyClassType($type->getClassName(), $type->isNullable(), $readableLink),
×
454
            default => ['type' => 'string'],
×
455
        };
×
456
    }
457

458
    /**
459
     * 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.
460
     *
461
     * Note: if the class is not part of exceptions listed above, any class is considered as a resource.
462
     *
463
     * @throws PropertyNotFoundException
464
     */
465
    private function getLegacyClassType(?string $className, bool $nullable, ?bool $readableLink): array
466
    {
467
        if (null === $className) {
×
468
            return ['type' => 'string'];
×
469
        }
470

471
        if (is_a($className, \DateTimeInterface::class, true)) {
×
472
            return [
×
473
                'type' => 'string',
×
474
                'format' => 'date-time',
×
475
            ];
×
476
        }
477

478
        if (is_a($className, \DateInterval::class, true)) {
×
479
            return [
×
480
                'type' => 'string',
×
481
                'format' => 'duration',
×
482
            ];
×
483
        }
484

485
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
×
486
            return [
×
487
                'type' => 'string',
×
488
                'format' => 'uuid',
×
489
            ];
×
490
        }
491

492
        if (is_a($className, Ulid::class, true)) {
×
493
            return [
×
494
                'type' => 'string',
×
495
                'format' => 'ulid',
×
496
            ];
×
497
        }
498

499
        if (is_a($className, \SplFileInfo::class, true)) {
×
500
            return [
×
501
                'type' => 'string',
×
502
                'format' => 'binary',
×
503
            ];
×
504
        }
505

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

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

512
            if ($nullable) {
×
513
                $enumCases[] = null;
×
514
            }
515

516
            return [
×
517
                'type' => $type,
×
518
                'enum' => $enumCases,
×
519
            ];
×
520
        }
521

522
        if (true !== $readableLink && $isResourceClass) {
×
523
            return [
×
524
                'type' => 'string',
×
525
                'format' => 'iri-reference',
×
526
                'example' => 'https://example.com/',
×
527
            ];
×
528
        }
529

530
        return ['type' => Schema::UNKNOWN_TYPE];
×
531
    }
532

533
    /**
534
     * @param array<string, mixed> $jsonSchema
535
     *
536
     * @return array<string, mixed>
537
     */
538
    private function addNullabilityToTypeDefinition(array $jsonSchema, LegacyType $type): array
539
    {
540
        if (!$type->isNullable()) {
×
541
            return $jsonSchema;
×
542
        }
543

544
        if (\array_key_exists('$ref', $jsonSchema)) {
×
545
            return ['anyOf' => [$jsonSchema, ['type' => 'null']]];
×
546
        }
547

548
        return [...$jsonSchema, ...[
×
549
            'type' => \is_array($jsonSchema['type'])
×
550
                ? array_merge($jsonSchema['type'], ['null'])
×
551
                : [$jsonSchema['type'], 'null'],
×
552
        ]];
×
553
    }
554
}
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