Skip to content
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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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(),
Copy link
Contributor

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 ? 👼

$services->set('.sensiolabs_gotenberg.builder')
    ->abstract()   
    ->args([
        service('sensiolabs_gotenberg.client'),
        service('sensiolabs_gotenberg.asset.base_dir_formatter'),
        service('.sensiolabs_gotenberg.webhook_configuration_registry'),
        service('request_stack'),
        service('router')->nullOnInvalid(),
    ])
    ->call('setLogger', [service('logger')->nullOnInvalid()])
;

$services->set('.sensiolabs_gotenberg.pdf_builder.markdown', MarkdownPdfBuilder::class)
    ->parent('.sensiolabs_gotenberb.builder')
    ->share(false)
    ->tag('sensiolabs_gotenberg.pdf_builder')
;

// ... 

Or... even better: introduce a BuilderFactory ?

Copy link
Contributor Author

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 ? 👼

I want to clean this file. I did it in a PoC about refactoring builder: #118

Or... even better: introduce a BuilderFactory ?

I also planned to test that 😉

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'
```
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.
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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

strict ?

if (!$this->urlGenerator) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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,
];

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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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 {
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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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 {
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