Skip to content

Commit 63dc3c9

Browse files
authored
Merge pull request #28 from ensi-platform/task-104744
#104744
2 parents 7a24470 + 6a0d2fb commit 63dc3c9

24 files changed

+695
-42
lines changed

src/Commands/GenerateServer.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010

1111
class GenerateServer extends Command
1212
{
13+
public const SUPPORTED_ENTITIES = [
14+
'controllers',
15+
'enums',
16+
'requests',
17+
'routes',
18+
'pest_tests',
19+
'resources',
20+
'policies',
21+
];
22+
1323
/** var @string */
1424
protected $signature = 'openapi:generate-server {--e|entities=}';
1525

@@ -55,7 +65,12 @@ public function handleMapping(string $sourcePath, array $optionsPerEntity)
5565
{
5666
$specObject = $this->parseSpec($sourcePath);
5767

58-
foreach ($this->config['supported_entities'] as $entity => $generatorClass) {
68+
foreach (static::SUPPORTED_ENTITIES as $entity) {
69+
$generatorClass = $this->config['supported_entities'][$entity] ?? null;
70+
if (!isset($generatorClass)) {
71+
continue;
72+
}
73+
5974
if (!$this->shouldEntityBeGenerated($entity)) {
6075
continue;
6176
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Data\Controllers;
4+
5+
class ControllersStorage
6+
{
7+
/** @var array Recently created controllers */
8+
protected array $controllers = [];
9+
10+
public function markNewControllerMethod(
11+
string $serversUrl,
12+
string $path,
13+
string $method,
14+
array $responseCodes
15+
): void {
16+
$this->controllers[$serversUrl][$path][$method] = $responseCodes;
17+
}
18+
19+
public function isExistControllerMethod(
20+
string $serversUrl,
21+
string $path,
22+
string $method,
23+
int $responseCode
24+
): bool {
25+
$codes = $this->controllers[$serversUrl][$path][$method] ?? [];
26+
27+
return !in_array($responseCode, $codes);
28+
}
29+
}

src/Generators/BaseGenerator.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Ensi\LaravelOpenApiServerGenerator\Generators;
44

5+
use Ensi\LaravelOpenApiServerGenerator\Data\Controllers\ControllersStorage;
6+
use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser;
57
use Ensi\LaravelOpenApiServerGenerator\Utils\PhpDocGenerator;
68
use Ensi\LaravelOpenApiServerGenerator\Utils\PSR4PathConverter;
79
use Ensi\LaravelOpenApiServerGenerator\Utils\RouteHandlerParser;
@@ -22,6 +24,8 @@ public function __construct(
2224
protected RouteHandlerParser $routeHandlerParser,
2325
protected TypesMapper $typesMapper,
2426
protected PhpDocGenerator $phpDocGenerator,
27+
protected ClassParser $classParser,
28+
protected ControllersStorage $controllersStorage,
2529
) {
2630
}
2731

src/Generators/ControllersGenerator.php

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,26 @@
33
namespace Ensi\LaravelOpenApiServerGenerator\Generators;
44

55
use cebe\openapi\SpecObjectInterface;
6+
use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser;
67
use stdClass;
78

89
class ControllersGenerator extends BaseGenerator implements GeneratorInterface
910
{
11+
public const REQUEST_NAMESPACE = 'Illuminate\Http\Request';
12+
public const RESPONSABLE_NAMESPACE = 'Illuminate\Contracts\Support\Responsable';
13+
public const DELIMITER = "\n ";
14+
1015
private array $methodsWithRequests = ['PATCH', 'POST', 'PUT', 'DELETE'];
1116

17+
private string $serversUrl;
18+
1219
public function generate(SpecObjectInterface $specObject): void
1320
{
21+
$openApiData = $specObject->getSerializableData();
22+
$this->serversUrl = $openApiData?->servers[0]?->url ?? '';
23+
1424
$controllers = $this->extractControllers($specObject);
15-
$this->createControllersFiles($controllers, $this->templatesManager->getTemplate('Controller.template'));
25+
$this->createControllersFiles($controllers);
1626
}
1727

1828
private function extractControllers(SpecObjectInterface $specObject): array
@@ -21,7 +31,7 @@ private function extractControllers(SpecObjectInterface $specObject): array
2131

2232
$controllers = [];
2333
$paths = $openApiData->paths ?: [];
24-
foreach ($paths as $routes) {
34+
foreach ($paths as $path => $routes) {
2535
foreach ($routes as $method => $route) {
2636
$requestClassName = null;
2737
$methodWithRequest = in_array(strtoupper($method), $this->methodsWithRequests);
@@ -56,14 +66,20 @@ private function extractControllers(SpecObjectInterface $specObject): array
5666
list($requestClassName, $requestNamespace) = $this->getActualClassNameAndNamespace($requestClassName, $requestNamespace);
5767
$requestNamespace .= '\\' . ucfirst($requestClassName);
5868

59-
$controllers[$fqcn]['requestsNamespaces'][] = $requestNamespace;
60-
} elseif ($methodWithRequest) {
61-
$controllers[$fqcn]['requestsNamespaces'][] = 'Illuminate\Http\Request';
69+
$controllers[$fqcn]['requestsNamespaces'][$requestNamespace] = $requestNamespace;
6270
}
6371

72+
$responses = $route->responses ?? null;
6473
$controllers[$fqcn]['actions'][] = [
6574
'name' => $handler->method ?: '__invoke',
75+
'with_request_namespace' => $methodWithRequest && !empty($route->{'x-lg-skip-request-generation'}),
6676
'parameters' => array_merge($this->extractPathParameters($route), $this->getActionExtraParameters($methodWithRequest, $requestClassName)),
77+
78+
'route' => [
79+
'method' => $method,
80+
'path' => $path,
81+
'responseCodes' => $responses ? array_keys(get_object_vars($responses)) : [],
82+
],
6783
];
6884
}
6985
}
@@ -73,7 +89,7 @@ private function extractControllers(SpecObjectInterface $specObject): array
7389

7490
private function extractPathParameters(stdClass $route): array
7591
{
76-
$oasRoutePath = array_filter($route->parameters ?? [], fn (stdClass $param) => $param?->in === "path");
92+
$oasRoutePath = array_filter($route->parameters ?? [], fn (stdClass $param) => $param?->in === "path");
7793

7894
return array_map(fn (stdClass $param) => [
7995
'name' => $param->name,
@@ -93,55 +109,106 @@ private function getActionExtraParameters(bool $methodWithRequest, $requestClass
93109
return [];
94110
}
95111

96-
private function createControllersFiles(array $controllers, string $template): void
112+
private function createControllersFiles(array $controllers): void
97113
{
98114
foreach ($controllers as $controller) {
99115
$namespace = $controller['namespace'];
100116
$className = $controller['className'];
101117

102118
$filePath = $this->getNamespacedFilePath($className, $namespace);
103-
if ($this->filesystem->exists($filePath)) {
119+
$controllerExists = $this->filesystem->exists($filePath);
120+
if (!$controllerExists) {
121+
$this->createEmptyControllerFile($filePath, $controller);
122+
}
123+
124+
$class = $this->classParser->parse("$namespace\\$className");
125+
126+
$newMethods = $this->convertMethodsToString($class, $controller['actions'], $controller['requestsNamespaces']);
127+
if (!empty($newMethods)) {
128+
$controller['requestsNamespaces'][static::RESPONSABLE_NAMESPACE] = static::RESPONSABLE_NAMESPACE;
129+
} elseif ($controllerExists) {
104130
continue;
105131
}
106132

107-
$this->putWithDirectoryCheck(
108-
$filePath,
109-
$this->replacePlaceholders($template, [
110-
'{{ namespace }}' => $namespace,
111-
'{{ className }}' => $className,
112-
'{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']),
113-
'{{ methods }}' => $this->convertMethodsToString($controller['actions']),
114-
])
115-
);
133+
$content = $class->getContentWithAdditionalMethods($newMethods, $controller['requestsNamespaces']);
134+
135+
$this->writeControllerFile($filePath, $controller, $content);
116136
}
117137
}
118138

139+
protected function writeControllerFile(string $filePath, array $controller, string $classContent): void
140+
{
141+
$this->putWithDirectoryCheck(
142+
$filePath,
143+
$this->replacePlaceholders(
144+
$this->templatesManager->getTemplate('ControllerExists.template'),
145+
[
146+
'{{ namespace }}' => $controller['namespace'],
147+
'{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']),
148+
'{{ classContent }}' => $classContent,
149+
]
150+
)
151+
);
152+
}
153+
154+
protected function createEmptyControllerFile(string $filePath, array $controller): void
155+
{
156+
$this->putWithDirectoryCheck(
157+
$filePath,
158+
$this->replacePlaceholders(
159+
$this->templatesManager->getTemplate('ControllerEmpty.template'),
160+
[
161+
'{{ namespace }}' => $controller['namespace'],
162+
'{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']),
163+
'{{ className }}' => $controller['className'],
164+
]
165+
)
166+
);
167+
}
168+
119169
private function formatActionParamsAsString(array $params): string
120170
{
121171
return implode(', ', array_map(fn (array $param) => $param['type'] . " $" . $param['name'], $params));
122172
}
123173

124-
private function convertMethodsToString(array $methods): string
174+
private function convertMethodsToString(ClassParser $class, array $methods, array &$namespaces): string
125175
{
126176
$methodsStrings = [];
127177

128178
foreach ($methods as $method) {
179+
if ($class->hasMethod($method['name'])) {
180+
continue;
181+
}
182+
183+
if ($method['with_request_namespace']) {
184+
$namespaces[static::REQUEST_NAMESPACE] = static::REQUEST_NAMESPACE;
185+
}
186+
129187
$methodsStrings[] = $this->replacePlaceholders(
130188
$this->templatesManager->getTemplate('ControllerMethod.template'),
131189
[
132190
'{{ method }}' => $method['name'],
133191
'{{ params }}' => $this->formatActionParamsAsString($method['parameters']),
134192
]
135193
);
194+
195+
$this->controllersStorage->markNewControllerMethod(
196+
serversUrl: $this->serversUrl,
197+
path: $method['route']['path'],
198+
method: $method['route']['method'],
199+
responseCodes: $method['route']['responseCodes'],
200+
);
136201
}
137202

138-
return implode("\n\n ", $methodsStrings);
203+
$prefix = !empty($methodsStrings) ? static::DELIMITER : '';
204+
205+
return $prefix . implode(static::DELIMITER, $methodsStrings);
139206
}
140207

141208
protected function formatRequestNamespaces(array $namespaces): string
142209
{
143-
$namespaces = array_unique($namespaces);
144-
sort($namespaces);
210+
$namespaces = array_values($namespaces);
211+
sort($namespaces, SORT_STRING | SORT_FLAG_CASE);
145212

146213
return implode("\n", array_map(fn (string $namespaces) => "use {$namespaces};", $namespaces));
147214
}

src/Generators/PestTestsGenerator.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,30 @@ private function getPhpHttpTestMethodCommon(string $httpMethod): string
3434
return $httpMethod;
3535
}
3636

37-
protected function convertRoutesToTestsString(array $routes, string $serversUrl): string
37+
protected function convertRoutesToTestsString(array $routes, string $serversUrl, bool $onlyNewMethods = false): string
3838
{
39-
$testsFunctions = [
40-
"uses()->group('component');",
41-
];
39+
$testsFunctions = $onlyNewMethods ? [] : ["uses()->group('component');"];
4240

4341
foreach ($routes as $route) {
4442
foreach ($route['responseCodes'] as $responseCode) {
4543
if ($responseCode < 200 || $responseCode >= 500) {
4644
continue;
4745
}
4846

47+
$methodExists = $this->controllersStorage->isExistControllerMethod(
48+
serversUrl: $serversUrl,
49+
path: $route['path'],
50+
method: $route['method'],
51+
responseCode: $responseCode,
52+
);
53+
54+
if ($onlyNewMethods && $methodExists) {
55+
continue;
56+
}
57+
4958
$url = $serversUrl . $route['path'];
5059
$testName = strtoupper($route['method']) . ' ' . $url. ' ' . $responseCode;
51-
$phpHttpMethod = $this->getPhpHttpTestMethod($route['method'], $route['responseContentType']);
60+
$phpHttpMethod = $this->getPhpHttpTestMethod($route['method'], $route['responseContentType']);
5261
$testsFunctions[] = <<<FUNC
5362
5463
test('{$testName}', function () {

src/Generators/PoliciesGenerator.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use cebe\openapi\SpecObjectInterface;
66
use Ensi\LaravelOpenApiServerGenerator\DTO\ParsedRouteHandler;
7+
use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser;
78
use InvalidArgumentException;
89
use RuntimeException;
910
use stdClass;
@@ -70,6 +71,13 @@ protected function createPoliciesFiles(array $policies, string $template): void
7071
foreach ($policies as ['className' => $className, 'namespace' => $namespace, 'methods' => $methods]) {
7172
$filePath = $this->getNamespacedFilePath($className, $namespace);
7273
if ($this->filesystem->exists($filePath)) {
74+
$class = $this->classParser->parse("$namespace\\$className");
75+
76+
$newPolicies = $this->convertMethodsToString($methods, $class);
77+
if (!empty($newPolicies)) {
78+
$class->addMethods($newPolicies);
79+
}
80+
7381
continue;
7482
}
7583

@@ -104,17 +112,31 @@ private function handlerValidation(ParsedRouteHandler $handler): bool
104112
};
105113
}
106114

107-
private function convertMethodsToString(array $methods): string
115+
private function convertMethodsToString(array $methods, ?ClassParser $class = null): string
108116
{
109117
$methodsStrings = [];
110118

111119
foreach ($methods as $method) {
120+
if ($class?->hasMethod($method)) {
121+
continue;
122+
}
123+
112124
$methodsStrings[] = $this->replacePlaceholders(
113125
$this->templatesManager->getTemplate('PolicyGate.template'),
114126
['{{ method }}' => $method]
115127
);
116128
}
117129

130+
if ($class) {
131+
$existMethods = $class->getMethods();
132+
foreach ($existMethods as $methodName => $method) {
133+
if (!in_array($methodName, $methods) && !$class->isTraitMethod($methodName)) {
134+
$className = $class->getClassName();
135+
console_warning("Warning: метод {$className}::{$methodName} отсутствует в спецификации или не может возвращать 403 ошибку");
136+
}
137+
}
138+
}
139+
118140
return implode("\n\n ", $methodsStrings);
119141
}
120142
}

src/Generators/RoutesGenerator.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,7 @@ private function formatControllerNamespaces(array $controllerNamespaces): string
112112
}
113113
}
114114

115-
uasort($namespaces, function (string $first, string $second) {
116-
$firstNamespace = str_replace('\\', ' ', trim(preg_replace('%/\*(.*)\*/%s', '', $first)));
117-
$secondNamespace = str_replace('\\', ' ', trim(preg_replace('%/\*(.*)\*/%s', '', $second)));
118-
119-
return strcasecmp($firstNamespace, $secondNamespace);
120-
});
115+
sort($namespaces, SORT_STRING | SORT_FLAG_CASE);
121116

122117
return implode("\n", array_map(fn (string $namespace) => "use {$namespace};", $namespaces));
123118
}

0 commit comments

Comments
 (0)