Skip to content

Commit 863c108

Browse files
feat: generate components from responses and requests
1 parent f4d3310 commit 863c108

File tree

8 files changed

+99
-38
lines changed

8 files changed

+99
-38
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,6 @@ parameters:
120120
count: 1
121121
path: src/Generator/ResponsesCreator.php
122122

123-
-
124-
message: "#^Cannot access offset 0 on array\\|Illuminate\\\\Contracts\\\\Support\\\\Arrayable\\|JsonSerializable\\.$#"
125-
count: 1
126-
path: src/Generator/ResponsesCreator.php
127-
128123
-
129124
message: "#^PHPDoc tag @param references unknown parameter\\: \\$resourceName$#"
130125
count: 1

src/Generator/ComponentManager.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Intermax\LaravelOpenApi\Generator;
6+
7+
use cebe\openapi\spec\Components;
8+
use cebe\openapi\spec\Schema;
9+
10+
class ComponentManager
11+
{
12+
/**
13+
* @var array<string, mixed>
14+
*/
15+
protected array $components = [
16+
'schemas' => [],
17+
];
18+
19+
public function __construct()
20+
{
21+
}
22+
23+
public function addSchema(string $name, Schema $schema): void
24+
{
25+
$schema = (array) $schema->getSerializableData();
26+
27+
if (isset($this->components['schemas'][$name])) {
28+
$schema = array_replace_recursive($this->components['schemas'][$name], $schema);
29+
}
30+
31+
$this->components['schemas'][$name] = $schema;
32+
}
33+
34+
public function components(): Components
35+
{
36+
return new Components(array_filter($this->components));
37+
}
38+
}

src/Generator/Generator.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
use cebe\openapi\exceptions\TypeErrorException;
66
use cebe\openapi\spec\OpenApi;
7+
use cebe\openapi\spec\Operation;
78
use cebe\openapi\spec\PathItem;
89
use cebe\openapi\spec\Server;
910
use cebe\openapi\Writer;
1011
use Illuminate\Contracts\Config\Repository;
1112
use Illuminate\Foundation\Http\FormRequest;
13+
use Illuminate\Routing\Route;
1214
use Illuminate\Routing\Router;
1315
use Illuminate\Support\Str;
1416
use Intermax\LaravelOpenApi\Generator\Parameters\ParametersCreator;
@@ -18,17 +20,18 @@ class Generator
1820
public function __construct(
1921
protected Router $router,
2022
protected OperationCreator $operationCreator,
21-
protected ComponentsCreator $componentsCreator,
23+
protected ComponentManager $componentManager,
2224
protected Repository $config,
2325
protected RouteAnalyser $routeAnalyser,
2426
protected RequestBodyCreator $requestBodyCreator,
2527
protected ParametersCreator $parametersCreator,
26-
protected ResponsesCreator $responsesCreator
28+
protected ResponsesCreator $responsesCreator,
29+
2730
) {
2831
}
2932

3033
/**
31-
* @param string $output json or yaml
34+
* @param string $output json or yaml
3235
*
3336
* @throws TypeErrorException
3437
*/
@@ -77,6 +80,8 @@ public function generate(string $output = 'json'): string
7780
}
7881
}
7982

