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 |
81.48 | |
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 |
10.12 | |||
| 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: $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: $oneOfType->allowsNull()); |
| 154 | } |
| 155 | $result = new Schema([ |
| 156 | 'oneOf' => $oneOfs, |
| 157 | ] + $map); |
| 158 | } elseif ($type instanceof ReflectionNamedType) { |
| 159 | $result = $this->$methodName($type->getName(), 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 | } |