Completed
Pull Request — 2.5 (#3402)
by Marco
05:35
created

TypeFactory::addNullabilityToTypeDefinition()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
nc 3
nop 3
dl 0
loc 14
rs 10
c 1
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
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\Core\JsonSchema;
15
16
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
17
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
18
use Ramsey\Uuid\UuidInterface;
19
use Symfony\Component\PropertyInfo\Type;
20
21
/**
22
 * {@inheritdoc}
23
 *
24
 * @experimental
25
 *
26
 * @author Kévin Dunglas <[email protected]>
27
 */
28
final class TypeFactory implements TypeFactoryInterface
29
{
30
    /**
31
     * This constant is to be provided as serializer context key to conditionally enable types to be generated in
32
     * a format that is compatible with OpenAPI specifications **PREVIOUS** to 3.0.
33
     *
34
     * Without this flag being set, the generated format will only be compatible with Swagger 3.0 or newer.
35
     *
36
     * Once support for OpenAPI < 3.0 is gone, this constant **WILL BE REMOVED**
37
     *
38
     * @internal Once support for OpenAPI < 3.0 is gone, this constant **WILL BE REMOVED** - do not rely on
39
     *           it in downstream projects!
40
     */
41
    public const CONTEXT_SERIALIZATION_FORMAT_OPENAPI_PRE_V3_0 = self::class.'::CONTEXT_SERIALIZATION_FORMAT_OPENAPI_PRE_V3_0';
42
43
    use ResourceClassInfoTrait;
44
45
    /**
46
     * @var SchemaFactoryInterface|null
47
     */
48
    private $schemaFactory;
49
50
    public function __construct(ResourceClassResolverInterface $resourceClassResolver = null)
51
    {
52
        $this->resourceClassResolver = $resourceClassResolver;
53
    }
54
55
    public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
56
    {
57
        $this->schemaFactory = $schemaFactory;
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array
64
    {
65
        if ($type->isCollection()) {
66
            $keyType = $type->getCollectionKeyType();
67
            $subType = $type->getCollectionValueType()
68
                ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false);
69
70
            if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
71
                return $this->addNullabilityToTypeDefinition(
72
                    [
73
                        'type' => 'object',
74
                        'additionalProperties' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema),
75
                    ],
76
                    $type,
77
                    (array) $serializerContext
78
                );
79
            }
80
81
            return $this->addNullabilityToTypeDefinition(
82
                [
83
                    'type' => 'array',
84
                    'items' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema),
85
                ],
86
                $type,
87
                (array) $serializerContext
88
            );
89
        }
90
91
        return $this->addNullabilityToTypeDefinition(
92
            $this->makeBasicType($type, $format, $readableLink, $serializerContext, $schema),
93
            $type,
94
            (array) $serializerContext
95
        );
96
    }
97
98
    private function makeBasicType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array
99
    {
100
        switch ($type->getBuiltinType()) {
101
            case Type::BUILTIN_TYPE_INT:
102
                return ['type' => 'integer'];
103
            case Type::BUILTIN_TYPE_FLOAT:
104
                return ['type' => 'number'];
105
            case Type::BUILTIN_TYPE_BOOL:
106
                return ['type' => 'boolean'];
107
            case Type::BUILTIN_TYPE_OBJECT:
108
                return $this->getClassType($type->getClassName(), $format, $readableLink, $serializerContext, $schema);
109
            default:
110
                return ['type' => 'string'];
111
        }
112
    }
113
114
    /**
115
     * 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.
116
     */
117
    private function getClassType(?string $className, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array
118
    {
119
        if (null === $className) {
120
            return ['type' => 'object'];
121
        }
122
123
        if (is_a($className, \DateTimeInterface::class, true)) {
124
            return [
125
                'type' => 'string',
126
                'format' => 'date-time',
127
            ];
128
        }
129
        if (is_a($className, \DateInterval::class, true)) {
130
            return [
131
                'type' => 'string',
132
                'format' => 'duration',
133
            ];
134
        }
135
        if (is_a($className, UuidInterface::class, true)) {
136
            return [
137
                'type' => 'string',
138
                'format' => 'uuid',
139
            ];
140
        }
141
142
        // Skip if $schema is null (filters only support basic types)
143
        if (null === $schema) {
144
            return ['type' => 'object'];
145
        }
146
147
        if ($this->isResourceClass($className) && true !== $readableLink) {
148
            return [
149
                'type' => 'string',
150
                'format' => 'iri-reference',
151
            ];
152
        }
153
154
        $version = $schema->getVersion();
155
156
        $subSchema = new Schema($version);
157
        $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema
158
159
        if (null === $this->schemaFactory) {
160
            throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.');
161
        }
162
163
        $subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext);
164
165
        return ['$ref' => $subSchema['$ref']];
166
    }
167
168
    /**
169
     * @param array<string, mixed> $jsonSchema
170
     *
171
     * @return array<string, mixed>
172
     */
173
    private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type, array $serializerContext): array
174
    {
175
        if (\array_key_exists(self::CONTEXT_SERIALIZATION_FORMAT_OPENAPI_PRE_V3_0, $serializerContext)) {
176
            return $jsonSchema;
177
        }
178
179
        if (!$type->isNullable()) {
180
            return $jsonSchema;
181
        }
182
183
        return [
184
            'oneOf' => [
185
                ['type' => 'null'],
186
                $jsonSchema,
187
            ],
188
        ];
189
    }
190
}
191