Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
93.99% |
344 / 366 |
|
53.33% |
8 / 15 |
CRAP | |
0.00% |
0 / 1 |
| OpenApiGenerator | |
93.99% |
344 / 366 |
|
53.33% |
8 / 15 |
78.29 | |
0.00% |
0 / 1 |
| __construct | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| createDefaultSpec | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| create | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
5 | |||
| createExamplesForInput | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
4.01 | |||
| createSchemaForInput | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
5 | |||
| findUploads | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
5.00 | |||
| doSchemaForInput | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
| doSchemaForOutput | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| createSchemaForOutput | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
| createExamplesForParameter | |
81.25% |
26 / 32 |
|
0.00% |
0 / 1 |
9.53 | |||
| createSchemaForParameter | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
8 | |||
| generateParameter | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| getDisplayValue | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
8.60 | |||
| supportsMultipart | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| addAction | |
95.27% |
141 / 148 |
|
0.00% |
0 / 1 |
17 | |||
| 1 | <?php |
| 2 | namespace Apie\RestApi\OpenApi; |
| 3 | |
| 4 | use Apie\Common\ContextBuilders\Exceptions\WrongTokenException; |
| 5 | use Apie\Common\Enums\UrlPrefix; |
| 6 | use Apie\Common\Interfaces\RestApiRouteDefinition; |
| 7 | use Apie\Common\Interfaces\RouteDefinitionProviderInterface; |
| 8 | use Apie\Core\Actions\ActionResponseStatus; |
| 9 | use Apie\Core\Attributes\AllowMultipart; |
| 10 | use Apie\Core\Attributes\ExampleValue; |
| 11 | use Apie\Core\BoundedContext\BoundedContext; |
| 12 | use Apie\Core\BoundedContext\BoundedContextId; |
| 13 | use Apie\Core\ContextBuilders\ContextBuilderFactory; |
| 14 | use Apie\Core\ContextConstants; |
| 15 | use Apie\Core\Dto\ListOf; |
| 16 | use Apie\Core\Enums\RequestMethod; |
| 17 | use Apie\Core\Identifiers\SnakeCaseSlug; |
| 18 | use Apie\Core\Utils\ConverterUtils; |
| 19 | use Apie\Core\ValueObjects\NonEmptyString; |
| 20 | use Apie\RestApi\Events\OpenApiOperationAddedEvent; |
| 21 | use Apie\RestApi\Events\OpenApiSchemaGeneratedEvent; |
| 22 | use Apie\SchemaGenerator\Builders\ComponentsBuilder; |
| 23 | use Apie\SchemaGenerator\ComponentsBuilderFactory; |
| 24 | use Apie\Serializer\Exceptions\NotAcceptedException; |
| 25 | use Apie\Serializer\Exceptions\ValidationException; |
| 26 | use Apie\Serializer\Serializer; |
| 27 | use Apie\TypeConverter\ReflectionTypeFactory; |
| 28 | use cebe\openapi\Reader; |
| 29 | use cebe\openapi\ReferenceContext; |
| 30 | use cebe\openapi\spec\Example; |
| 31 | use cebe\openapi\spec\MediaType; |
| 32 | use cebe\openapi\spec\OpenApi; |
| 33 | use cebe\openapi\spec\Operation; |
| 34 | use cebe\openapi\spec\Parameter; |
| 35 | use cebe\openapi\spec\PathItem; |
| 36 | use cebe\openapi\spec\Paths; |
| 37 | use cebe\openapi\spec\Reference; |
| 38 | use cebe\openapi\spec\RequestBody; |
| 39 | use cebe\openapi\spec\Response; |
| 40 | use cebe\openapi\spec\Schema; |
| 41 | use cebe\openapi\spec\Server; |
| 42 | use Psr\EventDispatcher\EventDispatcherInterface; |
| 43 | use ReflectionClass; |
| 44 | use ReflectionMethod; |
| 45 | use ReflectionNamedType; |
| 46 | use ReflectionType; |
| 47 | use Throwable; |
| 48 | |
| 49 | class OpenApiGenerator |
| 50 | { |
| 51 | /** |
| 52 | * Serialized string of OpenAPI so we always get a deep clone. |
| 53 | */ |
| 54 | private string $baseSpec; |
| 55 | public function __construct( |
| 56 | private ContextBuilderFactory $contextBuilder, |
| 57 | private ComponentsBuilderFactory $componentsFactory, |
| 58 | private RouteDefinitionProviderInterface $routeDefinitionProvider, |
| 59 | private Serializer $serializer, |
| 60 | private EventDispatcherInterface $dispatcher, |
| 61 | private string $baseUrl = '', |
| 62 | ?OpenApi $baseSpec = null |
| 63 | ) { |
| 64 | $baseSpec ??= $this->createDefaultSpec(); |
| 65 | if (!$baseSpec->paths) { |
| 66 | $baseSpec->paths = new Paths([]); |
| 67 | } |
| 68 | $this->baseSpec = serialize($baseSpec); |
| 69 | } |
| 70 | |
| 71 | private function createDefaultSpec(): OpenApi |
| 72 | { |
| 73 | return Reader::readFromYamlFile( |
| 74 | __DIR__ . '/../../resources/openapi.yaml', |
| 75 | OpenApi::class, |
| 76 | ReferenceContext::RESOLVE_MODE_INLINE |
| 77 | ); |
| 78 | } |
| 79 | |
| 80 | public function create(BoundedContext $boundedContext): OpenApi |
| 81 | { |
| 82 | $spec = unserialize($this->baseSpec); |
| 83 | $urlPrefix = $this->baseUrl . '/' . $boundedContext->getId(); |
| 84 | $spec->servers = [new Server(['url' => $urlPrefix]), new Server(['url' => 'http://localhost/' . $urlPrefix])]; |
| 85 | $componentsBuilder = $this->componentsFactory->createComponentsBuilder($spec->components); |
| 86 | $context = $this->contextBuilder->createGeneralContext( |
| 87 | [ |
| 88 | OpenApiGenerator::class => $this, |
| 89 | ContextConstants::REST_API => true, |
| 90 | Serializer::class => $this->serializer, |
| 91 | BoundedContextId::class => $boundedContext->getId(), |
| 92 | BoundedContext::class => $boundedContext, |
| 93 | ] |
| 94 | ); |
| 95 | foreach ($this->routeDefinitionProvider->getActionsForBoundedContext($boundedContext, $context) as $routeDefinition) { |
| 96 | if ($routeDefinition instanceof RestApiRouteDefinition) { |
| 97 | if (!in_array(UrlPrefix::API, $routeDefinition->getUrlPrefixes()->toArray())) { |
| 98 | continue; |
| 99 | } |
| 100 | $path = $routeDefinition->getUrl()->toNative(); |
| 101 | if ($spec->paths->hasPath($path)) { |
| 102 | $pathItem = $spec->paths->getPath($path); |
| 103 | } else { |
| 104 | $pathItem = new PathItem([]); |
| 105 | $spec->paths->addPath($path, $pathItem); |
| 106 | } |
| 107 | $this->addAction($pathItem, $componentsBuilder, $routeDefinition); |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | $spec->components = $componentsBuilder->getComponents(); |
| 112 | $this->dispatcher->dispatch( |
| 113 | new OpenApiSchemaGeneratedEvent( |
| 114 | $spec, |
| 115 | $boundedContext |
| 116 | ) |
| 117 | ); |
| 118 | return $spec; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * @return array<string, Example> |
| 123 | */ |
| 124 | private function createExamplesForInput(ComponentsBuilder $componentsBuilder, RestApiRouteDefinition $routeDefinition): array |
| 125 | { |
| 126 | $input = $routeDefinition->getInputType(); |
| 127 | if ($input instanceof ReflectionClass || $input instanceof ReflectionMethod) { |
| 128 | $examples = []; |
| 129 | foreach ($input->getAttributes(ExampleValue::class) as $attribute) { |
| 130 | $exampleValue = $attribute->newInstance(); |
| 131 | $id = SnakeCaseSlug::fromText($exampleValue->name)->toNative(); |
| 132 | $examples[$id] = new Example([ |
| 133 | 'summary' => $exampleValue->name, |
| 134 | 'value' => $exampleValue->toExample(), |
| 135 | ]); |
| 136 | } |
| 137 | return $examples; |
| 138 | } |
| 139 | // TODO: ReflectionType? |
| 140 | return []; |
| 141 | } |
| 142 | |
| 143 | private function createSchemaForInput(ComponentsBuilder $componentsBuilder, RestApiRouteDefinition $routeDefinition, bool $forUpload = false): Schema|Reference |
| 144 | { |
| 145 | $input = $routeDefinition->getInputType(); |
| 146 | |
| 147 | $result = $this->doSchemaForInput($input, $componentsBuilder, $routeDefinition->getMethod()); |
| 148 | if ($forUpload && $routeDefinition->getMethod() !== RequestMethod::GET) { |
| 149 | $uploads = []; |
| 150 | $visited = []; |
| 151 | $state = []; |
| 152 | $this->findUploads($result, $componentsBuilder, $state, $uploads, $visited); |
| 153 | $required = ['form']; |
| 154 | foreach ($uploads as $uploadName => $upload) { |
| 155 | if (!$upload->nullable) { |
| 156 | $required[] = $uploadName; |
| 157 | } |
| 158 | } |
| 159 | return new Schema([ |
| 160 | 'type' => 'object', |
| 161 | 'properties' => [ |
| 162 | 'form' => $result, |
| 163 | '_csrf' => new Schema(['type' => 'string']), |
| 164 | // TODO _internal |
| 165 | ...$uploads |
| 166 | ], |
| 167 | 'required' => $required, |
| 168 | ]); |
| 169 | } |
| 170 | return $result; |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * @param array<int, string> $state |
| 175 | * @param array <int|string, mixed> $uploads |
| 176 | * @param array <string, true> $visited |
| 177 | */ |
| 178 | private function findUploads( |
| 179 | Schema|Reference $schema, |
| 180 | ComponentsBuilder $componentsBuilder, |
| 181 | array $state, |
| 182 | array& $uploads, |
| 183 | array& $visited |
| 184 | ): void { |
| 185 | if ($schema instanceof Reference) { |
| 186 | if (isset($visited[$schema->getReference()])) { |
| 187 | return; |
| 188 | } |
| 189 | $visited[$schema->getReference()] = true; |
| 190 | $schema = $componentsBuilder->getSchemaForReference($schema); |
| 191 | } |
| 192 | if ($schema->__isset('x-upload')) { |
| 193 | $uploads[implode('.', $state)] = new Schema([ |
| 194 | 'type' => 'string', |
| 195 | 'format' => 'binary', |
| 196 | 'nullable' => $schema->nullable, |
| 197 | ]); |
| 198 | } |
| 199 | foreach ($schema->properties ?? [] as $propertyName => $propertySchema) { |
| 200 | $this->findUploads( |
| 201 | $propertySchema, |
| 202 | $componentsBuilder, |
| 203 | [...$state, $propertyName], |
| 204 | $uploads, |
| 205 | $visited |
| 206 | ); |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * @param ReflectionClass<object>|ReflectionMethod|ReflectionType $input |
| 212 | */ |
| 213 | private function doSchemaForInput(ReflectionClass|ReflectionMethod|ReflectionType $input, ComponentsBuilder $componentsBuilder, RequestMethod $method = RequestMethod::GET): Schema|Reference |
| 214 | { |
| 215 | if ($input instanceof ReflectionClass) { |
| 216 | if ($method === RequestMethod::PATCH) { |
| 217 | return $componentsBuilder->addModificationSchemaFor($input->name); |
| 218 | } |
| 219 | return $componentsBuilder->addCreationSchemaFor($input->name); |
| 220 | } |
| 221 | if ($input instanceof ReflectionMethod) { |
| 222 | $info = $componentsBuilder->getSchemaForMethod($input); |
| 223 | return new Schema( |
| 224 | [ |
| 225 | 'type' => 'object', |
| 226 | 'properties' => $info->schemas, |
| 227 | ] + ($info->required ? ['required' => $info->required] : []) |
| 228 | ); |
| 229 | } |
| 230 | return $componentsBuilder->getSchemaForType($input, nullable: $input->allowsNull()); |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * @param ReflectionClass<object>|ReflectionMethod|ReflectionType $output |
| 235 | */ |
| 236 | private function doSchemaForOutput(ReflectionClass|ReflectionMethod|ReflectionType $output, ComponentsBuilder $componentsBuilder): Schema|Reference |
| 237 | { |
| 238 | if ($output instanceof ReflectionClass) { |
| 239 | return $componentsBuilder->addDisplaySchemaFor($output->name); |
| 240 | } |
| 241 | if ($output instanceof ReflectionMethod) { |
| 242 | $output = $output->getReturnType(); |
| 243 | } |
| 244 | return $componentsBuilder->getSchemaForType($output, false, true, $output ? $output->allowsNull() : true); |
| 245 | } |
| 246 | |
| 247 | private function createSchemaForOutput(ComponentsBuilder $componentsBuilder, RestApiRouteDefinition $routeDefinition): Schema|Reference |
| 248 | { |
| 249 | $input = $routeDefinition->getOutputType(); |
| 250 | if ($input instanceof ListOf) { |
| 251 | return new Schema([ |
| 252 | 'type' => 'object', |
| 253 | 'required' => [ |
| 254 | 'filteredCount', |
| 255 | 'totalCount', |
| 256 | 'first', |
| 257 | 'last', |
| 258 | 'list', |
| 259 | ], |
| 260 | 'properties' => [ |
| 261 | 'totalCount' => ['type' => 'integer', 'minimum' => 0], |
| 262 | 'filteredCount' => ['type' => 'integer', 'minimum' => 0], |
| 263 | 'first' => ['type' => 'string', 'format' => 'uri'], |
| 264 | 'last' => ['type' => 'string', 'format' => 'uri'], |
| 265 | 'prev' => ['type' => 'string', 'format' => 'uri'], |
| 266 | 'next' => ['type' => 'string', 'format' => 'uri'], |
| 267 | 'list' => [ |
| 268 | 'type' => 'array', |
| 269 | 'items' => $this->doSchemaForOutput($input->type, $componentsBuilder), |
| 270 | ] |
| 271 | ] |
| 272 | ]); |
| 273 | } |
| 274 | return $this->doSchemaForOutput($input, $componentsBuilder); |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * @return array<string, Example> |
| 279 | */ |
| 280 | private function createExamplesForParameter( |
| 281 | RestApiRouteDefinition $routeDefinition, |
| 282 | string $placeholderName |
| 283 | ): array { |
| 284 | $input = $routeDefinition->getInputType(); |
| 285 | $examples = []; |
| 286 | if ($input instanceof ReflectionMethod) { |
| 287 | foreach ($input->getParameters() as $parameter) { |
| 288 | if ($parameter->name === $placeholderName) { |
| 289 | foreach ($parameter->getAttributes(ExampleValue::class) as $attribute) { |
| 290 | $exampleValue = $attribute->newInstance(); |
| 291 | $id = SnakeCaseSlug::fromText($exampleValue->name)->toNative(); |
| 292 | $examples[$id] = new Example([ |
| 293 | 'summary' => $exampleValue->name, |
| 294 | 'value' => $exampleValue->toExample() |
| 295 | ]); |
| 296 | } |
| 297 | break; |
| 298 | } |
| 299 | } |
| 300 | } |
| 301 | if ($input instanceof ReflectionClass) { |
| 302 | $methodNames = [ |
| 303 | ['get' . ucfirst($placeholderName), 'hasMethod', 'getMethod'], |
| 304 | ['has' . ucfirst($placeholderName), 'hasMethod', 'getMethod'], |
| 305 | ['is' . ucfirst($placeholderName), 'hasMethod', 'getMethod'], |
| 306 | [$placeholderName, 'hasProperty', 'getProperty'], |
| 307 | ]; |
| 308 | |
| 309 | foreach ($methodNames as $optionToCheck) { |
| 310 | list($propertyName, $has, $get) = $optionToCheck; |
| 311 | if ($input->$has($propertyName)) { |
| 312 | $option = $input->$get($propertyName); |
| 313 | foreach ($option->getAttributes(ExampleValue::class) as $attribute) { |
| 314 | $exampleValue = $attribute->newInstance(); |
| 315 | $id = SnakeCaseSlug::fromText($exampleValue->name)->toNative(); |
| 316 | $examples[$id] = new Example([ |
| 317 | 'summary' => $exampleValue->name, |
| 318 | 'value' => $exampleValue->toExample() |
| 319 | ]); |
| 320 | } |
| 321 | } |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | return $examples; |
| 326 | } |
| 327 | |
| 328 | private function createSchemaForParameter( |
| 329 | ComponentsBuilder $componentsBuilder, |
| 330 | RestApiRouteDefinition $routeDefinition, |
| 331 | string $placeholderName |
| 332 | ): Schema|Reference { |
| 333 | $input = $routeDefinition->getInputType(); |
| 334 | $found = false; |
| 335 | if ($input instanceof ReflectionMethod) { |
| 336 | foreach ($input->getParameters() as $parameter) { |
| 337 | if ($parameter->name === $placeholderName) { |
| 338 | $found = true; |
| 339 | $input = $parameter->getType() ?? ReflectionTypeFactory::createReflectionType('string'); |
| 340 | break; |
| 341 | } |
| 342 | } |
| 343 | } |
| 344 | if ($input instanceof ReflectionClass) { |
| 345 | $methodNames = [ |
| 346 | ['get' . ucfirst($placeholderName), 'hasMethod', 'getMethod', 'getReturnType'], |
| 347 | ['has' . ucfirst($placeholderName), 'hasMethod', 'getMethod', 'getReturnType'], |
| 348 | ['is' . ucfirst($placeholderName), 'hasMethod', 'getMethod', 'getReturnType'], |
| 349 | [$placeholderName, 'hasProperty', 'getProperty', 'getType'], |
| 350 | ]; |
| 351 | |
| 352 | foreach ($methodNames as $optionToCheck) { |
| 353 | list($propertyName, $has, $get, $type) = $optionToCheck; |
| 354 | if ($input->$has($propertyName)) { |
| 355 | $input = $input->$get($propertyName)->$type(); |
| 356 | $found = true; |
| 357 | break; |
| 358 | } |
| 359 | } |
| 360 | } |
| 361 | if (!$found) { |
| 362 | $input = ReflectionTypeFactory::createReflectionType(NonEmptyString::class); |
| 363 | } |
| 364 | return $this->doSchemaForInput($input, $componentsBuilder); |
| 365 | } |
| 366 | |
| 367 | private function generateParameter( |
| 368 | ComponentsBuilder $componentsBuilder, |
| 369 | RestApiRouteDefinition $routeDefinition, |
| 370 | string $placeholderName |
| 371 | ): Parameter { |
| 372 | $examples = $this->createExamplesForParameter($routeDefinition, $placeholderName); |
| 373 | return new Parameter(array_filter([ |
| 374 | 'in' => 'path', |
| 375 | 'name' => $placeholderName, |
| 376 | 'required' => true, |
| 377 | 'description' => $placeholderName . ' of instance of ' . $this->getDisplayValue($routeDefinition->getInputType(), $placeholderName), |
| 378 | 'schema' => $this->createSchemaForParameter($componentsBuilder, $routeDefinition, $placeholderName), |
| 379 | 'examples' => $examples, |
| 380 | ])); |
| 381 | } |
| 382 | |
| 383 | /** |
| 384 | * @param ReflectionClass<object>|ReflectionMethod|ReflectionType $type |
| 385 | */ |
| 386 | private function getDisplayValue(ReflectionClass|ReflectionMethod|ReflectionType $type, string $placeholderName): string |
| 387 | { |
| 388 | if ($type instanceof ReflectionNamedType) { |
| 389 | $name = $type->getName(); |
| 390 | if (class_exists($name)) { |
| 391 | return (new ReflectionClass($name))->getShortName(); |
| 392 | } |
| 393 | return $name; |
| 394 | } |
| 395 | if ($type instanceof ReflectionType) { |
| 396 | return (string) $type; |
| 397 | } |
| 398 | if ($type instanceof ReflectionClass) { |
| 399 | return $type->getShortName(); |
| 400 | } |
| 401 | if ($placeholderName === 'id') { |
| 402 | return $type->getDeclaringClass()->getShortName(); |
| 403 | } |
| 404 | return $type->name; |
| 405 | } |
| 406 | |
| 407 | private function supportsMultipart(RestApiRouteDefinition $routeDefinition): bool |
| 408 | { |
| 409 | $input = ConverterUtils::toReflectionClass($routeDefinition->getInputType()); |
| 410 | if ($input === null) { |
| 411 | return false; |
| 412 | } |
| 413 | if (!in_array($routeDefinition->getMethod(), [RequestMethod::POST, RequestMethod::PUT, RequestMethod::PATCH])) { |
| 414 | return false; |
| 415 | } |
| 416 | return !empty($input->getAttributes(AllowMultipart::class)); |
| 417 | } |
| 418 | |
| 419 | private function addAction(PathItem $pathItem, ComponentsBuilder $componentsBuilder, RestApiRouteDefinition $routeDefinition): void |
| 420 | { |
| 421 | $method = $routeDefinition->getMethod(); |
| 422 | if (!in_array($method, RequestMethod::allowedInOpenApi())) { |
| 423 | return; |
| 424 | } |
| 425 | $inputSchema = $this->createSchemaForInput($componentsBuilder, $routeDefinition); |
| 426 | $examples = $this->createExamplesForInput($componentsBuilder, $routeDefinition); |
| 427 | $outputSchema = $this->createSchemaForOutput($componentsBuilder, $routeDefinition); |
| 428 | $operation = new Operation([ |
| 429 | 'tags' => $routeDefinition->getTags()->toArray(), |
| 430 | 'description' => $routeDefinition->getDescription(), |
| 431 | 'operationId' => $routeDefinition->getOperationId(), |
| 432 | ]); |
| 433 | $parameters = []; |
| 434 | $parameters[] = new Parameter([ |
| 435 | 'name' => 'fields', |
| 436 | 'in' => 'query', |
| 437 | 'explode' => false, |
| 438 | 'schema' => new Schema([ |
| 439 | 'type' => 'array', |
| 440 | 'items' => new Schema([ |
| 441 | 'type' => 'string', |
| 442 | ]) |
| 443 | ]) |
| 444 | ]); |
| 445 | $parameters[] = new Parameter([ |
| 446 | 'name' => 'relations', |
| 447 | 'in' => 'query', |
| 448 | 'explode' => false, |
| 449 | 'schema' => new Schema([ |
| 450 | 'type' => 'array', |
| 451 | 'items' => new Schema([ |
| 452 | 'type' => 'string', |
| 453 | ]) |
| 454 | ]) |
| 455 | ]); |
| 456 | $placeholders = $routeDefinition->getUrl()->getPlaceholders(); |
| 457 | |
| 458 | foreach ($placeholders as $placeholderName) { |
| 459 | $parameters[] = $this->generateParameter($componentsBuilder, $routeDefinition, $placeholderName); |
| 460 | } |
| 461 | $operation->parameters = $parameters; |
| 462 | |
| 463 | if ($method !== RequestMethod::GET && $method !== RequestMethod::DELETE) { |
| 464 | $content = [ |
| 465 | 'application/json' => new MediaType(array_filter([ |
| 466 | 'schema' => $inputSchema, |
| 467 | 'examples' => $examples, |
| 468 | ])), |
| 469 | ]; |
| 470 | if ($this->supportsMultipart($routeDefinition)) { |
| 471 | $uploadSchema = $componentsBuilder->runInContentType( |
| 472 | 'multipart/form-data', |
| 473 | function () use ($componentsBuilder, $routeDefinition) { |
| 474 | return $this->createSchemaForInput($componentsBuilder, $routeDefinition, true); |
| 475 | } |
| 476 | ); |
| 477 | $content['multipart/form-data'] = new MediaType(array_filter([ |
| 478 | 'schema' => $uploadSchema, |
| 479 | 'examples' => $examples, |
| 480 | ])); |
| 481 | $parameters = $operation->parameters; |
| 482 | $parameters[] = new Parameter([ |
| 483 | 'name' => 'x-no-crsf', |
| 484 | 'in' => 'header', |
| 485 | 'description' => 'Disable csrf', |
| 486 | 'schema' => [ |
| 487 | 'type' => 'string', |
| 488 | 'enum' => ['1'] |
| 489 | ], |
| 490 | ]); |
| 491 | $operation->parameters = $parameters; |
| 492 | } |
| 493 | $operation->requestBody = new RequestBody([ |
| 494 | 'content' => $content |
| 495 | ]); |
| 496 | } |
| 497 | $responses = [ |
| 498 | ]; |
| 499 | foreach ($routeDefinition->getPossibleActionResponseStatuses() as $responseStatus) { |
| 500 | switch ($responseStatus) { |
| 501 | case ActionResponseStatus::CREATED: |
| 502 | $responses[201] = new Response([ |
| 503 | 'description' => 'Resource was created', |
| 504 | 'content' => [ |
| 505 | 'application/json' => new MediaType(['schema' => $outputSchema]) |
| 506 | ] |
| 507 | ]); |
| 508 | break; |
| 509 | case ActionResponseStatus::SUCCESS: |
| 510 | $responses[200] = new Response([ |
| 511 | 'description' => 'OK', |
| 512 | 'content' => [ |
| 513 | 'application/json' => new MediaType(['schema' => $outputSchema]) |
| 514 | ] |
| 515 | ]); |
| 516 | break; |
| 517 | case ActionResponseStatus::CLIENT_ERROR: |
| 518 | foreach ([400, 405, 406] as $statusCode) { |
| 519 | $responses[$statusCode] = new Response([ |
| 520 | 'description' => 'Invalid request', |
| 521 | 'content' => [ |
| 522 | 'application/json' => new MediaType(['schema' => $componentsBuilder->addDisplaySchemaFor(NotAcceptedException::class)]), |
| 523 | ] |
| 524 | ]); |
| 525 | } |
| 526 | $responses[422] = new Response([ |
| 527 | 'description' => 'A validation error occurred', |
| 528 | 'content' => [ |
| 529 | 'application/json' => new MediaType(['schema' => $componentsBuilder->addDisplaySchemaFor(ValidationException::class)]), |
| 530 | ] |
| 531 | ]); |
| 532 | break; |
| 533 | case ActionResponseStatus::AUTHORIZATION_ERROR: |
| 534 | foreach ([401 => 'Requires authorization', 403 => 'Access denied'] as $statusCode => $description) { |
| 535 | $responses[$statusCode] = new Response([ |
| 536 | 'description' => $description, |
| 537 | 'content' => [ |
| 538 | 'application/json' => new MediaType(['schema' => $componentsBuilder->addDisplaySchemaFor(WrongTokenException::class)]), |
| 539 | ] |
| 540 | ]); |
| 541 | } |
| 542 | break; |
| 543 | case ActionResponseStatus::DELETED: |
| 544 | $responses[204] = new Response(['description' => 'Resource was deleted']); |
| 545 | break; |
| 546 | case ActionResponseStatus::NOT_FOUND: |
| 547 | $responses[404] = new Response([ |
| 548 | 'description' => 'Resource not found', |
| 549 | 'content' => [ |
| 550 | 'application/json' => new MediaType(['schema' => $componentsBuilder->addDisplaySchemaFor(Throwable::class)]), |
| 551 | ] |
| 552 | ]); |
| 553 | break; |
| 554 | case ActionResponseStatus::PERISTENCE_ERROR: |
| 555 | $responses[409] = new Response([ |
| 556 | 'description' => 'Resource not found', |
| 557 | 'content' => [ |
| 558 | 'application/json' => new MediaType(['schema' => $componentsBuilder->addDisplaySchemaFor(Throwable::class)]), |
| 559 | ] |
| 560 | ]); |
| 561 | break; |
| 562 | default: |
| 563 | $responses[500] = new Response([ |
| 564 | 'description' => 'Unknown error occurred', |
| 565 | 'content' => [ |
| 566 | 'application/json' => new MediaType(['schema' => $componentsBuilder->addDisplaySchemaFor(Throwable::class)]), |
| 567 | ] |
| 568 | ]); |
| 569 | } |
| 570 | } |
| 571 | $operation->responses = $responses; |
| 572 | $prop = strtolower($method->value); |
| 573 | // @phpstan-ignore-next-line |
| 574 | $pathItem->{$prop} = $operation; |
| 575 | $this->dispatcher->dispatch( |
| 576 | new OpenApiOperationAddedEvent( |
| 577 | $componentsBuilder, |
| 578 | $operation, |
| 579 | $routeDefinition |
| 580 | ) |
| 581 | ); |
| 582 | } |
| 583 | } |