Skip to content

Commit

Permalink
Add ability to generate asynchronously using webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
maelanleborgne committed Jul 22, 2024
1 parent de08d60 commit 13eb438
Show file tree
Hide file tree
Showing 21 changed files with 680 additions and 27 deletions.
7 changes: 7 additions & 0 deletions config/builder_pdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Sensiolabs\GotenbergBundle\Builder\Pdf\MarkdownPdfBuilder;
use Sensiolabs\GotenbergBundle\Builder\Pdf\MergePdfBuilder;
use Sensiolabs\GotenbergBundle\Builder\Pdf\UrlPdfBuilder;
use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistryInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

Expand All @@ -25,6 +26,7 @@
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.pdf_builder')
;

Expand All @@ -38,6 +40,7 @@
service('router')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->call('setRequestContext', [service('.sensiolabs_gotenberg.request_context')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.pdf_builder')
;
Expand All @@ -49,6 +52,7 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
service('request_stack'),
service('twig')->nullOnInvalid(),
service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.pdf_builder')
Expand All @@ -61,6 +65,7 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.pdf_builder')
;

Expand All @@ -71,6 +76,7 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.pdf_builder')
;

Expand All @@ -81,6 +87,7 @@
service('sensiolabs_gotenberg.asset.base_dir_formatter'),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.pdf_builder')
;
};
4 changes: 4 additions & 0 deletions config/builder_screenshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use Sensiolabs\GotenbergBundle\Builder\Screenshot\HtmlScreenshotBuilder;
use Sensiolabs\GotenbergBundle\Builder\Screenshot\MarkdownScreenshotBuilder;
use Sensiolabs\GotenbergBundle\Builder\Screenshot\UrlScreenshotBuilder;
use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistryInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

Expand All @@ -22,6 +23,7 @@
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.screenshot_builder')
;

Expand All @@ -35,6 +37,7 @@
service('router')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->call('setRequestContext', [service('.sensiolabs_gotenberg.request_context')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.screenshot_builder')
;
Expand All @@ -48,6 +51,7 @@
service('twig')->nullOnInvalid(),
])
->call('setLogger', [service('logger')->nullOnInvalid()])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.screenshot_builder')
;
};
11 changes: 11 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

use Sensiolabs\GotenbergBundle\Client\GotenbergClient;
use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistry;
use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistryInterface;
use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter;
use Sensiolabs\GotenbergBundle\Gotenberg;
use Sensiolabs\GotenbergBundle\GotenbergInterface;
Expand Down Expand Up @@ -63,4 +65,13 @@
])
->alias(GotenbergInterface::class, 'sensiolabs_gotenberg')
;

$services->set('.sensiolabs_gotenberg.webhook_configuration_registry', WebhookConfigurationRegistry::class)
->args([
service('router'),
service('.sensiolabs_gotenberg.request_context')->nullOnInvalid(),
])
->tag('sensiolabs_gotenberg.webhook_configuration_registry')
->alias(WebhookConfigurationRegistryInterface::class, '.sensiolabs_gotenberg.webhook_configuration_registry')
;
};
11 changes: 11 additions & 0 deletions src/Builder/AsyncBuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Sensiolabs\GotenbergBundle\Builder;

interface AsyncBuilderInterface
{
/**
* Generates a file asynchronously.
*/
public function generateAsync(): string;
}
88 changes: 88 additions & 0 deletions src/Builder/AsyncBuilderTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Sensiolabs\GotenbergBundle\Builder;

use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistry;
use Sensiolabs\GotenbergBundle\Exception\WebhookConfigurationException;

trait AsyncBuilderTrait
{
use DefaultBuilderTrait;
private string $webhookUrl;
private string $errorWebhookUrl;
/**
* @var array<string, mixed>
*/
private array $webhookExtraHeaders = [];
private \Closure $operationIdGenerator;
private WebhookConfigurationRegistry|null $webhookConfigurationRegistry = null;

public function generateAsync(): string
{
$operationId = ($this->operationIdGenerator ?? self::defaultOperationIdGenerator(...))();
$this->logger?->debug('Generating a file asynchronously with operation id {sensiolabs_gotenberg.operation_id} using {sensiolabs_gotenberg.builder} builder.', [
'sensiolabs_gotenberg.operation_id' => $operationId,
'sensiolabs_gotenberg.builder' => $this::class,
]);

$this->webhookExtraHeaders['X-Gotenberg-Operation-Id'] = $operationId;
$headers = [
'Gotenberg-Webhook-Url' => $this->webhookUrl,
'Gotenberg-Webhook-Error-Url' => $this->errorWebhookUrl,
'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');
}
$this->client->call($this->getEndpoint(), $this->getMultipartFormData(), $headers);

return $operationId;
}