83+
$openApi->components = $this->componentManager->components();
84+
8085
return match ($output) {
8186
'yaml' => Writer::writeToYaml($openApi),
8287
default => Writer::writeToJson($openApi),
@@ -135,24 +140,26 @@ protected function getOperationId(string $method, string $uri): string
135140

136141
public function buildOperation(Route $route, string $method): Operation
137142
{
143+
$entityName = $this->deriveEntityNameFromUri($route->uri());
144+
138145
$requestClassName = $this->routeAnalyser->determineRequestClass($route);
139146

140147
if ($requestClassName) {
141148
/** @var FormRequest $requestClass */
142149
$requestClass = new $requestClassName();
143150

144-
$requestBody = $this->requestBodyCreator->create($requestClass);
151+
$requestBody = $this->requestBodyCreator->create($requestClass, $entityName);
145152
}
146153

147154
$resourceClassName = $this->routeAnalyser->determineResourceClass($route);
148155

149156
if ($resourceClassName) {
150-
$response = $this->responsesCreator->createFromResource($resourceClassName);
157+
$response = $this->responsesCreator->createFromResource($resourceClassName, $entityName);
151158
}
152159

153160
return $this->operationCreator->create(
154161
method: $method,
155-
entity: $this->deriveEntityNameFromUri($route->uri()),
162+
entity: $entityName,
156163
operationId: $this->getOperationId($method, $route->uri()),
157164
responses: $response ?? $this->responsesCreator->emptyResponse(),
158165
requestBody: $requestBody ?? null,

src/Generator/Mapping/Property.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Illuminate\Contracts\Support\Arrayable;
66
use JsonSerializable;
77

8-
class Property implements JsonSerializable, Arrayable
8+
class Property implements Arrayable, JsonSerializable
99
{
1010
public function __construct(
1111
protected string $type,

src/Generator/RequestBodyCreator.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Intermax\LaravelOpenApi\Generator;
44

55
use cebe\openapi\spec\RequestBody;
6+
use cebe\openapi\spec\Schema;
67
use Illuminate\Contracts\Config\Repository;
78
use Illuminate\Foundation\Http\FormRequest;
89
use Illuminate\Validation\Rule;
@@ -11,11 +12,11 @@
1112

1213
class RequestBodyCreator
1314
{
14-
public function __construct(private readonly Repository $config)
15+
public function __construct(private readonly Repository $config, private ComponentManager $componentManager)
1516
{
1617
}
1718

18-
public function create(FormRequest $request): ?RequestBody
19+
public function create(FormRequest $request, ?string $entityName = null): ?RequestBody
1920
{
2021
$body = [];
2122

@@ -74,12 +75,24 @@ public function create(FormRequest $request): ?RequestBody
7475
$properties = array_replace_recursive($properties, $property);
7576
}
7677

78+
$schema = [
79+
'type' => 'object',
80+
'properties' => $properties,
81+
];
82+
83+
if ($entityName) {
84+
if (array_key_exists('data', $properties)) {
85+
$this->componentManager->addSchema($entityName, new Schema($properties['data']));
86+
} else {
87+
$this->componentManager->addSchema($entityName, new Schema($properties));
88+
}
89+
90+
$schema = ['$ref' => '#/components/schemas/'.$entityName];
91+
}
92+
7793
$body['content'] = [
7894
$this->config->get('open-api.content_type') => [
79-
'schema' => [
80-
'type' => 'object',
81-
'properties' => $properties,
82-
],
95+
'schema' => $schema,
8396
],
8497
];
8598

src/Generator/ResponsesCreator.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
use cebe\openapi\exceptions\TypeErrorException;
88
use cebe\openapi\spec\Response;
99
use cebe\openapi\spec\Responses;
10+
use cebe\openapi\spec\Schema;
1011
use Illuminate\Contracts\Config\Repository;
1112
use Illuminate\Contracts\Support\Arrayable;
1213
use Illuminate\Http\Request;
1314
use Illuminate\Http\Resources\Json\ResourceCollection;
1415
use Illuminate\Support\Arr;
16+
use Illuminate\Support\Str;
1517
use Intermax\LaravelOpenApi\Generator\Values\Value;
1618

1719
class ResponsesCreator
@@ -21,6 +23,7 @@ public function __construct(
2123
protected ResourceFactory $resourceFactory,
2224
protected ResourceAnalyser $resourceAnalyser,
2325
protected Repository $config,
26+
protected ComponentManager $componentManager,
2427
) {
2528
}
2629

@@ -29,15 +32,15 @@ public function __construct(
2932
*
3033
* @throws TypeErrorException
3134
*/
32-
public function createFromResource(string $className): Responses
35+
public function createFromResource(string $className, ?string $entityName = null): Responses
3336
{
3437
$mapping = $this->resourceAnalyser->retrieveMappingFromResource($className);
3538

3639
if ($mapping) {
3740
return $this->convertSchemaToResponse($mapping, $className);
3841
}
3942

40-
return $this->discoverResponse($className);
43+
return $this->discoverResponse($className, $entityName);
4144
}
4245

4346
/**
@@ -192,7 +195,7 @@ public function emptyResponse(): Responses
192195
*
193196
* @throws TypeErrorException
194197
*/
195-
public function discoverResponse(string $className): Responses
198+
public function discoverResponse(string $className, ?string $entityName): Responses
196199
{
197200
try {
198201
$resource = $this->resourceFactory->createFromClassName($className);
@@ -201,26 +204,37 @@ public function discoverResponse(string $className): Responses
201204
return $this->emptyResponse();
202205
}
203206

207+
/** @var array<int, mixed> $responseData */
204208
$responseData = $resource->toArray($this->request);
205209
} catch (\Throwable $e) {
206210
return $this->emptyResponse();
207211
}
208212

213+
if ($resource instanceof ResourceCollection) {
214+
$responseData = $responseData[0];
215+
}
216+
217+
$this->componentManager->addSchema(
218+
name: $entityName ?? Str::of($className)->replace('Resource', '')->toString(),
219+
schema: new Schema([
220+
'type' => 'object',
221+
'properties' => $this->createProperties($responseData),
222+
]),
223+
);
224+
209225
if ($resource instanceof ResourceCollection) {
210226
$schemaProperties = [
211227
'data' => [
212228
'type' => 'array',
213229
'items' => [
214-
'type' => 'object',
215-
'properties' => $this->createProperties($responseData[0]),
230+
'$ref' => '#/components/schemas/'.$entityName,
216231
],
217232
],
218233
];
219234
} else {
220235
$schemaProperties = [
221236
'data' => [
222-
'type' => 'object',
223-
'properties' => $this->createProperties($responseData),
237+
'$ref' => '#/components/schemas/'.$entityName,
224238
],
225239
];
226240
}

src/OpenApiServiceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Intermax\LaravelOpenApi;
44

55
use Illuminate\Support\ServiceProvider;
6+
use Intermax\LaravelOpenApi\Generator\ComponentManager;
67
use phpDocumentor\Reflection\DocBlockFactory;
78

89
class OpenApiServiceProvider extends ServiceProvider
@@ -25,5 +26,7 @@ public function register()
2526
$this->app->bind(DocBlockFactory::class, function () {
2627
return DocBlockFactory::createInstance();
2728
});
29+
30+
$this->app->scoped(ComponentManager::class);
2831
}
2932
}

tests/ResponseDiscoveryTest.php

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,13 @@ protected function assert200ResponseExists(array $spec): void
5959
$this->assertNotNull($spec['paths']['/things']['post']['responses']['200'] ?? null);
6060
}
6161

62-
/**
63-
* @param array<mixed> $spec
64-
* @return array<mixed>
65-
*/
66-
protected function getSchema(array $spec): ?array
67-
{
68-
return $spec['paths']['/things']['post']['responses']['200']['content']['application/json']['schema'];
69-
}
70-
7162
protected function assertNumberOfSubThingsIsInteger($spec): void
7263
{
7364
$this->assertEquals(
7465
'integer',
7566
Arr::get(
76-
$this->getSchema($spec),
77-
'properties.data.properties.attributes.properties.numberOfSubThings.type'
67+
$spec,
68+
'components.schemas.Thing.properties.attributes.properties.numberOfSubThings.type'
7869
)
7970
);
8071
}
@@ -84,8 +75,8 @@ protected function assertFractionalNumberIsNumber($spec): void
8475
$this->assertEquals(
8576
'number',
8677
Arr::get(
87-
$this->getSchema($spec),
88-
'properties.data.properties.attributes.properties.fractionalNumber.type'
78+
$spec,
79+
'components.schemas.Thing.properties.attributes.properties.fractionalNumber.type'
8980
)
9081
);
9182
}

0 commit comments

Comments
 (0)