Passed
Pull Request — main (#65)
by Niels
01:53
created

parseOpenapiBundleSpecificationExtension()   B

Complexity

Conditions 8
Paths 48

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 12
nc 48
nop 3
dl 0
loc 22
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the OpenapiBundle package.
7
 *
8
 * (c) Niels Nijens <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Nijens\OpenapiBundle\Routing;
15
16
use Nijens\OpenapiBundle\Controller\CatchAllController;
17
use Nijens\OpenapiBundle\Json\JsonPointer;
18
use Nijens\OpenapiBundle\Json\SchemaLoaderInterface;
19
use stdClass;
20
use Symfony\Component\Config\FileLocatorInterface;
21
use Symfony\Component\Config\Loader\FileLoader;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\Routing\Route;
24
use Symfony\Component\Routing\RouteCollection;
25
26
/**
27
 * Loads the paths from an OpenAPI specification as routes.
28
 *
29
 * @author Niels Nijens <[email protected]>
30
 */
31
class RouteLoader extends FileLoader
32
{
33
    /**
34
     * @var string
35
     */
36
    public const TYPE = 'openapi';
37
38
    /**
39
     * @var SchemaLoaderInterface
40
     */
41
    private $schemaLoader;
42
43
    /**
44
     * @var bool
45
     */
46
    private $useOperationIdAsRouteName;
47
48
    /**
49
     * Constructs a new RouteLoader instance.
50
     */
51
    public function __construct(
52
        FileLocatorInterface $locator,
53
        SchemaLoaderInterface $schemaLoader,
54
        bool $useOperationIdAsRouteName = false
55
    ) {
56
        parent::__construct($locator);
57
58
        $this->schemaLoader = $schemaLoader;
59
        $this->useOperationIdAsRouteName = $useOperationIdAsRouteName;
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function supports($resource, $type = null): bool
66
    {
67
        return self::TYPE === $type;
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function load($resource, $type = null): RouteCollection
74
    {
75
        $file = $this->getLocator()->locate($resource, null, true);
76
77
        $schema = $this->schemaLoader->load($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type array; however, parameter $file of Nijens\OpenapiBundle\Jso...LoaderInterface::load() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

77
        $schema = $this->schemaLoader->load(/** @scrutinizer ignore-type */ $file);
Loading history...
78
79
        $jsonPointer = new JsonPointer($schema);
80
81
        $routeCollection = new RouteCollection();
82
        $routeCollection->addResource($this->schemaLoader->getFileResource($file));
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type array; however, parameter $file of Nijens\OpenapiBundle\Jso...face::getFileResource() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
        $routeCollection->addResource($this->schemaLoader->getFileResource(/** @scrutinizer ignore-type */ $file));
Loading history...
Bug introduced by
It seems like $this->schemaLoader->getFileResource($file) can also be of type null; however, parameter $resource of Symfony\Component\Routin...llection::addResource() does only seem to accept Symfony\Component\Config...ource\ResourceInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
        $routeCollection->addResource(/** @scrutinizer ignore-type */ $this->schemaLoader->getFileResource($file));
Loading history...
83
84
        $paths = get_object_vars($jsonPointer->get('/paths'));
85
        foreach ($paths as $path => $pathItem) {
86
            $this->parsePathItem($jsonPointer, $file, $routeCollection, $path, $pathItem);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type array; however, parameter $resource of Nijens\OpenapiBundle\Rou...Loader::parsePathItem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

86
            $this->parsePathItem($jsonPointer, /** @scrutinizer ignore-type */ $file, $routeCollection, $path, $pathItem);
Loading history...
87
        }
88
89
        $this->addDefaultRoutes($routeCollection, $file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type array; however, parameter $resource of Nijens\OpenapiBundle\Rou...der::addDefaultRoutes() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
        $this->addDefaultRoutes($routeCollection, /** @scrutinizer ignore-type */ $file);
Loading history...
90
91
        return $routeCollection;
92
    }
93
94
    /**
95
     * Parses a path item of the OpenAPI specification for a route.
96
     */
97
    private function parsePathItem(
98
        JsonPointer $jsonPointer,
99
        string $resource,
100
        RouteCollection $collection,
101
        string $path,
102
        stdClass $pathItem
103
    ): void {
104
        $operations = get_object_vars($pathItem);
105
        foreach ($operations as $requestMethod => $operation) {
106
            if ($this->isValidRequestMethod($requestMethod) === false) {
107
                return;
108
            }
109
110
            $this->parseOperation($jsonPointer, $resource, $collection, $path, $requestMethod, $operation);
111
        }
112
    }
113
114
    /**
115
     * Parses an operation of the OpenAPI specification for a route.
116
     */
117
    private function parseOperation(
118
        JsonPointer $jsonPointer,
119
        string $resource,
120
        RouteCollection $collection,
121
        string $path,
122
        string $requestMethod,
123
        stdClass $operation
124
    ): void {
125
        $defaults = [];
126
        $openapiRouteContext = [
127
            RouteContext::RESOURCE => $resource,
128
        ];
129
130
        $this->parseOpenapiBundleSpecificationExtension($operation, $defaults, $openapiRouteContext);
131
132
        if (isset($operation->requestBody->content->{'application/json'})) {
133
            $openapiRouteContext[RouteContext::JSON_REQUEST_VALIDATION_POINTER] = sprintf(
134
                '/paths/%s/%s/requestBody/content/%s/schema',
135
                $jsonPointer->escape($path),
136
                $requestMethod,
137
                $jsonPointer->escape('application/json')
138
            );
139
        }
140
141
        $defaults[RouteContext::REQUEST_ATTRIBUTE] = $openapiRouteContext;
142
143
        $route = new Route($path, $defaults, []);
144
        $route->setMethods($requestMethod);
145
146
        $routeName = null;
147
        if ($this->useOperationIdAsRouteName && isset($operation->operationId)) {
148
            $routeName = $operation->operationId;
149
        }
150
151
        $collection->add(
152
            $routeName ?? $this->createRouteName($path, $requestMethod),
153
            $route
154
        );
155
    }
156
157
    private function parseOpenapiBundleSpecificationExtension(stdClass $operation, array &$defaults, array &$openapiRouteContext): void
158
    {
159
        if (isset($operation->{'x-openapi-bundle'}->controller)) {
160
            $defaults['_controller'] = $operation->{'x-openapi-bundle'}->controller;
161
        }
162
163
        if (isset($defaults['_controller']) === false && isset($operation->{'x-symfony-controller'})) {
164
            $defaults['_controller'] = $operation->{'x-symfony-controller'};
165
        }
166
167
        if (isset($operation->{'x-openapi-bundle'}->deserializationObject)) {
168
            $openapiRouteContext[RouteContext::DESERIALIZATION_OBJECT] = $operation->{'x-openapi-bundle'}->deserializationObject;
169
        }
170
171
        if (isset($operation->{'x-openapi-bundle'}->deserializationObjectArgumentName)) {
172
            $openapiRouteContext[RouteContext::DESERIALIZATION_OBJECT_ARGUMENT_NAME] = $operation->{'x-openapi-bundle'}->deserializationObjectArgumentName;
173
        }
174
175
        if (isset($operation->{'x-openapi-bundle'}->additionalRouteAttributes)) {
176
            $additionalRouteAttributes = get_object_vars($operation->{'x-openapi-bundle'}->additionalRouteAttributes);
177
            foreach ($additionalRouteAttributes as $key => $value) {
178
                $defaults[$key] = $value;
179
            }
180
        }
181
    }
182
183
    /**
184
     * Returns true when the provided request method is a valid request method in the OpenAPI specification.
185
     */
186
    private function isValidRequestMethod(string $requestMethod): bool
187
    {
188
        return in_array(
189
            strtoupper($requestMethod),
190
            [
191
                Request::METHOD_GET,
192
                Request::METHOD_PUT,
193
                Request::METHOD_POST,
194
                Request::METHOD_DELETE,
195
                Request::METHOD_OPTIONS,
196
                Request::METHOD_HEAD,
197
                Request::METHOD_PATCH,
198
                Request::METHOD_TRACE,
199
            ]
200
        );
201
    }
202
203
    /**
204
     * Creates a route name based on the path and request method.
205
     */
206
    private function createRouteName(string $path, string $requestMethod): string
207
    {
208
        return sprintf('%s_%s',
209
            trim(preg_replace('/[^a-zA-Z0-9]+/', '_', $path), '_'),
210
            $requestMethod
211
        );
212
    }
213
214
    /**
215
     * Adds a catch-all route to handle responses for non-existing routes.
216
     */
217
    private function addDefaultRoutes(RouteCollection $collection, string $resource): void
218
    {
219
        $catchAllRoute = new Route(
220
            '/{catchall}',
221
            [
222
                '_controller' => CatchAllController::CONTROLLER_REFERENCE,
223
                RouteContext::REQUEST_ATTRIBUTE => [RouteContext::RESOURCE => $resource],
224
            ],
225
            ['catchall' => '.*']
226
        );
227
228
        $collection->add('catch_all', $catchAllRoute);
229
    }
230
}
231