Completed
Push — main ( 39253f...17fd68 )
by Niels
22s queued 14s
created

parseOpenapiBundleSpecificationExtension()   A

Complexity

Conditions 6
Paths 24

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 10
c 2
b 0
f 0
nc 24
nop 3
dl 0
loc 18
rs 9.2222
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, $pathItem);
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
        stdClass $pathItem
125
    ): void {
126
        $defaults = [];
127
        $openapiRouteContext = [
128
            RouteContext::RESOURCE => $resource,
129
        ];
130
131
        $this->parseOpenapiBundleSpecificationExtension($operation, $defaults, $openapiRouteContext);
132
        $this->addRouteContextForValidation(
133
            $jsonPointer,
134
            $path,
135
            $requestMethod,
136
            $operation,
137
            $pathItem,
138
            $openapiRouteContext
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($operation->{'x-openapi-bundle'}->deserializationObject)) {
164
            $openapiRouteContext[RouteContext::DESERIALIZATION_OBJECT] = $operation->{'x-openapi-bundle'}->deserializationObject;
165
        }
166
167
        if (isset($operation->{'x-openapi-bundle'}->deserializationObjectArgumentName)) {
168
            $openapiRouteContext[RouteContext::DESERIALIZATION_OBJECT_ARGUMENT_NAME] = $operation->{'x-openapi-bundle'}->deserializationObjectArgumentName;
169
        }
170
171
        if (isset($operation->{'x-openapi-bundle'}->additionalRouteAttributes)) {
172
            $additionalRouteAttributes = get_object_vars($operation->{'x-openapi-bundle'}->additionalRouteAttributes);
173
            foreach ($additionalRouteAttributes as $key => $value) {
174
                $defaults[$key] = $value;
175
            }
176
        }
177
    }
178
179
    private function addRouteContextForValidation(
180
        JsonPointer $jsonPointer,
181
        string $path,
182
        string $requestMethod,
183
        stdClass $operation,
184
        stdClass $pathItem,
185
        array &$openapiRouteContext
186
    ): void {
187
        $openapiRouteContext[RouteContext::REQUEST_BODY_REQUIRED] = false;
188
        if (isset($operation->requestBody->required)) {
189
            $openapiRouteContext[RouteContext::REQUEST_BODY_REQUIRED] = $operation->requestBody->required;
190
        }
191
192
        $openapiRouteContext[RouteContext::REQUEST_ALLOWED_CONTENT_TYPES] = [];
193
        if (isset($operation->requestBody->content)) {
194
            $openapiRouteContext[RouteContext::REQUEST_ALLOWED_CONTENT_TYPES] = array_keys(
195
                get_object_vars($operation->requestBody->content)
196
            );
197
        }
198
199
        $openapiRouteContext[RouteContext::REQUEST_VALIDATE_QUERY_PARAMETERS] = [];
200
        $parameters = array_merge(
201
            $pathItem->parameters ?? [],
202
            $operation->parameters ?? []
203
        );
204
        foreach ($parameters as $parameter) {
205
            if ($parameter->in !== 'query') {
206
                continue;
207
            }
208
209
            $openapiRouteContext[RouteContext::REQUEST_VALIDATE_QUERY_PARAMETERS][$parameter->name] = json_encode($parameter);
210
        }
211
212
        if (isset($operation->requestBody->content->{'application/json'}->schema)) {
213
            $openapiRouteContext[RouteContext::REQUEST_BODY_SCHEMA] = serialize($operation->requestBody->content->{'application/json'}->schema);
214
        }
215
216
        if (isset($operation->requestBody->content->{'application/json'})) {
217
            $openapiRouteContext[RouteContext::JSON_REQUEST_VALIDATION_POINTER] = sprintf(
218
                '/paths/%s/%s/requestBody/content/%s/schema',
219
                $jsonPointer->escape($path),
220
                $requestMethod,
221
                $jsonPointer->escape('application/json')
222
            );
223
        }
224
    }
225
226
    /**
227
     * Returns true when the provided request method is a valid request method in the OpenAPI specification.
228
     */
229
    private function isValidRequestMethod(string $requestMethod): bool
230
    {
231
        return in_array(
232
            strtoupper($requestMethod),
233
            [
234
                Request::METHOD_GET,
235
                Request::METHOD_PUT,
236
                Request::METHOD_POST,
237
                Request::METHOD_DELETE,
238
                Request::METHOD_OPTIONS,
239
                Request::METHOD_HEAD,
240
                Request::METHOD_PATCH,
241
                Request::METHOD_TRACE,
242
            ]
243
        );
244
    }
245
246
    /**
247
     * Creates a route name based on the path and request method.
248
     */
249
    private function createRouteName(string $path, string $requestMethod): string
250
    {
251
        return sprintf('%s_%s',
252
            trim(preg_replace('/[^a-zA-Z0-9]+/', '_', $path), '_'),
253
            $requestMethod
254
        );
255
    }
256
257
    /**
258
     * Adds a catch-all route to handle responses for non-existing routes.
259
     */
260
    private function addDefaultRoutes(RouteCollection $collection, string $resource): void
261
    {
262
        $catchAllRoute = new Route(
263
            '/{catchall}',
264
            [
265
                '_controller' => CatchAllController::CONTROLLER_REFERENCE,
266
                RouteContext::REQUEST_ATTRIBUTE => [RouteContext::RESOURCE => $resource],
267
            ],
268
            ['catchall' => '.*']
269
        );
270
271
        $collection->add('catch_all', $catchAllRoute);
272
    }
273
}
274