-
Notifications
You must be signed in to change notification settings - Fork 11
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
feat: add Webhook component integration #120
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
## Using the Symfony Webhook component | ||
|
||
Symfony provides a specific [Webhook component](https://symfony.com/doc/current/webhook.html) dedicated to this task. | ||
|
||
Its role is to parse requests related to known webhooks and dispatch a corresponding remote event. Then, this event can | ||
be handled by your application through the [Messenger component](https://symfony.com/doc/current/messenger.html). | ||
|
||
The GotenbergBundle offers a native integration of this component if installed. | ||
|
||
### Usage | ||
|
||
To connect the provider to your application, you need to configure the Webhook component routing: | ||
|
||
```yaml | ||
# config/packages/webhook.yaml | ||
framework: | ||
webhook: | ||
routing: | ||
gotenberg: | ||
service: 'sensiolabs_gotenberg.webhook.request_parser' | ||
``` | ||
Jean-Beru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Then, create your handler to respond to the Gotenberg RemoteEvent: | ||
|
||
```php | ||
use Sensiolabs\GotenbergBundle\RemoteEvent\ErrorGotenbergEvent; | ||
use Sensiolabs\GotenbergBundle\RemoteEvent\SuccessGotenbergEvent; | ||
use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface; | ||
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; | ||
use Symfony\Component\RemoteEvent\RemoteEvent; | ||
|
||
#[AsRemoteEventConsumer('gotenberg')] | ||
class WebhookListener implements ConsumerInterface | ||
{ | ||
public function consume(RemoteEvent $event): void | ||
{ | ||
if ($event instanceof SuccessGotenbergEvent) { | ||
// Handle the event | ||
// PDF content is available as a resource through the getFile() method | ||
} elseif ($event instanceof ErrorGotenbergEvent) { | ||
// Handle the error | ||
} | ||
} | ||
} | ||
``` | ||
|
||
> [!WARNING] | ||
> The webhook component **won't be used** if a [native webhook configuration](native.md) is set. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# Going async | ||
|
||
* [Native](./async/native.md) | ||
* [Using the Symfony Webhook component](./async/webhook.md) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,9 @@ | |
|
||
namespace Sensiolabs\GotenbergBundle\Builder; | ||
|
||
use Sensiolabs\GotenbergBundle\Exception\MissingRequiredFieldException; | ||
use Sensiolabs\GotenbergBundle\Exception\WebhookConfigurationException; | ||
use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; | ||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||
|
||
trait AsyncBuilderTrait | ||
{ | ||
|
@@ -30,16 +31,22 @@ trait AsyncBuilderTrait | |
|
||
private WebhookConfigurationRegistryInterface $webhookConfigurationRegistry; | ||
|
||
protected UrlGeneratorInterface|null $urlGenerator; | ||
|
||
public function generateAsync(): void | ||
{ | ||
if (null === $this->successWebhookUrl) { | ||
throw new MissingRequiredFieldException('->webhookUrl() was never called.'); | ||
} | ||
$successWebhookUrl = $this->successWebhookUrl; | ||
if (!$successWebhookUrl) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. strict ? |
||
if (!$this->urlGenerator) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. strict ? |
||
throw new WebhookConfigurationException(\sprintf('A webhook URL or Router is required to use "%s" method. Set the URL or try to run "composer require symfony/routing".', __METHOD__)); | ||
} | ||
|
||
$errorWebhookUrl = $this->errorWebhookUrl ?? $this->successWebhookUrl; | ||
$successWebhookUrl = $this->urlGenerator->generate('_webhook_controller', ['type' => 'gotenberg'], UrlGeneratorInterface::ABSOLUTE_URL); | ||
} | ||
$errorWebhookUrl = $this->errorWebhookUrl ?? $successWebhookUrl; | ||
|
||
$headers = [ | ||
'Gotenberg-Webhook-Url' => $this->successWebhookUrl, | ||
'Gotenberg-Webhook-Url' => $successWebhookUrl, | ||
'Gotenberg-Webhook-Error-Url' => $errorWebhookUrl, | ||
]; | ||
|
||
|
@@ -59,6 +66,7 @@ public function generateAsync(): void | |
// Gotenberg will add the extension to the file name (e.g. filename : "file.pdf" => generated file : "file.pdf.pdf"). | ||
$headers['Gotenberg-Output-Filename'] = $this->fileName; | ||
} | ||
|
||
$this->client->call($this->getEndpoint(), $this->getMultipartFormData(), $headers); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Component\Mime\Part\DataPart; | ||
use Symfony\Component\Mime\Part\File as DataPartFile; | ||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||
use Twig\Environment; | ||
|
||
abstract class AbstractChromiumPdfBuilder extends AbstractPdfBuilder | ||
|
@@ -30,9 +31,10 @@ public function __construct( | |
AssetBaseDirFormatter $asset, | ||
WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, | ||
private readonly RequestStack $requestStack, | ||
UrlGeneratorInterface|null $urlGenerator = null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. better to be consistent with parent construct. I suggest to switch with the request stack. |
||
private readonly Environment|null $twig = null, | ||
) { | ||
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry); | ||
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $urlGenerator); | ||
|
||
$normalizers = [ | ||
'extraHttpHeaders' => function (mixed $value): array { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Component\Mime\Part\DataPart; | ||
use Symfony\Component\Mime\Part\File as DataPartFile; | ||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||
use Twig\Environment; | ||
|
||
abstract class AbstractChromiumScreenshotBuilder extends AbstractScreenshotBuilder | ||
|
@@ -27,9 +28,10 @@ public function __construct( | |
AssetBaseDirFormatter $asset, | ||
WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, | ||
private readonly RequestStack $requestStack, | ||
UrlGeneratorInterface|null $urlGenerator = null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. switch with request stack |
||
private readonly Environment|null $twig = null, | ||
) { | ||
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry); | ||
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $urlGenerator); | ||
|
||
$normalizers = [ | ||
'extraHttpHeaders' => function (mixed $value): array { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
namespace Sensiolabs\GotenbergBundle\RemoteEvent; | ||
|
||
use Symfony\Component\RemoteEvent\RemoteEvent; | ||
|
||
class ErrorGotenbergEvent extends RemoteEvent | ||
{ | ||
public const ERROR = 'error'; | ||
|
||
/** | ||
* @param array{status: int, message: string} $payload | ||
*/ | ||
public function __construct( | ||
string $id, | ||
array $payload, | ||
private readonly int $status, | ||
private readonly string $message, | ||
) { | ||
parent::__construct(self::ERROR, $id, $payload); | ||
} | ||
|
||
public function getStatus(): int | ||
{ | ||
return $this->status; | ||
} | ||
|
||
public function getMessage(): string | ||
{ | ||
return $this->message; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still no desire to clean this file ? 👼
Or... even better: introduce a BuilderFactory ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to clean this file. I did it in a PoC about refactoring builder: #118
I also planned to test that 😉