Skip to content

Commit a4e3f42

Browse files
committed
load routes based on open api annotations
1 parent ca85961 commit a4e3f42

6 files changed

Lines changed: 219 additions & 0 deletions

File tree

.editorconfig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
indent_size = 4
7+
indent_style = space
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/tests export-ignore
2+
.editorconfig export-ignore
3+
.gitattributes export-ignore
4+
.gitignore export-ignore
5+
phpunit.xml.dist export-ignore

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.phpunit.result.cache
2+
composer.lock
3+
phpunit.xml
4+
vendor/

composer.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "tobion/openapi-symfony-routing",
3+
"type": "library",
4+
"description": "Loads routes in Symfony based on OpenAPI/Swagger annotations",
5+
"keywords": ["OpenAPI", "swagger", "routing", "router", "routes", "annotations", "OAI"],
6+
"homepage": "https://github.com/Tobion/OpenAPI-Symfony-Routing",
7+
"license": "MIT",
8+
"authors": [
9+
{
10+
"name": "Tobias Schultze",
11+
"homepage": "https://github.com/Tobion"
12+
}
13+
],
14+
"require": {
15+
"php": ">=7.2",
16+
"symfony/finder": "^4.4|^5.0",
17+
"symfony/framework-bundle": "^4.4|^5.0",
18+
"symfony/routing": "^4.4|^5.0",
19+
"zircote/swagger-php": "^2.0"
20+
},
21+
"require-dev": {
22+
"symfony/phpunit-bridge": "^5.0@dev"
23+
},
24+
"autoload": {
25+
"psr-4": {
26+
"Tobion\\OpenApiSymfonyRouting\\": "src/"
27+
}
28+
},
29+
"autoload-dev": {
30+
"psr-4": {
31+
"Tobion\\OpenApiSymfonyRouting\\Tests\\": "tests/"
32+
}
33+
},
34+
"extra": {
35+
"branch-alias": {
36+
"dev-master": "0.1-dev"
37+
}
38+
}
39+
}

phpunit.xml.dist

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
5+
beStrictAboutTestsThatDoNotTestAnything = "true"
6+
beStrictAboutOutputDuringTests = "true"
7+
colors = "true"
8+
verbose = "true"
9+
bootstrap = "vendor/autoload.php"
10+
>
11+
<testsuites>
12+
<testsuite name="Test Suite">
13+
<directory>tests/</directory>
14+
</testsuite>
15+
</testsuites>
16+
17+
<filter>
18+
<whitelist>
19+
<directory>src/</directory>
20+
</whitelist>
21+
</filter>
22+
</phpunit>

src/OpenApiRouteLoader.php

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tobion\OpenApiSymfonyRouting;
6+
7+
use Swagger\Annotations\Operation;
8+
use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface;
9+
use Symfony\Component\Finder\Finder;
10+
use Symfony\Component\Routing\Route;
11+
use Symfony\Component\Routing\RouteCollection;
12+
13+
class OpenApiRouteLoader implements RouteLoaderInterface
14+
{
15+
/**
16+
* @var string[]
17+
*/
18+
private $sourceDirectories;
19+
20+
/**
21+
* @var string
22+
*/
23+
private $sourcePattern;
24+
25+
/**
26+
* @var array<string, int>
27+
*/
28+
private $routeNames = [];
29+
30+
/**
31+
* @param string[] $sourceDirectories
32+
*/
33+
public function __construct(
34+
array $sourceDirectories,
35+
string $sourcePattern = '/\.php/'
36+
) {
37+
$this->sourceDirectories = $sourceDirectories;
38+
$this->sourcePattern = $sourcePattern;
39+
}
40+
41+
public function __invoke(): RouteCollection
42+
{
43+
$finder = new Finder();
44+
$finder->in($this->sourceDirectories)->path($this->sourcePattern);
45+
46+
$fullSwagger = \Swagger\scan($finder);
47+
$routeCollection = new RouteCollection();
48+
49+
foreach ($fullSwagger->paths as $path) {
50+
$this->addRouteFromSwaggerOperation($routeCollection, $path->get);
51+
$this->addRouteFromSwaggerOperation($routeCollection, $path->put);
52+
$this->addRouteFromSwaggerOperation($routeCollection, $path->post);
53+
$this->addRouteFromSwaggerOperation($routeCollection, $path->delete);
54+
$this->addRouteFromSwaggerOperation($routeCollection, $path->options);
55+
$this->addRouteFromSwaggerOperation($routeCollection, $path->head);
56+
$this->addRouteFromSwaggerOperation($routeCollection, $path->patch);
57+
}
58+
59+
$this->routeNames = [];
60+
61+
return $routeCollection;
62+
}
63+
64+
private function addRouteFromSwaggerOperation(RouteCollection $routeCollection, ?Operation $operation): void
65+
{
66+
if (null === $operation) {
67+
return;
68+
}
69+
70+
$controller = $this->getControllerFromSwaggerOperation($operation);
71+
$name = $this->getRouteName($operation, $controller);
72+
$route = $this->createRoute($operation, $controller);
73+
$routeCollection->add($name, $route);
74+
}
75+
76+
private function createRoute(Operation $operation, string $controller): Route
77+
{
78+
$formatSuffix = $operation->x['format-suffix'] ?? true;
79+
$path = $formatSuffix ? $operation->path.'.{_format}' : $operation->path;
80+
$route = new Route($path);
81+
$route->setMethods($operation->method);
82+
$route->setDefault('_controller', $controller);
83+
if ($formatSuffix) {
84+
$formatPattern = $operation->x['format-pattern'] ?? 'json|xml';
85+
$route->setDefault('_format', null);
86+
$route->setRequirement('_format', $formatPattern);
87+
}
88+
if (null !== $operation->parameters) {
89+
foreach ($operation->parameters as $parameter) {
90+
if ('path' === $parameter->in && null !== $parameter->pattern) {
91+
$route->setRequirement($parameter->name, $parameter->pattern);
92+
}
93+
}
94+
}
95+
96+
return $route;
97+
}
98+
99+
private function getControllerFromSwaggerOperation(Operation $operation): string
100+
{
101+
$classOrService = ltrim($operation->_context->fullyQualifiedName($operation->_context->class), '\\');
102+
103+
return $classOrService.'::'.$operation->_context->method;
104+
}
105+
106+
private function getRouteName(Operation $operation, string $controller): string
107+
{
108+
return \Swagger\UNDEFINED === $operation->operationId ? $this->getDefaultRouteName($controller) : $operation->operationId;
109+
}
110+
111+
/**
112+
* @see \Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader::getDefaultRouteName
113+
*/
114+
private function getDefaultRouteName(string $controller): string
115+
{
116+
$name = str_replace(['\\', '::'], '_', $controller);
117+
$name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name);
118+
119+
$name = preg_replace([
120+
'/(bundle|controller)_/',
121+
'/action(_\d+)?$/',
122+
'/__/',
123+
], [
124+
'_',
125+
'\\1',
126+
'_',
127+
], $name);
128+
129+
// handle several routes for the same controller
130+
if (isset($this->routeNames[$name])) {
131+
++$this->routeNames[$name];
132+
133+
$name .= '_'.$this->routeNames[$name];
134+
} else {
135+
$this->routeNames[$name] = 0;
136+
}
137+
138+
return $name;
139+
}
140+
}

0 commit comments

Comments
 (0)