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

api-platform / core / 6067528200

04 Sep 2023 12:12AM UTC coverage: 36.875% (-21.9%) from 58.794%
6067528200

Pull #5791

github

web-flow
Merge 64157e578 into d09cfc9d2
Pull Request #5791: fix: strip down any sql function name

3096 of 3096 new or added lines in 205 files covered. (100.0%)

9926 of 26918 relevant lines covered (36.87%)

6.5 hits per line

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

83.72
/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\Exception\PropertyNotFoundException;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
19
use ApiPlatform\Metadata\ResourceClassResolverInterface;
20
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
21
use Ramsey\Uuid\UuidInterface;
22
use Symfony\Component\PropertyInfo\Type;
23
use Symfony\Component\Uid\Ulid;
24
use Symfony\Component\Uid\Uuid;
25

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

33
    public function __construct(ResourceClassResolverInterface $resourceClassResolver, private readonly ?PropertyMetadataFactoryInterface $decorated = null)
34
    {
35
        $this->resourceClassResolver = $resourceClassResolver;
105✔
36
    }
37

38
    public function create(string $resourceClass, string $property, array $options = []): ApiProperty
39
    {
40
        if (null === $this->decorated) {
24✔
41
            $propertyMetadata = new ApiProperty();
×
42
        } else {
43
            try {
44
                $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
24✔
45
            } catch (PropertyNotFoundException) {
×
46
                $propertyMetadata = new ApiProperty();
×
47
            }
48
        }
49

50
        $propertySchema = $propertyMetadata->getSchema() ?? [];
24✔
51

52
        if (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) {
24✔
53
            $propertySchema['readOnly'] = true;
18✔
54
        }
55

56
        if (!\array_key_exists('writeOnly', $propertySchema) && false === $propertyMetadata->isReadable()) {
24✔
57
            $propertySchema['writeOnly'] = true;
6✔
58
        }
59

60
        if (!\array_key_exists('description', $propertySchema) && null !== ($description = $propertyMetadata->getDescription())) {
24✔
61
            $propertySchema['description'] = $description;
6✔
62
        }
63

64
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
65
        if (!\array_key_exists('deprecated', $propertySchema) && null !== $propertyMetadata->getDeprecationReason()) {
24✔
66
            $propertySchema['deprecated'] = true;
3✔
67
        }
68

69
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
70
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
71
        if (!\array_key_exists('externalDocs', $propertySchema) && null !== ($iri = $propertyMetadata->getTypes()[0] ?? null)) {
24✔
72
            $propertySchema['externalDocs'] = ['url' => $iri];
3✔
73
        }
74

75
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
24✔
76

77
        if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!\count($types) || null === ($className = $types[0]->getClassName()) || !$this->isResourceClass($className))) {
24✔
78
            if ($default instanceof \BackedEnum) {
3✔
79
                $default = $default->value;
3✔
80
            }
81
            $propertySchema['default'] = $default;
3✔
82
        }
83

84
        if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
24✔
85
            $propertySchema['example'] = $example;
×
86
        }
87

88
        if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
24✔
89
            $propertySchema['example'] = $propertySchema['default'];
3✔
90
        }
91

92
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
24✔
93

94
        // never override the following keys if at least one is already set
95
        if ([] === $types
24✔
96
            || ($propertySchema['type'] ?? $propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
24✔
97
        ) {
98
            return $propertyMetadata->withSchema($propertySchema);
3✔
99
        }
100

101
        $valueSchema = [];
24✔
102
        foreach ($types as $type) {
24✔
103
            if ($isCollection = $type->isCollection()) {
24✔
104
                $keyType = $type->getCollectionKeyTypes()[0] ?? null;
12✔
105
                $valueType = $type->getCollectionValueTypes()[0] ?? null;
12✔
106
            } else {
107
                $keyType = null;
24✔
108
                $valueType = $type;
24✔
109
            }
110

111
            if (null === $valueType) {
24✔
112
                $builtinType = 'string';
6✔
113
                $className = null;
6✔
114
            } else {
115
                $builtinType = $valueType->getBuiltinType();
24✔
116
                $className = $valueType->getClassName();
24✔
117
            }
118

119
            if (!\array_key_exists('owl:maxCardinality', $propertySchema)
24✔
120
                && !$isCollection
24✔
121
                && null !== $className
24✔
122
                && $this->resourceClassResolver->isResourceClass($className)
24✔
123
            ) {
124
                $propertySchema['owl:maxCardinality'] = 1;
6✔
125
            }
126

127
            $propertyType = $this->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $propertyMetadata->isReadableLink());
24✔
128
            if (!\in_array($propertyType, $valueSchema, true)) {
24✔
129
                $valueSchema[] = $propertyType;
24✔
130
            }
131
        }
132

133
        // only one builtInType detected (should be "type" or "$ref")
134
        if (1 === \count($valueSchema)) {
24✔
135
            return $propertyMetadata->withSchema($propertySchema + $valueSchema[0]);
24✔
136
        }
