Skip to content

Commit 125f2ce

Browse files
alli83soyuka
andauthored
feat: add webhook - openapi (#5873)
* feat: add webhook - openapi * cs --------- Co-authored-by: Antoine Bluchet <[email protected]>
1 parent 5523bf5 commit 125f2ce

File tree

14 files changed

+181
-21
lines changed

14 files changed

+181
-21
lines changed

features/openapi/docs.feature

+5
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Feature: Documentation support
7070
And the OpenAPI class "UuidIdentifierDummy" exists
7171
And the OpenAPI class "ThirdLevel" exists
7272
And the OpenAPI class "DummyCar" exists
73+
And the OpenAPI class "DummyWebhook" exists
7374
And the OpenAPI class "ParentDummy" doesn't exist
7475
And the OpenAPI class "UnknownDummy" doesn't exist
7576
And the OpenAPI path "/relation_embedders/{id}/custom" exists
@@ -115,6 +116,10 @@ Feature: Documentation support
115116
And the JSON node "paths./dummy_cars.get.parameters[8].name" should be equal to "foobar[]"
116117
And the JSON node "paths./dummy_cars.get.parameters[8].description" should be equal to "Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: foobar[]={propertyName}&foobar[]={anotherPropertyName}&foobar[{nestedPropertyParent}][]={nestedProperty}"
117118

119+
# Webhook
120+
And the JSON node "webhooks.webhook[0].get.description" should be equal to "Something else here for example"
121+
And the JSON node "webhooks.webhook[1].post.description" should be equal to "Hi! it's me, I'm the problem, it's me"
122+
118123
# Subcollection - check filter on subResource
119124
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].name" should be equal to "id"
120125
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].in" should be equal to "path"

src/Metadata/Delete.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819

