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 Sep 3, 2024
1 parent de08d60 commit 9915a29
Show file tree
Hide file tree
Showing 19 changed files with 711 additions and 31 deletions.
6 changes: 6 additions & 0 deletions config/builder_pdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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 +39,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 @@ -50,6 +52,7 @@
service('request_stack'),
service('twig')->nullOnInvalid(),
])
->call('setWebhookConfigurationRegistry', [service('.sensiolabs_gotenberg.webhook_configuration_registry')->nullOnInvalid()])
->call('setLogger', [service('logger')->nullOnInvalid()])
->tag('sensiolabs_gotenberg.pdf_builder')
;
Expand All @@ -61,6 +64,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 +75,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 +86,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')
;
};
3 changes: 3 additions & 0 deletions config/builder_screenshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 +36,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 +50,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);
}
}
1 change: 1 addition & 0 deletions src/Builder/Pdf/AbstractChromiumPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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
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
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
27 changes: 27 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,32 @@ 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');
$operationId = $this->inner->generateAsync();
$swEvent?->stop();

$this->pdfs[] = [
'calls' => $this->calls,
'time' => $swEvent?->getDuration(),
'memory' => $swEvent?->getMemory(),
'size' => null,
'fileName' => null,
];

++$this->totalGenerated;

return $operationId;
}

/**
* @param array<mixed> $arguments
*/
Expand Down
41 changes: 34 additions & 7 deletions src/Debug/Builder/TraceableScreenshotBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

namespace Sensiolabs\GotenbergBundle\Debug\Builder;

use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderInterface;
use Sensiolabs\GotenbergBundle\Builder\GotenbergFileResult;
use Sensiolabs\GotenbergBundle\Builder\Screenshot\ScreenshotBuilderInterface;
use Symfony\Component\Stopwatch\Stopwatch;

final class TraceableScreenshotBuilder implements ScreenshotBuilderInterface
{
/**
* @var list<array{'time': float, 'memory': int, 'size': int<0, max>|null, 'fileName': string, 'calls': list<array{'method': string, 'class': class-string<ScreenshotBuilderInterface>, 'arguments': array<mixed>}>}>
* @var list<array{'time': float|null, 'memory': int|null, 'size': int<0, max>|null, 'fileName': string|null, 'calls': list<array{'method': string, 'class': class-string<ScreenshotBuilderInterface>, 'arguments': array<mixed>}>}>
*/
private array $screenshots = [];

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

Expand All @@ -33,9 +34,9 @@ public function generate(): GotenbergFileResult
$name = self::$count.'.'.$this->inner::class.'::'.__FUNCTION__;
++self::$count;

$swEvent = $this->stopwatch->start($name, 'gotenberg.generate_screenshot');
$swEvent = $this->stopwatch?->start($name, 'gotenberg.generate_screenshot');
$response = $this->inner->generate();
$swEvent->stop();
$swEvent?->stop();

$fileName = 'Unknown';
if ($response->getHeaders()->has('Content-Disposition')) {
Expand All @@ -53,8 +54,8 @@ public function generate(): GotenbergFileResult

$this->screenshots[] = [
'calls' => $this->calls,
'time' => $swEvent->getDuration(),
'memory' => $swEvent->getMemory(),
'time' => $swEvent?->getDuration(),
'memory' => $swEvent?->getMemory(),
'status' => $response->getStatusCode(),
'size' => $lengthInBytes,
'fileName' => $fileName,
Expand All @@ -65,6 +66,32 @@ 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_screenshot');
$operationId = $this->inner->generateAsync();
$swEvent?->stop();

$this->screenshots[] = [
'calls' => $this->calls,
'time' => $swEvent?->getDuration(),
'memory' => $swEvent?->getMemory(),
'size' => null,
'fileName' => null,
];

++$this->totalGenerated;

return $operationId;
}

/**
* @param array<mixed> $arguments
*/
Expand All @@ -86,7 +113,7 @@ public function __call(string $name, array $arguments): mixed
}

/**
* @return list<array{'time': float, 'memory': int, 'size': int<0, max>|null, 'fileName': string, 'calls': list<array{'class': class-string<ScreenshotBuilderInterface>, 'method': string, 'arguments': array<mixed>}>}>
* @return list<array{'time': float|null, 'memory': int|null, 'size': int<0, max>|null, 'fileName': string|null, 'calls': list<array{'class': class-string<ScreenshotBuilderInterface>, 'method': string, 'arguments': array<mixed>}>}>
*/
public function getFiles(): array
{
Expand Down
Loading

0 comments on commit 9915a29

Please sign in to comment.