137

138
        // multiple builtInTypes detected: determine oneOf/allOf if union vs intersect types
139
        try {
140
            $reflectionClass = new \ReflectionClass($resourceClass);
3✔
141
            $reflectionProperty = $reflectionClass->getProperty($property);
3✔
142
            $composition = $reflectionProperty->getType() instanceof \ReflectionUnionType ? 'oneOf' : 'allOf';
3✔
143
        } catch (\ReflectionException) {
×
144
            // cannot detect types
145
            $composition = 'anyOf';
×
146
        }
147

148
        return $propertyMetadata->withSchema($propertySchema + [$composition => $valueSchema]);
3✔
149
    }
150

151
    private function getType(Type $type, bool $readableLink = null): array
152
    {
153
        if (!$type->isCollection()) {
24✔
154
            return $this->addNullabilityToTypeDefinition($this->typeToArray($type, $readableLink), $type);
24✔
155
        }
156

157
        $keyType = $type->getCollectionKeyTypes()[0] ?? null;
12✔
158
        $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false);
12✔
159

160
        if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
12✔
161
            return $this->addNullabilityToTypeDefinition([
3✔
162
                'type' => 'object',
3✔
163
                'additionalProperties' => $this->getType($subType, $readableLink),
3✔
164
            ], $type);
3✔
165
        }
166

167
        return $this->addNullabilityToTypeDefinition([
12✔
168
            'type' => 'array',
12✔
169
            'items' => $this->getType($subType, $readableLink),
12✔
170
        ], $type);
12✔
171
    }
172

173
    private function typeToArray(Type $type, bool $readableLink = null): array
174
    {
175
        return match ($type->getBuiltinType()) {
24✔
176
            Type::BUILTIN_TYPE_INT => ['type' => 'integer'],
24✔
177
            Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'],
24✔
178
            Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'],
24✔
179
            Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable(), $readableLink),
24✔
180
            default => ['type' => 'string'],
24✔
181
        };
24✔
182
    }
183

184
    /**
185
     * 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.
186
     *
187
     * Note: if the class is not part of exceptions listed above, any class is considered as a resource.
188
     */
189
    private function getClassType(?string $className, bool $nullable, ?bool $readableLink): array
190
    {
191
        if (null === $className) {
12✔
192
            return ['type' => 'string'];
×
193
        }
194

195
        if (is_a($className, \DateTimeInterface::class, true)) {
12✔
196
            return [
6✔
197
                'type' => 'string',
6✔
198
                'format' => 'date-time',
6✔
199
            ];
6✔
200
        }
201

202
        if (is_a($className, \DateInterval::class, true)) {
12✔
203
            return [
×
204
                'type' => 'string',
×
205
                'format' => 'duration',
×
206
            ];
×
207
        }
208

209
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
12✔
210
            return [
3✔
211
                'type' => 'string',
3✔
212
                'format' => 'uuid',
3✔
213
            ];
3✔
214
        }
215

216
        if (is_a($className, Ulid::class, true)) {
12✔
217
            return [
×
218
                'type' => 'string',
×
219
                'format' => 'ulid',
×
220
            ];
×
221
        }
222

223
        if (is_a($className, \SplFileInfo::class, true)) {
12✔
224
            return [
×
225
                'type' => 'string',
×
226
                'format' => 'binary',
×
227
            ];
×
228
        }
229

230
        if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) {
12✔
231
            $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
3✔
232

233
            $type = \is_string($enumCases[0] ?? '') ? 'string' : 'int';
3✔
234

235
            if ($nullable) {
3✔
236
                $enumCases[] = null;
3✔
237
            }
238

239
            return [
3✔
240
                'type' => $type,
3✔
241
                'enum' => $enumCases,
3✔
242
            ];
3✔
243
        }
244

245
        if (true !== $readableLink && $this->isResourceClass($className)) {
12✔
246
            return [
9✔
247
                'type' => 'string',
9✔
248
                'format' => 'iri-reference',
9✔
249
            ];
9✔
250
        }
251

252
        return ['type' => 'string'];
9✔
253
    }
254

255
    /**
256
     * @param array<string, mixed> $jsonSchema
257
     *
258
     * @return array<string, mixed>
259
     */
260
    private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): array
261
    {
262
        if (!$type->isNullable()) {
24✔
263
            return $jsonSchema;
21✔
264
        }
265

266
        if (\array_key_exists('$ref', $jsonSchema)) {
12✔
267
            return ['anyOf' => [$jsonSchema, 'type' => 'null']];
×
268
        }
269

270
        return [...$jsonSchema, ...[
12✔
271
            'type' => \is_array($jsonSchema['type'])
12✔
272
                ? array_merge($jsonSchema['type'], ['null'])
×
273
                : [$jsonSchema['type'], 'null'],
12✔
274
        ]];
12✔
275
    }
276
}
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