vendor/nijens/openapi-bundle/src/Routing/RouteLoader.php line 168

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of the OpenapiBundle package.
  5.  *
  6.  * (c) Niels Nijens <nijens.niels@gmail.com>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Nijens\OpenapiBundle\Routing;
  12. use Nijens\OpenapiBundle\Controller\CatchAllController;
  13. use Nijens\OpenapiBundle\DependencyInjection\Configuration;
  14. use Nijens\OpenapiBundle\Json\JsonPointer;
  15. use Nijens\OpenapiBundle\Json\SchemaLoaderInterface;
  16. use stdClass;
  17. use Symfony\Component\Config\FileLocatorInterface;
  18. use Symfony\Component\Config\Loader\FileLoader;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\Routing\Route;
  21. use Symfony\Component\Routing\RouteCollection;
  22. /**
  23.  * Loads the paths from an OpenAPI specification as routes.
  24.  *
  25.  * @author Niels Nijens <nijens.niels@gmail.com>
  26.  */
  27. class RouteLoader extends FileLoader
  28. {
  29.     /**
  30.      * @var string
  31.      */
  32.     public const TYPE 'openapi';
  33.     /**
  34.      * @var SchemaLoaderInterface
  35.      */
  36.     private $schemaLoader;
  37.     /**
  38.      * @var bool
  39.      */
  40.     private $useOperationIdAsRouteName;
  41.     /**
  42.      * Constructs a new RouteLoader instance.
  43.      */
  44.     public function __construct(
  45.         FileLocatorInterface $locator,
  46.         SchemaLoaderInterface $schemaLoader,
  47.         bool $useOperationIdAsRouteName false
  48.     ) {
  49.         parent::__construct($locator);
  50.         $this->schemaLoader $schemaLoader;
  51.         $this->useOperationIdAsRouteName $useOperationIdAsRouteName;
  52.     }
  53.     /**
  54.      * {@inheritdoc}
  55.      */
  56.     public function supports($resource$type null): bool
  57.     {
  58.         return self::TYPE === $type;
  59.     }
  60.     /**
  61.      * {@inheritdoc}
  62.      */
  63.     public function load($resource$type null): RouteCollection
  64.     {
  65.         $file $this->getLocator()->locate($resourcenulltrue);
  66.         $schema $this->schemaLoader->load($file);
  67.         $jsonPointer = new JsonPointer($schema);
  68.         $routeCollection = new RouteCollection();
  69.         $routeCollection->addResource($this->schemaLoader->getFileResource($file));
  70.         $paths get_object_vars($jsonPointer->get('/paths'));
  71.         foreach ($paths as $path => $pathItem) {
  72.             $this->parsePathItem($jsonPointer$file$routeCollection$path$pathItem);
  73.         }
  74.         $this->addDefaultRoutes($routeCollection$file);
  75.         return $routeCollection;
  76.     }
  77.     /**
  78.      * Parses a path item of the OpenAPI specification for a route.
  79.      */
  80.     private function parsePathItem(
  81.         JsonPointer $jsonPointer,
  82.         string $resource,
  83.         RouteCollection $collection,
  84.         string $path,
  85.         stdClass $pathItem
  86.     ): void {
  87.         $operations get_object_vars($pathItem);
  88.         foreach ($operations as $requestMethod => $operation) {
  89.             if ($this->isValidRequestMethod($requestMethod) === false) {
  90.                 return;
  91.             }
  92.             $this->parseOperation($jsonPointer$resource$collection$path$requestMethod$operation$pathItem);
  93.         }
  94.     }
  95.     /**
  96.      * Parses an operation of the OpenAPI specification for a route.
  97.      */
  98.     private function parseOperation(
  99.         JsonPointer $jsonPointer,
  100.         string $resource,
  101.         RouteCollection $collection,
  102.         string $path,
  103.         string $requestMethod,
  104.         stdClass $operation,
  105.         stdClass $pathItem
  106.     ): void {
  107.         $defaults = [];
  108.         $openapiRouteContext = [
  109.             RouteContext::RESOURCE => $resource,
  110.         ];
  111.         $this->parseOpenapiBundleSpecificationExtension($operation$defaults$openapiRouteContext);
  112.         $this->addRouteContextForValidation(
  113.             $jsonPointer,
  114.             $path,
  115.             $requestMethod,
  116.             $operation,
  117.             $pathItem,
  118.             $openapiRouteContext
  119.         );
  120.         $defaults[RouteContext::REQUEST_ATTRIBUTE] = $openapiRouteContext;
  121.         $route = new Route($path$defaults, []);
  122.         $route->setMethods($requestMethod);
  123.         $routeName null;
  124.         if ($this->useOperationIdAsRouteName && isset($operation->operationId)) {
  125.             $routeName $operation->operationId;
  126.         }
  127.         $collection->add(
  128.             $routeName ?? $this->createRouteName($path$requestMethod),
  129.             $route
  130.         );
  131.     }
  132.     private function parseOpenapiBundleSpecificationExtension(stdClass $operation, array &$defaults, array &$openapiRouteContext): void
  133.     {
  134.         if (isset($operation->{'x-openapi-bundle'}->controller)) {
  135.             $defaults['_controller'] = $operation->{'x-openapi-bundle'}->controller;
  136.         }
  137.         if (isset($defaults['_controller']) === false && isset($operation->{'x-symfony-controller'})) {
  138.             trigger_deprecation(
  139.                 Configuration::BUNDLE_NAME,
  140.                 '1.5',
  141.                 'Using the "x-symfony-controller" specification extension is deprecated and will be removed in 2.0. Please use the "x-openapi-bundle" specification extension instead.'
  142.             );
  143.             $defaults['_controller'] = $operation->{'x-symfony-controller'};
  144.         }
  145.         if (isset($operation->{'x-openapi-bundle'}->deserializationObject)) {
  146.             $openapiRouteContext[RouteContext::DESERIALIZATION_OBJECT] = $operation->{'x-openapi-bundle'}->deserializationObject;
  147.         }
  148.         if (isset($operation->{'x-openapi-bundle'}->deserializationObjectArgumentName)) {
  149.             $openapiRouteContext[RouteContext::DESERIALIZATION_OBJECT_ARGUMENT_NAME] = $operation->{'x-openapi-bundle'}->deserializationObjectArgumentName;
  150.         }
  151.         if (isset($operation->{'x-openapi-bundle'}->additionalRouteAttributes)) {
  152.             $additionalRouteAttributes get_object_vars($operation->{'x-openapi-bundle'}->additionalRouteAttributes);
  153.             foreach ($additionalRouteAttributes as $key => $value) {
  154.                 $defaults[$key] = $value;
  155.             }
  156.         }
  157.     }
  158.     private function addRouteContextForValidation(
  159.         JsonPointer $jsonPointer,
  160.         string $path,
  161.         string $requestMethod,
  162.         stdClass $operation,
  163.         stdClass $pathItem,
  164.         array &$openapiRouteContext
  165.     ): void {
  166.         $openapiRouteContext[RouteContext::REQUEST_BODY_REQUIRED] = false;
  167.         if (isset($operation->requestBody->required)) {
  168.             $openapiRouteContext[RouteContext::REQUEST_BODY_REQUIRED] = $operation->requestBody->required;
  169.         }
  170.         $openapiRouteContext[RouteContext::REQUEST_ALLOWED_CONTENT_TYPES] = [];
  171.         if (isset($operation->requestBody->content)) {
  172.             $openapiRouteContext[RouteContext::REQUEST_ALLOWED_CONTENT_TYPES] = array_keys(
  173.                 get_object_vars($operation->requestBody->content)
  174.             );
  175.         }
  176.         $openapiRouteContext[RouteContext::REQUEST_VALIDATE_QUERY_PARAMETERS] = [];
  177.         $parameters array_merge(
  178.             $pathItem->parameters ?? [],
  179.             $operation->parameters ?? []
  180.         );
  181.         foreach ($parameters as $parameter) {
  182.             if ($parameter->in !== 'query') {
  183.                 continue;
  184.             }
  185.             $openapiRouteContext[RouteContext::REQUEST_VALIDATE_QUERY_PARAMETERS][$parameter->name] = json_encode($parameter);
  186.         }
  187.         if (isset($operation->requestBody->content->{'application/json'}->schema)) {
  188.             $openapiRouteContext[RouteContext::REQUEST_BODY_SCHEMA] = serialize($operation->requestBody->content->{'application/json'}->schema);
  189.         }
  190.         if (isset($operation->requestBody->content->{'application/json'})) {
  191.             $openapiRouteContext[RouteContext::JSON_REQUEST_VALIDATION_POINTER] = sprintf(
  192.                 '/paths/%s/%s/requestBody/content/%s/schema',
  193.                 $jsonPointer->escape($path),
  194.                 $requestMethod,
  195.                 $jsonPointer->escape('application/json')
  196.             );
  197.         }
  198.     }
  199.     /**
  200.      * Returns true when the provided request method is a valid request method in the OpenAPI specification.
  201.      */
  202.     private function isValidRequestMethod(string $requestMethod): bool
  203.     {
  204.         return in_array(
  205.             strtoupper($requestMethod),
  206.             [
  207.                 Request::METHOD_GET,
  208.                 Request::METHOD_PUT,
  209.                 Request::METHOD_POST,
  210.                 Request::METHOD_DELETE,
  211.                 Request::METHOD_OPTIONS,
  212.                 Request::METHOD_HEAD,
  213.                 Request::METHOD_PATCH,
  214.                 Request::METHOD_TRACE,
  215.             ]
  216.         );
  217.     }
  218.     /**
  219.      * Creates a route name based on the path and request method.
  220.      */
  221.     private function createRouteName(string $pathstring $requestMethod): string
  222.     {
  223.         return sprintf('%s_%s',
  224.             trim(preg_replace('/[^a-zA-Z0-9]+/''_'$path), '_'),
  225.             $requestMethod
  226.         );
  227.     }
  228.     /**
  229.      * Adds a catch-all route to handle responses for non-existing routes.
  230.      */
  231.     private function addDefaultRoutes(RouteCollection $collectionstring $resource): void
  232.     {
  233.         $catchAllRoute = new Route(
  234.             '/{catchall}',
  235.             [
  236.                 '_controller' => CatchAllController::CONTROLLER_REFERENCE,
  237.                 RouteContext::REQUEST_ATTRIBUTE => [RouteContext::RESOURCE => $resource],
  238.             ],
  239.             ['catchall' => '.*']
  240.         );
  241.         $collection->add('catch_all'$catchAllRoute);
  242.     }
  243. }