@@ -44,7 +45,7 @@ public function __construct(
4445
?array $paginationViaCursor = null,
4546
?array $hydraContext = null,
4647
?array $openapiContext = null,
47-
bool|OpenApiOperation|null $openapi = null,
48+
bool|OpenApiOperation|Webhook|null $openapi = null,
4849
?array $exceptionToStatus = null,
4950
?bool $queryParameterValidationEnabled = null,
5051
?array $links = null,

src/Metadata/Error.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819

@@ -44,7 +45,7 @@ public function __construct(
4445
?array $paginationViaCursor = null,
4546
?array $hydraContext = null,
4647
?array $openapiContext = null,
47-
bool|OpenApiOperation|null $openapi = null,
48+
bool|OpenApiOperation|Webhook|null $openapi = null,
4849
?array $exceptionToStatus = null,
4950
?bool $queryParameterValidationEnabled = null,
5051
?array $links = null,

src/Metadata/Get.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819

@@ -44,7 +45,7 @@ public function __construct(
4445
?array $paginationViaCursor = null,
4546
?array $hydraContext = null,
4647
?array $openapiContext = null,
47-
bool|OpenApiOperation|null $openapi = null,
48+
bool|OpenApiOperation|Webhook|null $openapi = null,
4849
?array $exceptionToStatus = null,
4950
?bool $queryParameterValidationEnabled = null,
5051
?array $links = null,

src/Metadata/GetCollection.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819

@@ -44,7 +45,7 @@ public function __construct(
4445
?array $paginationViaCursor = null,
4546
?array $hydraContext = null,
4647
?array $openapiContext = null,
47-
bool|OpenApiOperation|null $openapi = null,
48+
bool|OpenApiOperation|Webhook|null $openapi = null,
4849
?array $exceptionToStatus = null,
4950
?bool $queryParameterValidationEnabled = null,
5051
?array $links = null,

src/Metadata/HttpOperation.php

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819
use Symfony\Component\WebLink\Link as WebLink;
@@ -150,7 +151,7 @@ public function __construct(
150151
protected ?array $paginationViaCursor = null,
151152
protected ?array $hydraContext = null,
152153
protected ?array $openapiContext = null, // TODO Remove in 4.0
153-
protected bool|OpenApiOperation|null $openapi = null,
154+
protected bool|OpenApiOperation|Webhook|null $openapi = null,
154155
protected ?array $exceptionToStatus = null,
155156
protected ?bool $queryParameterValidationEnabled = null,
156157
protected ?array $links = null,
@@ -578,12 +579,12 @@ public function withOpenapiContext(array $openapiContext): self
578579
return $self;
579580
}
580581

581-
public function getOpenapi(): bool|OpenApiOperation|null
582+
public function getOpenapi(): bool|OpenApiOperation|Webhook|null
582583
{
583584
return $this->openapi;
584585
}
585586

586-
public function withOpenapi(bool|OpenApiOperation $openapi): self
587+
public function withOpenapi(bool|OpenApiOperation|Webhook $openapi): self
587588
{
588589
$self = clone $this;
589590
$self->openapi = $openapi;

src/Metadata/NotExposed.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819

@@ -56,7 +57,7 @@ public function __construct(
5657

5758
?array $hydraContext = null,
5859
?array $openapiContext = null,
59-
bool|OpenApiOperation|null $openapi = false,
60+
bool|OpenApiOperation|Webhook|null $openapi = false,
6061
?array $exceptionToStatus = null,
6162

6263
?bool $queryParameterValidationEnabled = null,

src/Metadata/Patch.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819

@@ -44,7 +45,7 @@ public function __construct(
4445
?array $paginationViaCursor = null,
4546
?array $hydraContext = null,
4647
?array $openapiContext = null,
47-
bool|OpenApiOperation|null $openapi = null,
48+
bool|OpenApiOperation|Webhook|null $openapi = null,
4849
?array $exceptionToStatus = null,
4950
?bool $queryParameterValidationEnabled = null,
5051
?array $links = null,

src/Metadata/Post.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819

@@ -44,7 +45,7 @@ public function __construct(
4445
?array $paginationViaCursor = null,
4546
?array $hydraContext = null,
4647
?array $openapiContext = null,
47-
bool|OpenApiOperation|null $openapi = null,
48+
bool|OpenApiOperation|Webhook|null $openapi = null,
4849
?array $exceptionToStatus = null,
4950
?bool $queryParameterValidationEnabled = null,
5051
?array $links = null,

src/Metadata/Put.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\OpenApi\Attributes\Webhook;
1617
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1718
use ApiPlatform\State\OptionsInterface;
1819

@@ -44,7 +45,7 @@ public function __construct(
4445
?array $paginationViaCursor = null,
4546
?array $hydraContext = null,
4647
?array $openapiContext = null,
47-
bool|OpenApiOperation|null $openapi = null,
48+
bool|OpenApiOperation|Webhook|null $openapi = null,
4849
?array $exceptionToStatus = null,
4950
?bool $queryParameterValidationEnabled = null,
5051
?array $links = null,

src/OpenApi/Attributes/Webhook.php

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\OpenApi\Attributes;
15+
16+
use ApiPlatform\OpenApi\Model\PathItem;
17+
18+
class Webhook
19+
{
20+
public function __construct(
21+
protected string $name,
22+
protected ?PathItem $pathItem = null,
23+
) {
24+
}
25+
26+
public function getName(): string
27+
{
28+
return $this->name;
29+
}
30+
31+
public function withName(string $name): self
32+
{
33+
$self = clone $this;
34+
$self->name = $name;
35+
36+
return $self;
37+
}
38+
39+
public function getPathItem(): ?PathItem
40+
{
41+
return $this->pathItem;
42+
}
43+
44+
public function withPathItem(PathItem $pathItem): self
45+
{
46+
$self = clone $this;
47+
$self->pathItem = $pathItem;
48+
49+
return $self;
50+
}
51+
}

src/OpenApi/Factory/OpenApiFactory.php

+29-9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2727
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
2828
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
29+
use ApiPlatform\OpenApi\Attributes\Webhook;
2930
use ApiPlatform\OpenApi\Model;
3031
use ApiPlatform\OpenApi\Model\Components;
3132
use ApiPlatform\OpenApi\Model\Contact;
@@ -90,12 +91,13 @@ public function __invoke(array $context = []): OpenApi
9091
$servers = '/' === $baseUrl || '' === $baseUrl ? [new Server('/')] : [new Server($baseUrl)];
9192
$paths = new Paths();
9293
$schemas = new \ArrayObject();
94+
$webhooks = new \ArrayObject();
9395

9496
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
9597
$resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
9698

9799
foreach ($resourceMetadataCollection as $resourceMetadata) {
98-
$this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas);
100+
$this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas, $webhooks);
99101
}
100102
}
101103

@@ -119,11 +121,15 @@ public function __invoke(array $context = []): OpenApi
119121
new \ArrayObject(),
120122
new \ArrayObject($securitySchemes)
121123
),
122-
$securityRequirements
124+
$securityRequirements,
125+
[],
126+
null,
127+
null,
128+
$webhooks
123129
);
124130
}
125131

126-
private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas): void
132+
private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas, \ArrayObject $webhooks): void
127133
{
128134
if (0 === $resource->getOperations()->count()) {
129135
return;
@@ -136,10 +142,10 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
136142
continue;
137143
}
138144

139-
$openapiOperation = $operation->getOpenapi();
145+
$openapiAttribute = $operation->getOpenapi();
140146

141147
// Operation ignored from OpenApi
142-
if ($operation instanceof HttpOperation && false === $openapiOperation) {
148+
if ($operation instanceof HttpOperation && false === $openapiAttribute) {
143149
continue;
144150
}
145151

@@ -163,8 +169,15 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
163169
continue;
164170
}
165171

166-
if (!\is_object($openapiOperation)) {
172+
$pathItem = null;
173+
174+
if ($openapiAttribute instanceof Webhook) {
175+
$pathItem = $openapiAttribute->getPathItem() ?: new PathItem();
176+
$openapiOperation = $pathItem->{'get'.ucfirst(strtolower($method))}() ?: new Model\Operation();
177+
} elseif (!\is_object($openapiAttribute)) {
167178
$openapiOperation = new Model\Operation();
179+
} else {
180+
$openapiOperation = $openapiAttribute;
168181
}
169182

170183
// Complete with defaults
@@ -230,7 +243,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
230243

231244
if ($path) {
232245
$pathItem = $paths->getPath($path) ?: new PathItem();
233-
} else {
246+
} elseif (!$pathItem) {
234247
$pathItem = new PathItem();
235248
}
236249

@@ -391,7 +404,14 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
391404
}
392405
}
393406

394-
$paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
407+
if ($openapiAttribute instanceof Webhook) {
408+
if (!isset($webhooks[$openapiAttribute->getName()])) {
409+
$webhooks[$openapiAttribute->getName()] = new \ArrayObject();
410+
}
411+
$webhooks[$openapiAttribute->getName()]->append($pathItem->{'with'.ucfirst($method)}($openapiOperation));
412+
} else {
413+
$paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
414+
}
395415
}
396416
}
397417

