Skip to content

Commit

Permalink
feat: add Webhook component integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Beru committed Nov 21, 2024
1 parent ee33766 commit c5809a8
Show file tree
Hide file tree
Showing 35 changed files with 462 additions and 54 deletions.
6 changes: 6 additions & 0 deletions composer-dependency-analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@
->addPathToScan(__DIR__.'/src/Debug', isDev: true)
->addPathToScan(__DIR__.'/tests', isDev: true)

->ignoreErrorsOnPackage('symfony/remote-event', [
ErrorType::DEV_DEPENDENCY_IN_PROD,
])
->ignoreErrorsOnPackage('symfony/routing', [
ErrorType::DEV_DEPENDENCY_IN_PROD,
])
->ignoreErrorsOnPackage('symfony/webhook', [
ErrorType::DEV_DEPENDENCY_IN_PROD,
])
->ignoreErrorsOnPackage('twig/twig', [
ErrorType::DEV_DEPENDENCY_IN_PROD,
])
Expand Down
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
"symfony/framework-bundle": "^6.4 || ^7.0",
"symfony/http-client": "^6.4 || ^7.0",
"symfony/monolog-bundle": "^3.10",
"symfony/remote-event": "^6.4 || ^7.0",
"symfony/routing": "^6.4 || ^7.0",
"symfony/stopwatch": "^6.4 || ^7.0",
"symfony/twig-bundle": "^6.4 || ^7.0",
"symfony/var-dumper": "^6.4 || ^7.0",
"symfony/webhook": "^6.4 || ^7.0",
"twig/twig": "^3.14"
},
"config": {
Expand All @@ -56,7 +58,9 @@
},
"suggest": {
"symfony/monolog-bundle": "Enables logging througout the generating process.",
"symfony/twig-bundle": "Allows you to use Twig to render templates into PDF",
"symfony/remote-event": "Allows you to use Webhook to handle asynchronous generation.",
"symfony/twig-bundle": "Allows you to use Twig to render templates into PDF.",
"symfony/webhook": "Allows you to use Webhook to handle asynchronous generation.",
"monolog/monolog": "Enables logging througout the generating process."
}
}
4 changes: 3 additions & 1 deletion config/builder_pdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
service('.sensiolabs_gotenberg.webhook_configuration_registry'),
service('request_stack'),
service('router')->nullOnInvalid(),
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
Expand All @@ -36,8 +37,8 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
service('.sensiolabs_gotenberg.webhook_configuration_registry'),
service('request_stack'),
service('twig')->nullOnInvalid(),
service('router')->nullOnInvalid(),
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setRequestContext', [service('.sensiolabs_gotenberg.request_context')->nullOnInvalid()])
Expand All @@ -51,6 +52,7 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
service('.sensiolabs_gotenberg.webhook_configuration_registry'),
service('request_stack'),
service('router')->nullOnInvalid(),
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
Expand Down
4 changes: 3 additions & 1 deletion config/builder_screenshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
service('.sensiolabs_gotenberg.webhook_configuration_registry'),
service('request_stack'),
service('router')->nullOnInvalid(),
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
Expand All @@ -33,8 +34,8 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
service('.sensiolabs_gotenberg.webhook_configuration_registry'),
service('request_stack'),
service('twig')->nullOnInvalid(),
service('router')->nullOnInvalid(),
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setRequestContext', [service('.sensiolabs_gotenberg.request_context')->nullOnInvalid()])
Expand All @@ -48,6 +49,7 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
service('.sensiolabs_gotenberg.webhook_configuration_registry'),
service('request_stack'),
service('router')->nullOnInvalid(),
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
Expand Down
48 changes: 48 additions & 0 deletions docs/async/webhook.md
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'
```
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.
1 change: 1 addition & 0 deletions docs/webhook.md
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)
20 changes: 14 additions & 6 deletions src/Builder/AsyncBuilderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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) {
if (!$this->urlGenerator) {
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,
];

Expand All @@ -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);
}

Expand Down
4 changes: 3 additions & 1 deletion src/Builder/Pdf/AbstractChromiumPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,9 +31,10 @@ public function __construct(
AssetBaseDirFormatter $asset,
WebhookConfigurationRegistryInterface $webhookConfigurationRegistry,
private readonly RequestStack $requestStack,
UrlGeneratorInterface|null $urlGenerator = null,
private readonly Environment|null $twig = null,
) {
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry);
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $urlGenerator);

$normalizers = [
'extraHttpHeaders' => function (mixed $value): array {
Expand Down
3 changes: 3 additions & 0 deletions src/Builder/Pdf/AbstractPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter;
use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

abstract class AbstractPdfBuilder implements PdfBuilderInterface, AsyncBuilderInterface
{
Expand All @@ -20,10 +21,12 @@ public function __construct(
GotenbergClientInterface $gotenbergClient,
AssetBaseDirFormatter $asset,
WebhookConfigurationRegistryInterface $webhookConfigurationRegistry,
UrlGeneratorInterface|null $urlGenerator = null,
) {
$this->client = $gotenbergClient;
$this->asset = $asset;
$this->webhookConfigurationRegistry = $webhookConfigurationRegistry;
$this->urlGenerator = $urlGenerator;

$this->normalizers = [
'metadata' => function (mixed $value): array {
Expand Down
4 changes: 2 additions & 2 deletions src/Builder/Pdf/UrlPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ public function __construct(
AssetBaseDirFormatter $asset,
WebhookConfigurationRegistryInterface $webhookConfigurationRegistry,
RequestStack $requestStack,
UrlGeneratorInterface|null $urlGenerator = null,
Environment|null $twig = null,
private readonly UrlGeneratorInterface|null $urlGenerator = null,
) {
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $requestStack, $twig);
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $requestStack, $urlGenerator, $twig);

$this->addNormalizer('route', $this->generateUrlFromRoute(...));
}
Expand Down
4 changes: 3 additions & 1 deletion src/Builder/Screenshot/AbstractChromiumScreenshotBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,9 +28,10 @@ public function __construct(
AssetBaseDirFormatter $asset,
WebhookConfigurationRegistryInterface $webhookConfigurationRegistry,
private readonly RequestStack $requestStack,
UrlGeneratorInterface|null $urlGenerator = null,
private readonly Environment|null $twig = null,
) {
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry);
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $urlGenerator);

$normalizers = [
'extraHttpHeaders' => function (mixed $value): array {
Expand Down
3 changes: 3 additions & 0 deletions src/Builder/Screenshot/AbstractScreenshotBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter;
use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

abstract class AbstractScreenshotBuilder implements ScreenshotBuilderInterface, AsyncBuilderInterface
{
Expand All @@ -20,10 +21,12 @@ public function __construct(
GotenbergClientInterface $gotenbergClient,
AssetBaseDirFormatter $asset,
WebhookConfigurationRegistryInterface $webhookConfigurationRegistry,
UrlGeneratorInterface|null $urlGenerator = null,
) {
$this->client = $gotenbergClient;
$this->asset = $asset;
$this->webhookConfigurationRegistry = $webhookConfigurationRegistry;
$this->urlGenerator = $urlGenerator;

$this->normalizers = [
'downloadFrom' => fn (array $value): array => $this->downloadFromNormalizer($value, $this->encodeData(...)),
Expand Down
4 changes: 2 additions & 2 deletions src/Builder/Screenshot/UrlScreenshotBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ public function __construct(
AssetBaseDirFormatter $asset,
WebhookConfigurationRegistryInterface $webhookConfigurationRegistry,
RequestStack $requestStack,
UrlGeneratorInterface|null $urlGenerator = null,
Environment|null $twig = null,
private readonly UrlGeneratorInterface|null $urlGenerator = null,
) {
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $requestStack, $twig);
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $requestStack, $urlGenerator, $twig);

$this->addNormalizer('route', $this->generateUrlFromRoute(...));
}
Expand Down
16 changes: 3 additions & 13 deletions src/Client/GotenbergResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Sensiolabs\GotenbergBundle\Client;

use Sensiolabs\GotenbergBundle\Utils\HeaderUtils;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;

Expand Down Expand Up @@ -31,25 +32,14 @@ public function getHeaders(): ResponseHeaderBag

public function getFileName(): string|null
{
$disposition = $this->headers->get('content-disposition', '');
/* @see https://onlinephp.io/c/c2606 */
if (1 === preg_match('#[^;]*;\sfilename="?(?P<fileName>[^"]*)"?#', $disposition, $matches)) {
return $matches['fileName'];
}

return null;
return HeaderUtils::extractFilename($this->headers);
}

/**
* @return non-negative-int|null
*/
public function getContentLength(): int|null
{
$length = $this->headers->get('content-length');
if (null !== $length) {
return abs((int) $length);
}

return null;
return HeaderUtils::extractContentLength($this->headers);
}
}
6 changes: 6 additions & 0 deletions src/DependencyInjection/SensiolabsGotenbergExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

use Sensiolabs\GotenbergBundle\Builder\Pdf\PdfBuilderInterface;
use Sensiolabs\GotenbergBundle\Builder\Screenshot\ScreenshotBuilderInterface;
use Sensiolabs\GotenbergBundle\Webhook\GotenbergRequestParser;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Webhook\Client\AbstractRequestParser;

/**
* @phpstan-type SensiolabsGotenbergConfiguration array{
Expand Down Expand Up @@ -120,6 +122,10 @@ public function load(array $configs, ContainerBuilder $container): void

$definition = $container->getDefinition('sensiolabs_gotenberg.asset.base_dir_formatter');
$definition->replaceArgument(2, $config['assets_directory']);

if ($container::willBeAvailable('symfony/webhook', AbstractRequestParser::class, ['symfony/framework-bundle'])) {
$container->register('sensiolabs_gotenberg.webhook.request_parser', GotenbergRequestParser::class);
}
}

/**
Expand Down
32 changes: 32 additions & 0 deletions src/RemoteEvent/ErrorGotenbergEvent.php
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;
}
}
Loading

0 comments on commit c5809a8

Please sign in to comment.