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

api-platform / core / 10943429050

19 Sep 2024 02:48PM UTC coverage: 7.647% (-0.03%) from 7.675%
10943429050

push

github

web-flow
feat: api-platform/json-hal component (#6621)

* feat: add hal support for laravel

* feat: quick review

* fix: typo & cs-fixer

* fix: typo in composer.json

* fix: cs-fixer & phpstan

* fix: forgot about hal item normalizer, therefore there's no more createbook nor updatebook test as Hal is a readonly format

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

9082 existing lines in 291 files now uncovered.

12629 of 165144 relevant lines covered (7.65%)

22.89 hits per line

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

85.07
/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\Type;
25
use Symfony\Component\Uid\Ulid;
26
use Symfony\Component\Uid\Uuid;
27

28
/**
29
 * Build ApiProperty::schema.
30
 */
31
final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInterface
32
{
33
    use ResourceClassInfoTrait;
34

35
    public const JSON_SCHEMA_USER_DEFINED = 'user_defined_schema';
36

37
    public function __construct(ResourceClassResolverInterface $resourceClassResolver, private readonly ?PropertyMetadataFactoryInterface $decorated = null)
38
    {
UNCOV
39
        $this->resourceClassResolver = $resourceClassResolver;
2,258✔
40
    }
41

42
    public function create(string $resourceClass, string $property, array $options = []): ApiProperty
43
    {
UNCOV
44
        if (null === $this->decorated) {
532✔
45
            $propertyMetadata = new ApiProperty();
×
46
        } else {
47
            try {
UNCOV
48
                $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
532✔
49
            } catch (PropertyNotFoundException) {
×
50
                $propertyMetadata = new ApiProperty();
×
51
            }
52
        }
53

UNCOV
54
        $extraProperties = $propertyMetadata->getExtraProperties() ?? [];
532✔
55
        // see AttributePropertyMetadataFactory
UNCOV
56
        if (true === ($extraProperties[self::JSON_SCHEMA_USER_DEFINED] ?? false)) {
532✔
57
            // schema seems to have been declared by the user: do not override nor complete user value
58
            return $propertyMetadata;
3✔
59
        }
60

UNCOV
61
        $link = (($options['schema_type'] ?? null) === Schema::TYPE_INPUT) ? $propertyMetadata->isWritableLink() : $propertyMetadata->isReadableLink();
532✔
UNCOV
62
        $propertySchema = $propertyMetadata->getSchema() ?? [];
532✔
63

UNCOV
64
        if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable())) {
532✔
UNCOV
65
            $propertySchema['readOnly'] = true;
364✔
66
        }
67

UNCOV
68
        if (!\array_key_exists('writeOnly', $propertySchema) && false === $propertyMetadata->isReadable()) {
532✔
UNCOV
69
            $propertySchema['writeOnly'] = true;
108✔
70
        }
71

UNCOV
72
        if (!\array_key_exists('description', $propertySchema) && null !== ($description = $propertyMetadata->getDescription())) {
532✔
UNCOV
73
            $propertySchema['description'] = $description;
184✔
74
        }
75

76
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
UNCOV
77
        if (!\array_key_exists('deprecated', $propertySchema) && null !== $propertyMetadata->getDeprecationReason()) {
532✔
UNCOV
78
            $propertySchema['deprecated'] = true;
27✔
79
        }
80

81
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
82
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
UNCOV
83
        if (!\array_key_exists('externalDocs', $propertySchema) && null !== ($iri = $propertyMetadata->getTypes()[0] ?? null)) {
532✔
UNCOV
84
            $propertySchema['externalDocs'] = ['url' => $iri];
67✔
85
        }
86

UNCOV
87
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
532✔
88

UNCOV
89
        if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!\count($types) || null === ($className = $types[0]->getClassName()) || !$this->isResourceClass($className))) {
532✔
UNCOV
90
            if ($default instanceof \BackedEnum) {
89✔
UNCOV
91
                $default = $default->value;
15✔
92
            }
UNCOV
93
            $propertySchema['default'] = $default;
89✔
94
        }
95

UNCOV
96
        if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
532✔
UNCOV
97
            $propertySchema['example'] = $example;
9✔
98
        }
99

UNCOV
100
        if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
