Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
85.71% |
120 / 140 |
|
43.75% |
7 / 16 |
CRAP | |
0.00% |
0 / 1 |
ComponentsBuilder | |
85.71% |
120 / 140 |
|
43.75% |
7 / 16 |
85.70 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
createWithExistingComponents | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
runInContentType | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setContentType | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getContentType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMixedReference | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getSchemaForReference | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getComponents | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
checkDuplicate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setSchema | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
getSchemaForMethod | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
getSchemaForType | |
76.00% |
19 / 25 |
|
0.00% |
0 / 1 |
13.99 | |||
addDisplaySchemaFor | |
90.00% |
27 / 30 |
|
0.00% |
0 / 1 |
17.29 | |||
addCreationSchemaFor | |
93.10% |
27 / 29 |
|
0.00% |
0 / 1 |
16.08 | |||
addModificationSchemaFor | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
5.03 | |||
addDescriptionOfObject | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 |
1 | <?php |
2 | namespace Apie\SchemaGenerator\Builders; |
3 | |
4 | use Apie\Core\Attributes\Context; |
5 | use Apie\Core\Attributes\Description; |
6 | use Apie\Core\Exceptions\DuplicateIdentifierException; |
7 | use Apie\Core\ValueObjects\Utils; |
8 | use Apie\SchemaGenerator\Exceptions\ICanNotExtractASchemaFromClassException; |
9 | use Apie\SchemaGenerator\Interfaces\ModifySchemaProvider; |
10 | use Apie\SchemaGenerator\Interfaces\SchemaProvider; |
11 | use Apie\SchemaGenerator\Other\MethodSchemaInfo; |
12 | use cebe\openapi\ReferenceContext; |
13 | use cebe\openapi\spec\Components; |
14 | use cebe\openapi\spec\OpenApi; |
15 | use cebe\openapi\spec\Reference; |
16 | use cebe\openapi\spec\Schema; |
17 | use ReflectionClass; |
18 | use ReflectionIntersectionType; |
19 | use ReflectionMethod; |
20 | use ReflectionNamedType; |
21 | use ReflectionType; |
22 | use ReflectionUnionType; |
23 | |
24 | class ComponentsBuilder |
25 | { |
26 | /** |
27 | * @var array<int, SchemaProvider<object>> |
28 | */ |
29 | private array $schemaProviders; |
30 | |
31 | private Components $components; |
32 | |
33 | private ?string $contentType = null; |
34 | |
35 | /** |
36 | * @param SchemaProvider<object> $schemaProviders |
37 | */ |
38 | public function __construct(SchemaProvider... $schemaProviders) |
39 | { |
40 | $this->schemaProviders = $schemaProviders; |
41 | $this->components = new Components([]); |
42 | } |
43 | |
44 | /** |
45 | * @param SchemaProvider<object> $schemaProviders |
46 | */ |
47 | public static function createWithExistingComponents(Components $components, SchemaProvider... $schemaProviders): self |
48 | { |
49 | $res = new self(...$schemaProviders); |
50 | $res->components = $components; |
51 | return $res; |
52 | } |
53 | |
54 | public function runInContentType(?string $contentType, callable $callback): mixed |
55 | { |
56 | $previousContentType = $this->contentType; |
57 | try { |
58 | $this->contentType = $contentType; |
59 | return $callback(); |
60 | } finally { |
61 | $this->contentType = $previousContentType; |
62 | } |
63 | } |
64 | |
65 | public function setContentType(?string $contentType): self |
66 | { |
67 | $this->contentType = $contentType; |
68 | return $this; |
69 | } |
70 | |
71 | public function getContentType(): ?string |
72 | { |
73 | return $this->contentType; |
74 | } |
75 | |
76 | public function getMixedReference(): Reference |
77 | { |
78 | if (!isset($this->components->schemas['mixed'])) { |
79 | $this->setSchema('mixed', new Schema(['nullable' => true])); |
80 | } |
81 | return new Reference(['$ref' => '#/components/schemas/mixed']); |
82 | } |
83 | |
84 | public function getSchemaForReference(Reference $reference): ?Schema |
85 | { |
86 | $result = $reference->resolve( |
87 | new ReferenceContext( |
88 | new OpenApi(['components' => $this->components]), |
89 | 'file:///#/components' |
90 | ) |
91 | ); |
92 | assert($result === null || $result instanceof Schema); |
93 | return $result; |
94 | } |
95 | |
96 | public function getComponents(): Components |
97 | { |
98 | $schemas = $this->components->schemas; |
99 | ksort($schemas); |
100 | $this->components->schemas = $schemas; |
101 | return $this->components; |
102 | } |
103 | |
104 | private function checkDuplicate(string $identifier, Schema $original, Schema $newObject): void |
105 | { |
106 | throw new DuplicateIdentifierException($identifier, json_encode($original->getSerializableData()), json_encode($newObject->getSerializableData())); |
107 | } |
108 | |
109 | public function setSchema(string $identifier, Schema $schema): self |
110 | { |
111 | if (isset($this->components->schemas[$identifier])) { |
112 | $this->checkDuplicate($identifier, $this->components->schemas[$identifier], $schema); |
113 | } |
114 | $schemas = $this->components->schemas; |
115 | $schemas[$identifier] = $schema; |
116 | |
117 | $this->components->schemas = $schemas; |
118 | return $this; |
119 | } |
120 | |
121 | public function getSchemaForMethod(ReflectionMethod $method): MethodSchemaInfo |
122 | { |
123 | $returnValue = new MethodSchemaInfo(); |
124 | foreach ($method->getParameters() as $parameter) { |
125 | if (count($parameter->getAttributes(Context::class)) > 0) { |
126 | continue; |
127 | } |
128 | if (!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull()) { |
129 | $returnValue->required[] = $parameter->name; |
130 | } |
131 | $type = $parameter->getType(); |
132 | $returnValue->schemas[$parameter->name] = $this->getSchemaForType($type, $parameter->isVariadic(), nullable: $type?->allowsNull() ?? false); |
133 | } |
134 | return $returnValue; |
135 | } |
136 | |
137 | public function getSchemaForType(ReflectionType|null $type, bool $array = false, bool $display = false, bool $nullable = false): Schema|Reference |
138 | { |
139 | $map = $nullable ? ['nullable' => true] : []; |
140 | $methodName = $display ? 'addDisplaySchemaFor' : 'addCreationSchemaFor'; |
141 | $result = $this->getMixedReference(); |
142 | if ($type instanceof ReflectionIntersectionType) { |
143 | $allOfs = []; |
144 | foreach ($type->getTypes() as $allOfType) { |
145 | $allOfs[] = $this->$methodName((string) $allOfType, nullable: $nullable || $allOfType->allowsNull()); |
146 | } |
147 | $result = new Schema([ |
148 | 'allOf' => $allOfs, |
149 | ] + $map); |
150 | } elseif ($type instanceof ReflectionUnionType) { |
151 | $oneOfs = []; |
152 | foreach ($type->getTypes() as $oneOfType) { |
153 | $oneOfs[] = $this->$methodName((string) $oneOfType, nullable: $nullable || $oneOfType->allowsNull()); |
154 | } |
155 | $result = new Schema([ |
156 | 'oneOf' => $oneOfs, |
157 | ] + $map); |
158 | } elseif ($type instanceof ReflectionNamedType) { |
159 | $result = $this->$methodName($type->getName(), nullable: $nullable || $type->allowsNull()); |
160 | } |
161 | if ($array) { |
162 | return new Schema([ |
163 | 'type' => 'array', |
164 | 'items' => $result, |
165 | ] + $map); |
166 | } |
167 | return $result; |
168 | } |
169 | |
170 | public function addDisplaySchemaFor(string $class, ?string $discriminatorColumn = null, bool $nullable = false): Reference|Schema |
171 | { |
172 | $map = $nullable ? ['nullable' => true] : []; |
173 | switch ($class) { |
174 | case 'mixed': |
175 | return $this->getMixedReference(); |
176 | case 'string': |
177 | return new Schema(['type' => $class] + $map); |
178 | case 'array': |
179 | return new Schema(['type' => 'object', 'additionalProperties' => $this->getMixedReference()] + $map); |
180 | case 'bool': |
181 | return new Schema(['type' => 'boolean'] + $map); |
182 | case 'true': |
183 | return new Schema(['type' => 'boolean', 'enum' => [true]]); |
184 | case 'false': |
185 | return new Schema(['type' => 'boolean', 'enum' => [false]]); |
186 | case 'int': |
187 | return new Schema(['type' => 'integer'] + $map); |
188 | case 'float': |
189 | case 'double': |
190 | return new Schema(['type' => 'number'] + $map); |
191 | case 'void': |
192 | case 'null': |
193 | return new Schema(['nullable' => true, 'default' => null]); |
194 | } |
195 | $refl = new ReflectionClass($class); |
196 | $identifier = Utils::getDisplayNameForValueObject($refl) . ($nullable ? '-nullable' : '') . '-get'; |
197 | if (isset($this->components->schemas[$identifier])) { |
198 | return new Reference(['$ref' => '#/components/schemas/' . $identifier]); |
199 | } |
200 | |
201 | foreach ($this->schemaProviders as $schemaProvider) { |
202 | if ($schemaProvider->supports($refl)) { |
203 | $this->components = $schemaProvider->addDisplaySchemaFor($this, $identifier, $refl, $nullable); |
204 | return new Reference(['$ref' => '#/components/schemas/' . $identifier]); |
205 | } |
206 | } |
207 | throw new ICanNotExtractASchemaFromClassException($refl->name); |
208 | } |
209 | |
210 | public function addCreationSchemaFor(string $class, ?string $discriminatorColumn = null, bool $nullable = false): Reference|Schema |
211 | { |
212 | $map = $nullable ? ['nullable' => true] : []; |
213 | switch ($class) { |
214 | case 'mixed': |
215 | return $this->getMixedReference(); |
216 | case 'object': |
217 | return new Schema(['type' => 'object', 'additionalProperties' => true] + $map); |
218 | case 'string': |
219 | return new Schema(['type' => $class] + $map); |
220 | case 'array': |
221 | return new Schema(['type' => 'object', 'additionalProperties' => $this->getMixedReference()] + $map); |
222 | case 'bool': |
223 | return new Schema(['type' => 'boolean'] + $map); |
224 | case 'int': |
225 | return new Schema(['type' => 'integer'] + $map); |
226 | case 'float': |
227 | case 'double': |
228 | return new Schema(['type' => 'number'] + $map); |
229 | case 'null': |
230 | return new Schema(['nullable' => true, 'default' => null]); |
231 | } |
232 | $refl = new ReflectionClass($class); |
233 | $identifier = Utils::getDisplayNameForValueObject($refl) . ($nullable ? '-nullable' : '') . '-post'; |
234 | if ($this->contentType) { |
235 | $identifier .= '-' . str_replace('/', '-', $this->contentType); |
236 | } |
237 | if (isset($this->components->schemas[$identifier])) { |
238 | return new Reference(['$ref' => '#/components/schemas/' . $identifier]); |
239 | } |
240 | foreach ($this->schemaProviders as $schemaProvider) { |
241 | if ($schemaProvider->supports($refl)) { |
242 | $this->components = $schemaProvider->addCreationSchemaFor($this, $identifier, $refl, $nullable); |
243 | return new Reference(['$ref' => '#/components/schemas/' . $identifier]); |
244 | } |
245 | } |
246 | throw new ICanNotExtractASchemaFromClassException($refl->name); |
247 | } |
248 | |
249 | public function addModificationSchemaFor(string $class, ?string $discriminatorColumn = null): Reference|Schema |
250 | { |
251 | $refl = new ReflectionClass($class); |
252 | $identifier = Utils::getDisplayNameForValueObject($refl) . '-patch'; |
253 | if (isset($this->components->schemas[$identifier])) { |
254 | return new Reference(['$ref' => '#/components/schemas/' . $identifier]); |
255 | } |
256 | foreach ($this->schemaProviders as $schemaProvider) { |
257 | if ($schemaProvider instanceof ModifySchemaProvider && $schemaProvider->supports($refl)) { |
258 | $this->components = $schemaProvider->addModificationSchemaFor($this, $identifier, $refl); |
259 | return new Reference(['$ref' => '#/components/schemas/' . $identifier]); |
260 | } |
261 | } |
262 | throw new ICanNotExtractASchemaFromClassException($refl->name); |
263 | } |
264 | |
265 | /** |
266 | * @param ReflectionClass<object> $class |
267 | */ |
268 | public static function addDescriptionOfObject(Schema $schema, ReflectionClass $class): void |
269 | { |
270 | if ($schema->description) { |
271 | return; |
272 | } |
273 | foreach ($class->getAttributes(Description::class) as $attribute) { |
274 | $schema->description = $attribute->newInstance()->description; |
275 | } |
276 | } |
277 | } |