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

[Feature] Async generation #84

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

maelanleborgne
Copy link
Contributor

@maelanleborgne maelanleborgne commented Jun 25, 2024

Closes #46

Gotenberg allows async generation of files by passing webhooks urls in dedicated headers. Doing this, the request to Gotenberg server returns a 204 immediately, and once the file is generated, a call will be made by Gotenberg server on the specified endpoint to upload the generated file. This PR aims at enabling easy integration of this feature with the bundle.

The base of the feature is a new interface AsyncBuilderInterface that defines a generateAsync method. The AbstractPdfBuilder and AbstractScreenshotBuilder now implement this interface.
Another important addition is the webhookUrls(string $successWebhook, string|null $errorWebhook) that allows directly setting the webhooks url to use with the builders.

To make things easier when not working directly with urls (if you'd like to generate the url from a route, or if using the Webhook component), a some new configuration options are available :

# packages/sensiolabs_gotenberg.yaml

sensiolabs_gotenberg:  
    # ...
    webhook:  
        # Here you can define any number of webhook configuration so that you can reuse them in builder configs or to switch configuration at runtime
        my_config_name:  
            success:  
                url: 'http://example.com/webhook'
                # route: ['my_route_name', ['param1', 'param2']]
            # This "error" entry is optional. If not set, the "success" configuration will be used for both success and error.
            error:  
                url: 'http://example.com/webhook-error'
                # route: ['my_route_name', ['param1', 'param2']]
    default_options:
        # Here you can define which webhook config to use by default
        webhook: 'my_config_name'
        pdf:
            html:
                webhook: 'my_config_name'
                # Or you can define a specific configuration directly from here
                # webhook:
                #    success:
                #	    url: 'http://example.com/custom-webhook'
    #...

TODO:

  • Write documentation
  • Add example config to flex recipe
  • Add extra-headers option in the webhook config ?

@maelanleborgne maelanleborgne changed the title Feature/webhook integration [Feature] Async generation Jun 25, 2024
@maelanleborgne maelanleborgne marked this pull request as draft June 25, 2024 07:40
@Neirda24
Copy link
Contributor

After discussing this with @ConstantBqt we have another usecase to take into account : If we generate the PDF but the webhook is not setup in our own app but is external. We should be able to declare such case. WDYT @maelanleborgne ?

@maelanleborgne
Copy link
Contributor Author

After discussing this with @ConstantBqt we have another usecase to take into account : If we generate the PDF but the webhook is not setup in our own app but is external. We should be able to declare such case. WDYT @maelanleborgne ?

This is already present in the PR :

// ...
$this->gotenberg->html()
  ->webhookUrls('https://external.service/webhook', 'https://external.service/webhook-error')
  ->webhookExtraHeaders(['X-ExternalService-Secret' => 'a_secret'])
  ->generateAsync()
;

or via the configuration :

sensiolabs_gotenberg:  
    # ...
    webhook:  
        external_service:  
            success:  
                url: 'https://external.service/webhook'
            error:  
                url: 'https://external.service/webhook-error'
    default_options:
        pdf:
            html:
                webhook: 'external_service'

Or maybe I'm missing the point.

@Jean-Beru
Copy link
Contributor

This is already present in the PR :

Could this part be done in a dedicated PR ?

  • allow defining a webhook to add as a header
  • make "internal" webhook easier to developer using the component

composer.json Outdated Show resolved Hide resolved
composer.json Outdated Show resolved Hide resolved
config/builder_pdf.php Outdated Show resolved Hide resolved
src/Builder/Pdf/AbstractPdfBuilder.php Outdated Show resolved Hide resolved
src/DependencyInjection/Configuration.php Show resolved Hide resolved

public function __construct(
protected readonly GotenbergClientInterface $gotenbergClient,
protected readonly AssetBaseDirFormatter $asset,
protected readonly WebhookConfigurationRegistry|null $webhookConfigurationRegistry = null,
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the purpose of the WebhookConfigurationRegistry ? Could it be handled like other parameters ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It holds the different webhook configs available, so changes can be made at runtime using ->webhookConfiguration('custom_config'). I'm not sure we can make it work with setConfiguration because the config of a builder may be one of :

  • the name of a webhook config defined above in the config file (so it will still have to access the config registry)
  • a new definition : success and error with a route/url each (and then we'd have to make the router available in order to generate urls for the routes)

Comment on lines +55 to +57
$container->registerForAutoconfiguration(WebhookConfigurationRegistryInterface::class)
->addTag('sensiolabs_gotenberg.webhook_configuration_registry')
;
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the purpose of this tag ?

@@ -25,6 +25,7 @@
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
Copy link
Contributor

Choose a reason for hiding this comment

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

is ->nullOnInvalid() needed since we always define this service in services.php ?

@@ -38,6 +39,7 @@
service('router')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
Copy link
Contributor

Choose a reason for hiding this comment

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

Why through a setter ? Why not the constructor ?

'Gotenberg-Webhook-Extra-Http-Headers' => json_encode($this->webhookExtraHeaders, \JSON_THROW_ON_ERROR),
];
if (null !== $this->fileName) {
$headers['Gotenberg-Output-Filename'] = basename($this->fileName, '.pdf');
Copy link
Contributor

Choose a reason for hiding this comment

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

remove the basename call for now. it will be handled correctly in another PR. Plus : the async is not only for PDFs.

return $this;
}

public function operationIdGenerator(\Closure $operationIdGenerator): static
Copy link
Contributor

Choose a reason for hiding this comment

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

phpstan annotation for closure please.

*/
public function webhookExtraHeaders(array $extraHeaders): static
{
$this->webhookExtraHeaders = array_merge($this->webhookExtraHeaders, $extraHeaders);
Copy link
Contributor

Choose a reason for hiding this comment

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

so how do we reset this ?


protected static function defaultOperationIdGenerator(): string
{
return 'gotenberg_'.bin2hex(random_bytes(16)).microtime(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should keep the id stateful per builder ? Reset it only when generating the PDF. Also the ID should be part of any operation I guess. Not only the async one. WDYT ?

@@ -586,6 +586,7 @@ protected function addConfiguration(string $configurationName, mixed $value): vo
'fail_on_console_exceptions' => $this->failOnConsoleExceptions($value),
'skip_network_idle_event' => $this->skipNetworkIdleEvent($value),
'metadata' => $this->metadata($value),
'webhook' => null,
Copy link
Contributor

Choose a reason for hiding this comment

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

why ?

}
$webhookConfiguration = $this->webhookConfigurationRegistry->get($webhook);

return $this->webhookUrls($webhookConfiguration['success'], $webhookConfiguration['error']);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe it can also contain extra headers ? Would that be relevant ? Or authent or anything else ?

@@ -24,7 +25,7 @@ final class TraceableScreenshotBuilder implements ScreenshotBuilderInterface

public function __construct(
private readonly ScreenshotBuilderInterface $inner,
private readonly Stopwatch $stopwatch,
private readonly Stopwatch|null $stopwatch,
Copy link
Contributor

Choose a reason for hiding this comment

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

why this change ?

->children()
->scalarNode('name')
->validate()
->ifTrue(static function ($option) {
Copy link
Contributor

Choose a reason for hiding this comment

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

return type + arg type

->append($this->addWebhookConfigurationNode('error'))
->end()
->validate()
->ifTrue(static function ($option): bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

arg type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implements the Webhook features
3 participants