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