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

api-platform / core / 6148660584

11 Sep 2023 03:40PM UTC coverage: 37.077% (-0.1%) from 37.185%
6148660584

push

github

soyuka
chore(symfony): security after validate when validator installed

10090 of 27214 relevant lines covered (37.08%)

19.39 hits per line

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

84.09
/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 (null !== $propertyMetadata->getUriTemplate() || (!\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
            if ($isCollection && null !== $propertyMetadata->getUriTemplate()) {
24✔
128
                $keyType = null;
3✔
129
                $isCollection = false;
3✔
130
            }
131

132
            $propertyType = $this->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $propertyMetadata->isReadableLink());
24✔
133
            if (!\in_array($propertyType, $valueSchema, true)) {
24✔
134
                $valueSchema[] = $propertyType;
24✔
135
            }
136
        }
137

138
        // only one builtInType detected (should be "type" or "$ref")
139
        if (1 === \count($valueSchema)) {
24✔
140
            return $propertyMetadata->withSchema($propertySchema + $valueSchema[0]);
24✔
141
        }
142

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

153
        return $propertyMetadata->withSchema($propertySchema + [$composition => $valueSchema]);
3✔
154
    }
155

156
    private function getType(Type $type, bool $readableLink = null): array
157
    {
158
        if (!$type->isCollection()) {
24✔
159
            return $this->addNullabilityToTypeDefinition($this->typeToArray($type, $readableLink), $type);
24✔
160
        }
161

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

165
        if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
12✔
166
            return $this->addNullabilityToTypeDefinition([
3✔
167
                'type' => 'object',
3✔
168
                'additionalProperties' => $this->getType($subType, $readableLink),
3✔
169
            ], $type);
3✔
170
        }
171

172
        return $this->addNullabilityToTypeDefinition([
12✔
173
            'type' => 'array',
12✔
174
            'items' => $this->getType($subType, $readableLink),
12✔
175
        ], $type);
12✔
176
    }
177

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

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

200
        if (is_a($className, \DateTimeInterface::class, true)) {
12✔
201
            return [
6✔
202
                'type' => 'string',
6✔
203
                'format' => 'date-time',
6✔
204
            ];
6✔
205
        }
206

207
        if (is_a($className, \DateInterval::class, true)) {
12✔
208
            return [
×
209
                'type' => 'string',
×
210
                'format' => 'duration',
×
211
            ];
×
212
        }
213

214
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
12✔
215
            return [
3✔
216
                'type' => 'string',
3✔
217
                'format' => 'uuid',
3✔
218
            ];
3✔
219
        }
220

221
        if (is_a($className, Ulid::class, true)) {
12✔
222
            return [
×
223
                'type' => 'string',
×
224
                'format' => 'ulid',
×
225
            ];
×
226
        }
227

228
        if (is_a($className, \SplFileInfo::class, true)) {
12✔
229
            return [
×
230
                'type' => 'string',
×
231
                'format' => 'binary',
×
232
            ];
×
233
        }
234

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

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

240
            if ($nullable) {
3✔
241
                $enumCases[] = null;
3✔
242
            }
243

244
            return [
3✔
245
                'type' => $type,
3✔
246
                'enum' => $enumCases,
3✔
247
            ];
3✔
248
        }
249

250
        if (true !== $readableLink && $this->isResourceClass($className)) {
12✔
251
            return [
9✔
252
                'type' => 'string',
9✔
253
                'format' => 'iri-reference',
9✔
254
            ];
9✔
255
        }
256

257
        return ['type' => 'string'];
9✔
258
    }
259

260
    /**
261
     * @param array<string, mixed> $jsonSchema
262
     *
263
     * @return array<string, mixed>
264
     */
265
    private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): array
266
    {
267
        if (!$type->isNullable()) {
24✔
268
            return $jsonSchema;
21✔
269
        }
270

271
        if (\array_key_exists('$ref', $jsonSchema)) {
12✔
272
            return ['anyOf' => [$jsonSchema, 'type' => 'null']];
×
273
        }
274

275
        return [...$jsonSchema, ...[
12✔
276
            'type' => \is_array($jsonSchema['type'])
12✔
277
                ? array_merge($jsonSchema['type'], ['null'])
×
278
                : [$jsonSchema['type'], 'null'],
12✔
279
        ]];
12✔
280
    }
281
}
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