public function setWebhookConfigurationRegistry(WebhookConfigurationRegistry $registry): static
{
$this->webhookConfigurationRegistry = $registry;

return $this;
}

public function webhookConfiguration(string $webhook): static
{
if (null === $this->webhookConfigurationRegistry) {
throw new WebhookConfigurationException('The WebhookConfigurationRegistry is not available.');
}
$webhookConfiguration = $this->webhookConfigurationRegistry->get($webhook);

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

public function webhookUrls(string $successWebhook, string|null $errorWebhook = null): static
{
$this->webhookUrl = $successWebhook;
$this->errorWebhookUrl = $errorWebhook ?? $successWebhook;

return $this;
}

/**
* @param array<string, mixed> $extraHeaders
*/
public function webhookExtraHeaders(array $extraHeaders): static
{
$this->webhookExtraHeaders = array_merge($this->webhookExtraHeaders, $extraHeaders);

return $this;
}

public function operationIdGenerator(\Closure $operationIdGenerator): static
{
$this->operationIdGenerator = $operationIdGenerator;

return $this;
}

protected static function defaultOperationIdGenerator(): string
{
return 'gotenberg_'.bin2hex(random_bytes(16)).microtime(true);
}
}
5 changes: 4 additions & 1 deletion src/Builder/Pdf/AbstractChromiumPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Sensiolabs\GotenbergBundle\Builder\CookieAwareTrait;
use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistry;
use Sensiolabs\GotenbergBundle\Enumeration\EmulatedMediaType;
use Sensiolabs\GotenbergBundle\Enumeration\PaperSizeInterface;
use Sensiolabs\GotenbergBundle\Enumeration\Part;
Expand All @@ -28,8 +29,9 @@ public function __construct(
AssetBaseDirFormatter $asset,
private readonly RequestStack $requestStack,
private readonly Environment|null $twig = null,
WebhookConfigurationRegistry|null $webhookConfigurationRegistry = null,
) {
parent::__construct($gotenbergClient, $asset);
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry);

Check failure on line 34 in src/Builder/Pdf/AbstractChromiumPdfBuilder.php

View workflow job for this annotation

GitHub Actions / PHPStan

Method Sensiolabs\GotenbergBundle\Builder\Pdf\AbstractPdfBuilder::__construct() invoked with 3 parameters, 2 required.

$normalizers = [
'extraHttpHeaders' => function (mixed $value): array {
Expand Down Expand Up @@ -586,6 +588,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,
default => throw new InvalidBuilderConfiguration(sprintf('Invalid option "%s": no method does not exist in class "%s" to configured it.', $configurationName, static::class)),
};
}
Expand Down
5 changes: 4 additions & 1 deletion src/Builder/Pdf/AbstractPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

namespace Sensiolabs\GotenbergBundle\Builder\Pdf;

use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderInterface;
use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderTrait;
use Sensiolabs\GotenbergBundle\Builder\DefaultBuilderTrait;
use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter;