532✔
UNCOV
101
            $propertySchema['example'] = $propertySchema['default'];
89✔
102
        }
103

104
        // never override the following keys if at least one is already set or if there's a custom openapi context
UNCOV
105
        if ([] === $types
532✔
UNCOV
106
            || ($propertySchema['type'] ?? $propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
532✔
UNCOV
107
            || ($propertyMetadata->getOpenapiContext() ?? false)
532✔
108
        ) {
UNCOV
109
            return $propertyMetadata->withSchema($propertySchema);
73✔
110
        }
111

UNCOV
112
        $valueSchema = [];
484✔
UNCOV
113
        foreach ($types as $type) {
484✔
114
            // Temp fix for https://github.com/symfony/symfony/pull/52699
UNCOV
115
            if (ArrayCollection::class === $type->getClassName()) {
484✔
UNCOV
116
                $type = new Type($type->getBuiltinType(), $type->isNullable(), $type->getClassName(), true, $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
9✔
117
            }
118

UNCOV
119
            if ($isCollection = $type->isCollection()) {
484✔
UNCOV
120
                $keyType = $type->getCollectionKeyTypes()[0] ?? null;
140✔
UNCOV
121
                $valueType = $type->getCollectionValueTypes()[0] ?? null;
140✔
122
            } else {
UNCOV
123
                $keyType = null;
483✔
UNCOV
124
                $valueType = $type;
483✔
125
            }
126

UNCOV
127
            if (null === $valueType) {
484✔
UNCOV
128
                $builtinType = 'string';
39✔
UNCOV
129
                $className = null;
39✔
130
            } else {
UNCOV
131
                $builtinType = $valueType->getBuiltinType();
484✔
UNCOV
132
                $className = $valueType->getClassName();
484✔
133
            }
134

UNCOV
135
            if ($isCollection && null !== $propertyMetadata->getUriTemplate()) {
484✔
UNCOV
136
                $keyType = null;
15✔
UNCOV
137
                $isCollection = false;
15✔
138
            }
139

UNCOV
140
            $propertyType = $this->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $link);
484✔
UNCOV
141
            if (!\in_array($propertyType, $valueSchema, true)) {
484✔
UNCOV
142
                $valueSchema[] = $propertyType;
484✔
143
            }
144
        }
145

146
        // only one builtInType detected (should be "type" or "$ref")
UNCOV
147
        if (1 === \count($valueSchema)) {
484✔
UNCOV
148
            return $propertyMetadata->withSchema($propertySchema + $valueSchema[0]);
484✔
149
        }
150

151
        // multiple builtInTypes detected: determine oneOf/allOf if union vs intersect types
152
        try {
UNCOV
153
            $reflectionClass = new \ReflectionClass($resourceClass);
12✔
UNCOV
154
            $reflectionProperty = $reflectionClass->getProperty($property);
12✔
UNCOV
155
            $composition = $reflectionProperty->getType() instanceof \ReflectionUnionType ? 'oneOf' : 'allOf';
12✔
156
        } catch (\ReflectionException) {
×
157
            // cannot detect types
158
            $composition = 'anyOf';
×
159
        }
160

UNCOV
161
        return $propertyMetadata->withSchema($propertySchema + [$composition => $valueSchema]);
12✔
162
    }
163

164
    private function getType(Type $type, ?bool $readableLink = null): array
165
    {
UNCOV
166
        if (!$type->isCollection()) {
484✔
UNCOV
167
            return $this->addNullabilityToTypeDefinition($this->typeToArray($type, $readableLink), $type);
484✔
168
        }
169

UNCOV
170
        $keyType = $type->getCollectionKeyTypes()[0] ?? null;
134✔
UNCOV
171
        $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false);
134✔
172

UNCOV
173
        if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
134✔
174
            return $this->addNullabilityToTypeDefinition([
7✔
175
                'type' => 'object',
7✔
176
                'additionalProperties' => $this->getType($subType, $readableLink),
7✔
177
            ], $type);
7✔
178
        }
179

UNCOV
180
        return $this->addNullabilityToTypeDefinition([
133✔
UNCOV
181
            'type' => 'array',
133✔
UNCOV
182
            'items' => $this->getType($subType, $readableLink),
133✔
UNCOV
183
        ], $type);
