Skip to content

Commit 2398d0e

Browse files
committed
feat: add webhook - openapi
1 parent ce0bb9e commit 2398d0e

File tree

6 files changed

+113
-16
lines changed

6 files changed

+113
-16
lines changed

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

@@ -43,7 +44,7 @@ public function __construct(
4344
array $paginationViaCursor = null,
4445
array $hydraContext = null,
4546
array $openapiContext = null,
46-
bool|OpenApiOperation $openapi = null,
47+
bool|OpenApiOperation|Webhook $openapi = null,
4748
array $exceptionToStatus = null,
4849
bool $queryParameterValidationEnabled = null,
4950
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;
@@ -146,7 +147,7 @@ public function __construct(
146147
protected ?array $paginationViaCursor = null,
147148
protected ?array $hydraContext = null,
148149
protected ?array $openapiContext = null, // TODO Remove in 4.0
149-
protected bool|OpenApiOperation|null $openapi = null,
150+
protected bool|OpenApiOperation|Webhook|null $openapi = null,
150151
protected ?array $exceptionToStatus = null,
151152
protected ?bool $queryParameterValidationEnabled = null,
152153
protected ?array $links = null,
@@ -561,12 +562,12 @@ public function withOpenapiContext(array $openapiContext): self
561562
return $self;
562563
}
563564

564-
public function getOpenapi(): bool|OpenApiOperation|null
565+
public function getOpenapi(): bool|OpenApiOperation|Webhook|null
565566
{
566567
return $this->openapi;
567568
}
568569

569-
public function withOpenapi(bool|OpenApiOperation $openapi): self
570+
public function withOpenapi(bool|OpenApiOperation|Webhook $openapi): self
570571
{
571572
$self = clone $this;
572573
$self->openapi = $openapi;

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

@@ -43,7 +44,7 @@ public function __construct(
4344
array $paginationViaCursor = null,
4445
array $hydraContext = null,
4546
array $openapiContext = null,
46-
bool|OpenApiOperation $openapi = null,
47+
bool|OpenApiOperation|Webhook $openapi = null,
4748
array $exceptionToStatus = null,
4849
bool $queryParameterValidationEnabled = null,
4950
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

+30-10
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;
@@ -89,12 +90,13 @@ public function __invoke(array $context = []): OpenApi
8990
$servers = '/' === $baseUrl || '' === $baseUrl ? [new Server('/')] : [new Server($baseUrl)];
9091
$paths = new Paths();
9192
$schemas = new \ArrayObject();
93+
$webhooks = new \ArrayObject();
9294

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

9698
foreach ($resourceMetadataCollection as $resourceMetadata) {
97-
$this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas);
99+
$this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas, $webhooks);
98100
}
99101
}
100102

@@ -118,11 +120,15 @@ public function __invoke(array $context = []): OpenApi
118120
new \ArrayObject(),
119121
new \ArrayObject($securitySchemes)
120122
),
121-
$securityRequirements
123+
$securityRequirements,
124+
[],
125+
null,
126+
null,
127+
$webhooks
122128
);
123129
}
124130

125-
private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas): void
131+
private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas, \ArrayObject $webhooks): void
126132
{
127133
if (0 === $resource->getOperations()->count()) {
128134
return;
@@ -135,10 +141,10 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
135141
continue;
136142
}
137143

138-
$openapiOperation = $operation->getOpenapi();
144+
$openapiAttribute = $operation->getOpenapi();
139145

140146
// Operation ignored from OpenApi
141-
if ($operation instanceof HttpOperation && false === $openapiOperation) {
147+
if ($operation instanceof HttpOperation && false === $openapiAttribute) {
142148
continue;
143149
}
144150

@@ -162,8 +168,15 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
162168
continue;
163169
}
164170

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

169182
// Complete with defaults
@@ -227,9 +240,9 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
227240
$openapiOperation = $openapiOperation->withDeprecated((bool) $operation->getOpenapiContext()['deprecated']);
228241
}
229242

230-
if ($path) {
243+
if (!$pathItem && $path) {
231244
$pathItem = $paths->getPath($path) ?: new PathItem();
232-
} else {
245+
} elseif (!$pathItem) {
233246
$pathItem = new PathItem();
234247
}
235248

@@ -384,7 +397,14 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
384397
}
385398
}
386399

387-
$paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
400+
if ($openapiAttribute instanceof Webhook) {
401+
if (!isset($webhooks[$openapiAttribute->getName()])) {
402+
$webhooks[$openapiAttribute->getName()] = new \ArrayObject();
403+
}
404+
$webhooks[$openapiAttribute->getName()]->append($pathItem->{'with'.ucfirst($method)}($openapiOperation));
405+
} else {
406+
$paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
407+
}
388408
}
389409
}
390410

@@ -510,7 +530,7 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection
510530
}
511531

512532
// Operation ignored from OpenApi
513-
if ($operation instanceof HttpOperation && false === $operation->getOpenapi()) {
533+
if ($operation instanceof HttpOperation && (false === $operation->getOpenapi() || $operation->getOpenapi() instanceof Webhook)) {
514534
continue;
515535
}
516536

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}'),
@@ -246,7 +255,7 @@ public function testInvoke(): void
246255
$resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class]));
247256

248257
$resourceCollectionMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
249-
$resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [$dummyResource]));
258+
$resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [$dummyResource, $dummyResourceWebhook]));
250259

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

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

0 commit comments

Comments
 (0)