abstract class AbstractPdfBuilder implements PdfBuilderInterface
abstract class AbstractPdfBuilder implements PdfBuilderInterface, AsyncBuilderInterface
{
use AsyncBuilderTrait;
use DefaultBuilderTrait;

public function __construct(
Expand Down
4 changes: 3 additions & 1 deletion src/Builder/Pdf/UrlPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Sensiolabs\GotenbergBundle\Builder\Pdf;

use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistry;
use Sensiolabs\GotenbergBundle\Exception\MissingRequiredFieldException;
use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter;
use Symfony\Component\HttpFoundation\RequestStack;
Expand All @@ -22,8 +23,9 @@ public function __construct(
RequestStack $requestStack,
Environment|null $twig = null,
private readonly UrlGeneratorInterface|null $urlGenerator = null,
WebhookConfigurationRegistry|null $webhookConfigurationRegistry = null,
) {
parent::__construct($gotenbergClient, $asset, $requestStack, $twig);
parent::__construct($gotenbergClient, $asset, $requestStack, $twig, $webhookConfigurationRegistry);

$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 @@ -4,6 +4,7 @@

use Sensiolabs\GotenbergBundle\Builder\CookieAwareTrait;
use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistryInterface;
use Sensiolabs\GotenbergBundle\Enumeration\EmulatedMediaType;
use Sensiolabs\GotenbergBundle\Enumeration\Part;
use Sensiolabs\GotenbergBundle\Enumeration\ScreenshotFormat;
Expand All @@ -26,8 +27,9 @@ public function __construct(
AssetBaseDirFormatter $asset,
private readonly RequestStack $requestStack,
private readonly Environment|null $twig = null,
WebhookConfigurationRegistryInterface|null $webhookConfigurationRegistry = null,
) {
parent::__construct($gotenbergClient, $asset);
parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry);

Check failure on line 32 in src/Builder/Screenshot/AbstractChromiumScreenshotBuilder.php

View workflow job for this annotation

GitHub Actions / PHPStan

Method Sensiolabs\GotenbergBundle\Builder\Screenshot\AbstractScreenshotBuilder::__construct() invoked with 3 parameters, 2 required.

$normalizers = [
'extraHttpHeaders' => function (mixed $value): array {
Expand Down
5 changes: 4 additions & 1 deletion src/Builder/Screenshot/AbstractScreenshotBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

namespace Sensiolabs\GotenbergBundle\Builder\Screenshot;

use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderInterface;
use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderTrait;
use Sensiolabs\GotenbergBundle\Builder\DefaultBuilderTrait;
use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\Enumeration\Part;
use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter;
use Symfony\Component\Mime\Part\DataPart;

abstract class AbstractScreenshotBuilder implements ScreenshotBuilderInterface
abstract class AbstractScreenshotBuilder implements ScreenshotBuilderInterface, AsyncBuilderInterface
{
use AsyncBuilderTrait;
use DefaultBuilderTrait;

public function __construct(
Expand Down
4 changes: 3 additions & 1 deletion src/Builder/Screenshot/UrlScreenshotBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Sensiolabs\GotenbergBundle\Builder\Screenshot;

use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface;
use Sensiolabs\GotenbergBundle\DependencyInjection\WebhookConfiguration\WebhookConfigurationRegistryInterface;
use Sensiolabs\GotenbergBundle\Exception\MissingRequiredFieldException;
use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter;
use Symfony\Component\HttpFoundation\RequestStack;
Expand All @@ -22,8 +23,9 @@ public function __construct(
RequestStack $requestStack,
Environment|null $twig = null,
private readonly UrlGeneratorInterface|null $urlGenerator = null,
WebhookConfigurationRegistryInterface|null $webhookConfigurationRegistry = null,
) {
parent::__construct($gotenbergClient, $asset, $requestStack, $twig);
parent::__construct($gotenbergClient, $asset, $requestStack, $twig, $webhookConfigurationRegistry);

$this->addNormalizer('route', $this->generateUrlFromRoute(...));
}
Expand Down
2 changes: 1 addition & 1 deletion src/Client/GotenbergClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function call(string $endpoint, array $multipartFormData, array $headers
],
);

if (200 !== $response->getStatusCode()) {
if (!\in_array($response->getStatusCode(), [200, 204], true)) {
throw new ClientException($response->getContent(false), $response->getStatusCode());
}

Expand Down
17 changes: 17 additions & 0 deletions src/Debug/Builder/TraceablePdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Sensiolabs\GotenbergBundle\Debug\Builder;

use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderInterface;
use Sensiolabs\GotenbergBundle\Builder\GotenbergFileResult;
use Sensiolabs\GotenbergBundle\Builder\Pdf\PdfBuilderInterface;
use Symfony\Component\Stopwatch\Stopwatch;
Expand Down Expand Up @@ -50,6 +51,22 @@ public function generate(): GotenbergFileResult
return $response;
}

public function generateAsync(): string
{
if (!$this->inner instanceof AsyncBuilderInterface) {
throw new \LogicException(sprintf('The inner builder of %s must implement %s.', self::class, AsyncBuilderInterface::class));
}

$name = self::$count.'.'.$this->inner::class.'::'.__FUNCTION__;
++self::$count;

$swEvent = $this->stopwatch->start($name, 'gotenberg.generate_pdf');

Check failure on line 63 in src/Debug/Builder/TraceablePdfBuilder.php

View workflow job for this annotation

GitHub Actions / PHPStan

Cannot call method start() on Symfony\Component\Stopwatch\Stopwatch|null.
$gotenbergTrace = $this->inner->generateAsync();
$swEvent->stop();

return $gotenbergTrace;
}

/**
* @param array<mixed> $arguments
*/
Expand Down
Loading

0 comments on commit 13eb438

Please sign in to comment.