133✔
184
    }
185

186
    private function typeToArray(Type $type, ?bool $readableLink = null): array
187
    {
UNCOV
188
        return match ($type->getBuiltinType()) {
484✔
UNCOV
189
            Type::BUILTIN_TYPE_INT => ['type' => 'integer'],
484✔
UNCOV
190
            Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'],
473✔
UNCOV
191
            Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'],
473✔
UNCOV
192
            Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable(), $readableLink),
469✔
UNCOV
193
            default => ['type' => 'string'],
484✔
UNCOV
194
        };
484✔
195
    }
196

197
    /**
198
     * 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.
199
     *
200
     * Note: if the class is not part of exceptions listed above, any class is considered as a resource.
201
     */
202
    private function getClassType(?string $className, bool $nullable, ?bool $readableLink): array
203
    {
UNCOV
204
        if (null === $className) {
262✔
205
            return ['type' => 'string'];
×
206
        }
207

UNCOV
208
        if (is_a($className, \DateTimeInterface::class, true)) {
262✔
UNCOV
209
            return [
68✔
UNCOV
210
                'type' => 'string',
68✔
UNCOV
211
                'format' => 'date-time',
68✔
UNCOV
212
            ];
68✔
213
        }
214

UNCOV
215
        if (is_a($className, \DateInterval::class, true)) {
249✔
216
            return [
×
217
                'type' => 'string',
×
218
                'format' => 'duration',
×
219
            ];
×
220
        }
221

UNCOV
222
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
249✔
UNCOV
223
            return [
9✔
UNCOV
224
                'type' => 'string',
9✔
UNCOV
225
                'format' => 'uuid',
9✔
UNCOV
226
            ];
9✔
227
        }
228

UNCOV
229
        if (is_a($className, Ulid::class, true)) {
249✔
230
            return [
×
231
                'type' => 'string',
×
232
                'format' => 'ulid',
×
233
            ];
×
234
        }
235

UNCOV
236
        if (is_a($className, \SplFileInfo::class, true)) {
249✔
237
            return [
×
238
                'type' => 'string',
×
239
                'format' => 'binary',
×
240
            ];
×
241
        }
242

UNCOV
243
        if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) {
249✔
UNCOV
244
            $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
15✔
245

UNCOV
246
            $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer';
15✔
247

UNCOV
248
            if ($nullable) {
15✔
UNCOV
249
                $enumCases[] = null;
15✔
250
            }
251

UNCOV
252
            return [
15✔
UNCOV
253
                'type' => $type,
15✔
UNCOV
254
                'enum' => $enumCases,
15✔
UNCOV
255
            ];
15✔
256
        }
257

UNCOV
258
        if (true !== $readableLink && $this->isResourceClass($className)) {
249✔
UNCOV
259
            return [
166✔
UNCOV
260
                'type' => 'string',
166✔
UNCOV
261
                'format' => 'iri-reference',
166✔
UNCOV
262
                'example' => 'https://example.com/',
166✔
UNCOV
263
            ];
166✔
264
        }
265

266
        // TODO: add propertyNameCollectionFactory and recurse to find the underlying schema? Right now SchemaFactory does the job so we don't compute anything here.
UNCOV
267
        return ['type' => Schema::UNKNOWN_TYPE];
137✔
268
    }
269

270
    /**
271
     * @param array<string, mixed> $jsonSchema
272
     *
273
     * @return array<string, mixed>
274
     */
275
    private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): array
276
    {
UNCOV
277
        if (!$type->isNullable()) {
484✔
UNCOV
278
            return $jsonSchema;
435✔
279
        }
280

UNCOV
281
        if (\array_key_exists('$ref', $jsonSchema)) {
284✔
282
            return ['anyOf' => [$jsonSchema, 'type' => 'null']];
×
283
        }
284

UNCOV
285
        return [...$jsonSchema, ...[
284✔
UNCOV
286
            'type' => \is_array($jsonSchema['type'])
284✔
287
                ? array_merge($jsonSchema['type'], ['null'])
×
UNCOV
288
                : [$jsonSchema['type'], 'null'],
284✔
UNCOV
289
        ]];
284✔
290
    }
291
}
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