Skip to content

feat: add webhook - openapi #5873

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions features/openapi/docs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Feature: Documentation support
And the OpenAPI class "UuidIdentifierDummy" exists
And the OpenAPI class "ThirdLevel" exists
And the OpenAPI class "DummyCar" exists
And the OpenAPI class "DummyWebhook" exists
And the OpenAPI class "ParentDummy" doesn't exist
And the OpenAPI class "UnknownDummy" doesn't exist
And the OpenAPI path "/relation_embedders/{id}/custom" exists
Expand Down Expand Up @@ -115,6 +116,10 @@ Feature: Documentation support
And the JSON node "paths./dummy_cars.get.parameters[8].name" should be equal to "foobar[]"
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}"

# Webhook
And the JSON node "webhooks.webhook[0].get.description" should be equal to "Something else here for example"
And the JSON node "webhooks.webhook[1].post.description" should be equal to "Hi! it's me, I'm the problem, it's me"

# Subcollection - check filter on subResource
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].name" should be equal to "id"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].in" should be equal to "path"
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Get.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/GetCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
7 changes: 4 additions & 3 deletions src/Metadata/HttpOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;
use Symfony\Component\WebLink\Link as WebLink;
Expand Down Expand Up @@ -150,7 +151,7 @@ public function __construct(
protected ?array $paginationViaCursor = null,
protected ?array $hydraContext = null,
protected ?array $openapiContext = null, // TODO Remove in 4.0
protected bool|OpenApiOperation|null $openapi = null,
protected bool|OpenApiOperation|Webhook|null $openapi = null,
protected ?array $exceptionToStatus = null,
protected ?bool $queryParameterValidationEnabled = null,
protected ?array $links = null,
Expand Down Expand Up @@ -578,12 +579,12 @@ public function withOpenapiContext(array $openapiContext): self
return $self;
}

public function getOpenapi(): bool|OpenApiOperation|null
public function getOpenapi(): bool|OpenApiOperation|Webhook|null
{
return $this->openapi;
}

public function withOpenapi(bool|OpenApiOperation $openapi): self
public function withOpenapi(bool|OpenApiOperation|Webhook $openapi): self
{
$self = clone $this;
$self->openapi = $openapi;
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/NotExposed.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -56,7 +57,7 @@ public function __construct(

?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = false,
bool|OpenApiOperation|Webhook|null $openapi = false,
?array $exceptionToStatus = null,

?bool $queryParameterValidationEnabled = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Patch.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Put.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
51 changes: 51 additions & 0 deletions src/OpenApi/Attributes/Webhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\OpenApi\Attributes;

use ApiPlatform\OpenApi\Model\PathItem;

class Webhook
{
public function __construct(
protected string $name,
protected ?PathItem $pathItem = null,
) {
}

public function getName(): string
{
return $this->name;
}

public function withName(string $name): self
{
$self = clone $this;
$self->name = $name;

return $self;
}

public function getPathItem(): ?PathItem
{
return $this->pathItem;
}

public function withPathItem(PathItem $pathItem): self
{
$self = clone $this;
$self->pathItem = $pathItem;

return $self;
}
}
38 changes: 29 additions & 9 deletions src/OpenApi/Factory/OpenApiFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\Model\Components;
use ApiPlatform\OpenApi\Model\Contact;
Expand Down Expand Up @@ -90,12 +91,13 @@ public function __invoke(array $context = []): OpenApi
$servers = '/' === $baseUrl || '' === $baseUrl ? [new Server('/')] : [new Server($baseUrl)];
$paths = new Paths();
$schemas = new \ArrayObject();
$webhooks = new \ArrayObject();

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

foreach ($resourceMetadataCollection as $resourceMetadata) {
$this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas);
$this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas, $webhooks);
}
}

Expand All @@ -119,11 +121,15 @@ public function __invoke(array $context = []): OpenApi
new \ArrayObject(),
new \ArrayObject($securitySchemes)
),
$securityRequirements
$securityRequirements,
[],
null,
null,
$webhooks
);
}

