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