@@ -517,7 +537,7 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection
517537
}
518538

519539
// Operation ignored from OpenApi
520-
if ($operation instanceof HttpOperation && false === $operation->getOpenapi()) {
540+
if ($operation instanceof HttpOperation && (false === $operation->getOpenapi() || $operation->getOpenapi() instanceof Webhook)) {
521541
continue;
522542
}
523543

src/OpenApi/Tests/Factory/OpenApiFactoryTest.php

+24-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
3535
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
3636
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
37+
use ApiPlatform\OpenApi\Attributes\Webhook;
3738
use ApiPlatform\OpenApi\Factory\OpenApiFactory;
3839
use ApiPlatform\OpenApi\Model;
3940
use ApiPlatform\OpenApi\Model\Components;
@@ -79,6 +80,14 @@ public function testInvoke(): void
7980
$baseOperation = (new HttpOperation())->withTypes(['http://schema.example.com/Dummy'])->withInputFormats(self::OPERATION_FORMATS['input_formats'])->withOutputFormats(self::OPERATION_FORMATS['output_formats'])->withClass(Dummy::class)->withOutput([
8081
'class' => OutputDto::class,
8182
])->withPaginationClientItemsPerPage(true)->withShortName('Dummy')->withDescription('This is a dummy');
83+
$dummyResourceWebhook = (new ApiResource())->withOperations(new Operations([
84+
'dummy webhook' => (new Get())->withUriTemplate('/dummy/{id}')->withShortName('short')->withOpenapi(new Webhook('happy webhook')),
85+
'an other dummy webhook' => (new Post())->withUriTemplate('/dummies')->withShortName('short something')->withOpenapi(new Webhook('happy webhook', new Model\PathItem(post: new Operation(
86+
summary: 'well...',
87+
description: 'I dont\'t know what to say',
88+
)))),
89+
]));
90+
8291
$dummyResource = (new ApiResource())->withOperations(new Operations([
8392
'ignored' => new NotExposed(),
8493
'ignoredWithUriTemplate' => (new NotExposed())->withUriTemplate('/dummies/{id}'),
@@ -247,7 +256,7 @@ public function testInvoke(): void
247256
$resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class]));
248257

249258
$resourceCollectionMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
250-
$resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [$dummyResource]));
259+
$resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [$dummyResource, $dummyResourceWebhook]));
251260

252261
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
253262
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate', 'enum']));
@@ -482,6 +491,20 @@ public function testInvoke(): void
482491
$this->assertEquals($openApi->getInfo(), new Info('Test API', '1.2.3', 'This is a test API.'));
483492
$this->assertEquals($openApi->getServers(), [new Server('/app_dev.php/')]);
484493

494+
$webhooks = $openApi->getWebhooks();
495+
$this->assertCount(1, $webhooks);
496+
497+
$this->assertNotNull($webhooks['happy webhook']);
498+
$this->assertCount(2, $webhooks['happy webhook']);
499+
500+
$firstOperationWebhook = $webhooks['happy webhook'][0];
501+
$secondOperationWebhook = $webhooks['happy webhook'][1];
502+
503+
$this->assertSame('dummy webhook', $firstOperationWebhook->getGet()->getOperationId());
504+
$this->assertSame('an other dummy webhook', $secondOperationWebhook->getPost()->getOperationId());
505+
$this->assertSame('I dont\'t know what to say', $secondOperationWebhook->getPost()->getDescription());
506+
$this->assertSame('well...', $secondOperationWebhook->getPost()->getSummary());
507+
485508
$components = $openApi->getComponents();
486509
$this->assertInstanceOf(Components::class, $components);
487510

0 commit comments

Comments
 (0)