private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas): void
private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas, \ArrayObject $webhooks): void
{
if (0 === $resource->getOperations()->count()) {
return;
Expand All @@ -136,10 +142,10 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
continue;
}

$openapiOperation = $operation->getOpenapi();
$openapiAttribute = $operation->getOpenapi();

// Operation ignored from OpenApi
if ($operation instanceof HttpOperation && false === $openapiOperation) {
if ($operation instanceof HttpOperation && false === $openapiAttribute) {
continue;
}

Expand All @@ -163,8 +169,15 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
continue;
}

if (!\is_object($openapiOperation)) {
$pathItem = null;

if ($openapiAttribute instanceof Webhook) {
$pathItem = $openapiAttribute->getPathItem() ?: new PathItem();
$openapiOperation = $pathItem->{'get'.ucfirst(strtolower($method))}() ?: new Model\Operation();
} elseif (!\is_object($openapiAttribute)) {
$openapiOperation = new Model\Operation();
} else {
$openapiOperation = $openapiAttribute;
}

// Complete with defaults
Expand Down Expand Up @@ -230,7 +243,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection

if ($path) {
$pathItem = $paths->getPath($path) ?: new PathItem();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will update: just $path should be enough

} else {
} elseif (!$pathItem) {
$pathItem = new PathItem();
}

Expand Down Expand Up @@ -391,7 +404,14 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
}
}

$paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
if ($openapiAttribute instanceof Webhook) {
if (!isset($webhooks[$openapiAttribute->getName()])) {
$webhooks[$openapiAttribute->getName()] = new \ArrayObject();
}
$webhooks[$openapiAttribute->getName()]->append($pathItem->{'with'.ucfirst($method)}($openapiOperation));
} else {
$paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
}
}
}

Expand Down Expand Up @@ -517,7 +537,7 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection
}

// Operation ignored from OpenApi
if ($operation instanceof HttpOperation && false === $operation->getOpenapi()) {
if ($operation instanceof HttpOperation && (false === $operation->getOpenapi() || $operation->getOpenapi() instanceof Webhook)) {
continue;
}

Expand Down
25 changes: 24 additions & 1 deletion src/OpenApi/Tests/Factory/OpenApiFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Factory\OpenApiFactory;
use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\Model\Components;
Expand Down Expand Up @@ -79,6 +80,14 @@ public function testInvoke(): void
$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([
'class' => OutputDto::class,
])->withPaginationClientItemsPerPage(true)->withShortName('Dummy')->withDescription('This is a dummy');
$dummyResourceWebhook = (new ApiResource())->withOperations(new Operations([
'dummy webhook' => (new Get())->withUriTemplate('/dummy/{id}')->withShortName('short')->withOpenapi(new Webhook('happy webhook')),
'an other dummy webhook' => (new Post())->withUriTemplate('/dummies')->withShortName('short something')->withOpenapi(new Webhook('happy webhook', new Model\PathItem(post: new Operation(
summary: 'well...',
description: 'I dont\'t know what to say',
)))),
]));

$dummyResource = (new ApiResource())->withOperations(new Operations([
'ignored' => new NotExposed(),
'ignoredWithUriTemplate' => (new NotExposed())->withUriTemplate('/dummies/{id}'),
Expand Down Expand Up @@ -247,7 +256,7 @@ public function testInvoke(): void
$resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class]));

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

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

$webhooks = $openApi->getWebhooks();
$this->assertCount(1, $webhooks);

$this->assertNotNull($webhooks['happy webhook']);
$this->assertCount(2, $webhooks['happy webhook']);

$firstOperationWebhook = $webhooks['happy webhook'][0];
$secondOperationWebhook = $webhooks['happy webhook'][1];

$this->assertSame('dummy webhook', $firstOperationWebhook->getGet()->getOperationId());
$this->assertSame('an other dummy webhook', $secondOperationWebhook->getPost()->getOperationId());
$this->assertSame('I dont\'t know what to say', $secondOperationWebhook->getPost()->getDescription());
$this->assertSame('well...', $secondOperationWebhook->getPost()->getSummary());

$components = $openApi->getComponents();
$this->assertInstanceOf(Components::class, $components);

Expand Down
Loading
Loading