diff --git a/config/builder_pdf.php b/config/builder_pdf.php index a150f13..74d83e9 100644 --- a/config/builder_pdf.php +++ b/config/builder_pdf.php @@ -21,6 +21,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), service('request_stack'), service('twig')->nullOnInvalid(), ]) @@ -33,6 +34,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), service('request_stack'), service('twig')->nullOnInvalid(), service('router')->nullOnInvalid(), @@ -47,6 +49,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), service('request_stack'), service('twig')->nullOnInvalid(), ]) @@ -59,6 +62,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), ]) ->call('setLogger', [service('logger')->nullOnInvalid()]) ->tag('sensiolabs_gotenberg.pdf_builder') @@ -69,6 +73,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), ]) ->call('setLogger', [service('logger')->nullOnInvalid()]) ->tag('sensiolabs_gotenberg.pdf_builder') @@ -79,6 +84,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), ]) ->call('setLogger', [service('logger')->nullOnInvalid()]) ->tag('sensiolabs_gotenberg.pdf_builder') diff --git a/config/builder_screenshot.php b/config/builder_screenshot.php index 7e04c10..bd78280 100644 --- a/config/builder_screenshot.php +++ b/config/builder_screenshot.php @@ -18,6 +18,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), service('request_stack'), service('twig')->nullOnInvalid(), ]) @@ -30,6 +31,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), service('request_stack'), service('twig')->nullOnInvalid(), service('router')->nullOnInvalid(), @@ -44,6 +46,7 @@ ->args([ service('sensiolabs_gotenberg.client'), service('sensiolabs_gotenberg.asset.base_dir_formatter'), + service('sensiolabs_gotenberg.webhook_configuration_registry'), service('request_stack'), service('twig')->nullOnInvalid(), ]) diff --git a/config/services.php b/config/services.php index caebd15..382478b 100644 --- a/config/services.php +++ b/config/services.php @@ -11,6 +11,8 @@ use Sensiolabs\GotenbergBundle\GotenbergScreenshot; use Sensiolabs\GotenbergBundle\GotenbergScreenshotInterface; use Sensiolabs\GotenbergBundle\Twig\GotenbergAssetExtension; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistry; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\Filesystem\Filesystem; use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg; @@ -68,4 +70,13 @@ $services->set('sensiolabs_gotenberg.http_kernel.stream_builder', ProcessBuilderOnControllerResponse::class) ->tag('kernel.event_listener', ['method' => 'streamBuilder', 'event' => 'kernel.view']) ; + + $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') + ; }; diff --git a/docs/builders_api.md b/docs/builders_api.md index 55a2b19..5842431 100644 --- a/docs/builders_api.md +++ b/docs/builders_api.md @@ -2,14 +2,16 @@ ## Pdf -* [HtmlPdfBuilder](./pdf/builders_api/HtmlPdfBuilder.md) -* [UrlPdfBuilder](./pdf/builders_api/UrlPdfBuilder.md) -* [MarkdownPdfBuilder](./pdf/builders_api/MarkdownPdfBuilder.md) -* [LibreOfficePdfBuilder](./pdf/builders_api/LibreOfficePdfBuilder.md) +* [HtmlPdfBuilder](./Pdf/builders_api/HtmlPdfBuilder.md) +* [UrlPdfBuilder](./Pdf/builders_api/UrlPdfBuilder.md) +* [MarkdownPdfBuilder](./Pdf/builders_api/MarkdownPdfBuilder.md) +* [LibreOfficePdfBuilder](./Pdf/builders_api/LibreOfficePdfBuilder.md) +* [MergePdfBuilder](./Pdf/builders_api/MergePdfBuilder.md) +* [ConvertPdfBuilder](./Pdf/builders_api/ConvertPdfBuilder.md) ## Screenshot -* [HtmlScreenshotBuilder](./screenshot/builders_api/HtmlScreenshotBuilder.md) -* [UrlScreenshotBuilder](./screenshot/builders_api/UrlScreenshotBuilder.md) -* [MarkdownScreenshotBuilder](./screenshot/builders_api/MarkdownScreenshotBuilder.md) +* [HtmlScreenshotBuilder](./Screenshot/builders_api/HtmlScreenshotBuilder.md) +* [UrlScreenshotBuilder](./Screenshot/builders_api/UrlScreenshotBuilder.md) +* [MarkdownScreenshotBuilder](./Screenshot/builders_api/MarkdownScreenshotBuilder.md) diff --git a/docs/generate.php b/docs/generate.php index 7e11acf..0b82613 100755 --- a/docs/generate.php +++ b/docs/generate.php @@ -1,9 +1,11 @@ #!/usr/bin/env php [ HtmlScreenshotBuilder::class, @@ -36,7 +40,10 @@ 'setLogger', 'setConfigurations', 'generate', + 'generateAsync', 'getMultipartFormData', + 'fileName', + 'processor', ]; function parseMethodSignature(ReflectionMethod $method): string @@ -59,7 +66,7 @@ function parseDocComment(string $rawDocComment): string { $result = ''; - $lines = explode("\n", $rawDocComment); + $lines = explode("\n", trim($rawDocComment, "\n")); array_shift($lines); array_pop($lines); @@ -89,13 +96,28 @@ function parseBuilder(ReflectionClass $builder): string $builderName = $builder->getShortName(); $markdown .= "# {$builderName}\n\n"; + $builderComment = $builder->getDocComment(); + + if (false !== $builderComment) { + $markdown .= parseDocComment($builderComment)."\n"; + } + + $methods = []; + foreach ($builder->getInterfaces() as $interface) { + foreach ($interface->getMethods() as $method) { + if ('' !== ($method->getDocComment() ?: '')) { + $methods[$method->getName()] = parseDocComment($method->getDocComment()); + } + } + } + foreach ($builder->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { if (\in_array($method->getName(), EXCLUDED_METHODS, true) === true) { continue; } $methodSignature = parseMethodSignature($method); - $docComment = parseDocComment($method->getDocComment() ?: ''); + $docComment = parseDocComment($methods[$method->getShortName()] ?? $method->getDocComment() ?: ''); $markdown .= <<<"MARKDOWN" * `{$methodSignature}`: @@ -118,7 +140,7 @@ function saveFile(InputInterface $input, string $filename, string $contents): vo $summary = "# Builders API\n\n"; foreach (BUILDERS as $type => $builderClasses) { - $subDirectory = strtolower($type).'/builders_api'; + $subDirectory = "{$type}/builders_api"; $directory = __DIR__.'/'.$subDirectory; if (!@mkdir($directory, recursive: true) && !is_dir($directory)) { diff --git a/docs/pdf/builders_api/ConvertPdfBuilder.md b/docs/pdf/builders_api/ConvertPdfBuilder.md new file mode 100644 index 0000000..0c49bf7 --- /dev/null +++ b/docs/pdf/builders_api/ConvertPdfBuilder.md @@ -0,0 +1,29 @@ +# ConvertPdfBuilder + +* `pdfFormat(Sensiolabs\GotenbergBundle\Enumeration\PdfFormat $format)`: +Convert the resulting PDF into the given PDF/A format. + +* `pdfUniversalAccess(bool $bool)`: +Enable PDF for Universal Access for optimal accessibility. + +* `files(string $paths)`: + +* `downloadFrom(array $downloadFrom)`: + +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. + +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. + diff --git a/docs/pdf/builders_api/HtmlPdfBuilder.md b/docs/pdf/builders_api/HtmlPdfBuilder.md index 28100c2..bea69e5 100644 --- a/docs/pdf/builders_api/HtmlPdfBuilder.md +++ b/docs/pdf/builders_api/HtmlPdfBuilder.md @@ -129,9 +129,22 @@ The metadata to write. * `downloadFrom(array $downloadFrom)`: -* `fileName(string $fileName, string $headerDisposition)`: +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. -* `processor(Sensiolabs\GotenbergBundle\Processor\ProcessorInterface $processor)`: +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. * `addCookies(array $cookies)`: Add cookies to store in the Chromium cookie jar. diff --git a/docs/pdf/builders_api/LibreOfficePdfBuilder.md b/docs/pdf/builders_api/LibreOfficePdfBuilder.md index 1839039..c27100e 100644 --- a/docs/pdf/builders_api/LibreOfficePdfBuilder.md +++ b/docs/pdf/builders_api/LibreOfficePdfBuilder.md @@ -87,7 +87,20 @@ If the form field reduceImageResolution is set to true, tell if all images will * `downloadFrom(array $downloadFrom)`: -* `fileName(string $fileName, string $headerDisposition)`: +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. -* `processor(Sensiolabs\GotenbergBundle\Processor\ProcessorInterface $processor)`: +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. diff --git a/docs/pdf/builders_api/MarkdownPdfBuilder.md b/docs/pdf/builders_api/MarkdownPdfBuilder.md index 8ac0c84..47ea071 100644 --- a/docs/pdf/builders_api/MarkdownPdfBuilder.md +++ b/docs/pdf/builders_api/MarkdownPdfBuilder.md @@ -132,9 +132,22 @@ The metadata to write. * `downloadFrom(array $downloadFrom)`: -* `fileName(string $fileName, string $headerDisposition)`: +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. -* `processor(Sensiolabs\GotenbergBundle\Processor\ProcessorInterface $processor)`: +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. * `addCookies(array $cookies)`: Add cookies to store in the Chromium cookie jar. diff --git a/docs/pdf/builders_api/MergePdfBuilder.md b/docs/pdf/builders_api/MergePdfBuilder.md new file mode 100644 index 0000000..db2a32f --- /dev/null +++ b/docs/pdf/builders_api/MergePdfBuilder.md @@ -0,0 +1,37 @@ +# MergePdfBuilder + +Merge `n` pdf files into a single one. + +* `pdfFormat(Sensiolabs\GotenbergBundle\Enumeration\PdfFormat $format)`: +Convert the resulting PDF into the given PDF/A format. + +* `pdfUniversalAccess(bool $bool)`: +Enable PDF for Universal Access for optimal accessibility. + +* `files(string $paths)`: + +* `metadata(array $metadata)`: +Resets the metadata. + +* `addMetadata(string $key, string $value)`: +The metadata to write. + +* `downloadFrom(array $downloadFrom)`: + +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. + +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. + diff --git a/docs/pdf/builders_api/UrlPdfBuilder.md b/docs/pdf/builders_api/UrlPdfBuilder.md index 6aedc16..30d74ae 100644 --- a/docs/pdf/builders_api/UrlPdfBuilder.md +++ b/docs/pdf/builders_api/UrlPdfBuilder.md @@ -131,9 +131,22 @@ The metadata to write. * `downloadFrom(array $downloadFrom)`: -* `fileName(string $fileName, string $headerDisposition)`: +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. -* `processor(Sensiolabs\GotenbergBundle\Processor\ProcessorInterface $processor)`: +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. * `addCookies(array $cookies)`: Add cookies to store in the Chromium cookie jar. diff --git a/docs/screenshot/builders_api/HtmlScreenshotBuilder.md b/docs/screenshot/builders_api/HtmlScreenshotBuilder.md index d70c992..a3b678a 100644 --- a/docs/screenshot/builders_api/HtmlScreenshotBuilder.md +++ b/docs/screenshot/builders_api/HtmlScreenshotBuilder.md @@ -74,9 +74,22 @@ Adds a file, like an image, font, stylesheet, and so on. * `downloadFrom(array $downloadFrom)`: -* `fileName(string $fileName, string $headerDisposition)`: +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. -* `processor(Sensiolabs\GotenbergBundle\Processor\ProcessorInterface $processor)`: +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. * `addCookies(array $cookies)`: Add cookies to store in the Chromium cookie jar. diff --git a/docs/screenshot/builders_api/MarkdownScreenshotBuilder.md b/docs/screenshot/builders_api/MarkdownScreenshotBuilder.md index 3627b5a..5180bb8 100644 --- a/docs/screenshot/builders_api/MarkdownScreenshotBuilder.md +++ b/docs/screenshot/builders_api/MarkdownScreenshotBuilder.md @@ -77,9 +77,22 @@ Adds a file, like an image, font, stylesheet, and so on. * `downloadFrom(array $downloadFrom)`: -* `fileName(string $fileName, string $headerDisposition)`: +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. -* `processor(Sensiolabs\GotenbergBundle\Processor\ProcessorInterface $processor)`: +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. * `addCookies(array $cookies)`: Add cookies to store in the Chromium cookie jar. diff --git a/docs/screenshot/builders_api/UrlScreenshotBuilder.md b/docs/screenshot/builders_api/UrlScreenshotBuilder.md index 35e20de..3eb4deb 100644 --- a/docs/screenshot/builders_api/UrlScreenshotBuilder.md +++ b/docs/screenshot/builders_api/UrlScreenshotBuilder.md @@ -76,9 +76,22 @@ Adds a file, like an image, font, stylesheet, and so on. * `downloadFrom(array $downloadFrom)`: -* `fileName(string $fileName, string $headerDisposition)`: +* `webhookConfiguration(string $name)`: +Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. -* `processor(Sensiolabs\GotenbergBundle\Processor\ProcessorInterface $processor)`: +* `webhookUrl(string $url, ?string $method)`: +Sets the webhook for cases of success. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `errorWebhookUrl(?string $url, ?string $method)`: +Sets the webhook for cases of error. +Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + +* `webhookUrls(string $successWebhook, ?string $errorWebhook, ?string $successMethod, ?string $errorMethod)`: +Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + +* `webhookExtraHeaders(array $extraHeaders)`: +Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. * `addCookies(array $cookies)`: Add cookies to store in the Chromium cookie jar. diff --git a/phpunit.xml b/phpunit.xml index 7fb1daf..db97099 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,6 +7,10 @@ requireCoverageMetadata="true" beStrictAboutCoverageMetadata="true" beStrictAboutOutputDuringTests="true" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true" failOnRisky="true" failOnWarning="true"> diff --git a/src/Builder/AsyncBuilderInterface.php b/src/Builder/AsyncBuilderInterface.php new file mode 100644 index 0000000..3daedb5 --- /dev/null +++ b/src/Builder/AsyncBuilderInterface.php @@ -0,0 +1,15 @@ + + */ + private array $webhookExtraHeaders = []; + + private WebhookConfigurationRegistryInterface $webhookConfigurationRegistry; + + public function generateAsync(): void + { + if (null === $this->successWebhookUrl) { + throw new MissingRequiredFieldException('->webhookUrls() was never called.'); + } + + $errorWebhookUrl = $this->errorWebhookUrl ?? $this->successWebhookUrl; + + $headers = [ + 'Gotenberg-Webhook-Url' => $this->successWebhookUrl, + 'Gotenberg-Webhook-Error-Url' => $errorWebhookUrl, + ]; + + if (null !== $this->successWebhookMethod) { + $headers['Gotenberg-Webhook-Method'] = $this->successWebhookMethod; + } + + if (null !== $this->errorWebhookMethod) { + $headers['Gotenberg-Webhook-Error-Method'] = $this->errorWebhookMethod; + } + + if ([] !== $this->webhookExtraHeaders) { + $headers['Gotenberg-Webhook-Extra-Http-Headers'] = json_encode($this->webhookExtraHeaders, \JSON_THROW_ON_ERROR); + } + + if (null !== $this->fileName) { + // 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); + } + + /** + * Providing an existing $name from the configuration file, it will correctly set both success and error webhook URLs as well as extra_http_headers if defined. + */ + public function webhookConfiguration(string $name): static + { + $webhookConfiguration = $this->webhookConfigurationRegistry->get($name); + + $result = $this->webhookUrls( + $webhookConfiguration['success']['url'], + $webhookConfiguration['error']['url'], + $webhookConfiguration['success']['method'], + $webhookConfiguration['error']['method'], + ); + + if (\array_key_exists('extra_http_headers', $webhookConfiguration)) { + $result = $result->webhookExtraHeaders($webhookConfiguration['extra_http_headers']); + } + + return $result; + } + + /** + * Sets the webhook for cases of success. + * Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + * + * @param 'POST'|'PATCH'|'PUT'|null $method + * + * @see https://gotenberg.dev/docs/webhook + */ + public function webhookUrl(string $url, string|null $method = null): static + { + $this->successWebhookUrl = $url; + $this->successWebhookMethod = $method; + + return $this; + } + + /** + * Sets the webhook for cases of error. + * Optionaly sets a custom HTTP method for such endpoint among : POST, PUT or PATCH. + * + * @param 'POST'|'PATCH'|'PUT'|null $method + * + * @see https://gotenberg.dev/docs/webhook + */ + public function errorWebhookUrl(string|null $url = null, string|null $method = null): static + { + $this->errorWebhookUrl = $url; + $this->errorWebhookMethod = $method; + + return $this; + } + + /** + * Allows to set both $successWebhook and $errorWebhook URLs. If $errorWebhook is not provided, it will fallback to $successWebhook one. + * + * @param 'POST'|'PATCH'|'PUT'|null $successMethod + * @param 'POST'|'PATCH'|'PUT'|null $errorMethod + */ + public function webhookUrls(string $successWebhook, string|null $errorWebhook = null, string|null $successMethod = null, string|null $errorMethod = null): static + { + return $this + ->webhookUrl($successWebhook, $successMethod) + ->errorWebhookUrl($errorWebhook, $errorMethod) + ; + } + + /** + * Extra headers that will be provided to the webhook endpoint. May it either be Success or Error. + * + * @param array $extraHeaders + */ + public function webhookExtraHeaders(array $extraHeaders): static + { + $this->webhookExtraHeaders = array_merge($this->webhookExtraHeaders, $extraHeaders); + + return $this; + } +} diff --git a/src/Builder/Pdf/AbstractChromiumPdfBuilder.php b/src/Builder/Pdf/AbstractChromiumPdfBuilder.php index 90c454b..4cb8968 100644 --- a/src/Builder/Pdf/AbstractChromiumPdfBuilder.php +++ b/src/Builder/Pdf/AbstractChromiumPdfBuilder.php @@ -14,6 +14,7 @@ use Sensiolabs\GotenbergBundle\Exception\InvalidBuilderConfiguration; use Sensiolabs\GotenbergBundle\Exception\PdfPartRenderingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Mime\Part\DataPart; @@ -27,10 +28,11 @@ abstract class AbstractChromiumPdfBuilder extends AbstractPdfBuilder public function __construct( GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, + WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, private readonly RequestStack $requestStack, private readonly Environment|null $twig = null, ) { - parent::__construct($gotenbergClient, $asset); + parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry); $normalizers = [ 'extraHttpHeaders' => function (mixed $value): array { diff --git a/src/Builder/Pdf/AbstractPdfBuilder.php b/src/Builder/Pdf/AbstractPdfBuilder.php index 658cab3..7215dfe 100644 --- a/src/Builder/Pdf/AbstractPdfBuilder.php +++ b/src/Builder/Pdf/AbstractPdfBuilder.php @@ -2,22 +2,28 @@ namespace Sensiolabs\GotenbergBundle\Builder\Pdf; +use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderInterface; +use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderTrait; use Sensiolabs\GotenbergBundle\Builder\DefaultBuilderTrait; use Sensiolabs\GotenbergBundle\Builder\DownloadFromTrait; use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; -abstract class AbstractPdfBuilder implements PdfBuilderInterface +abstract class AbstractPdfBuilder implements PdfBuilderInterface, AsyncBuilderInterface { + use AsyncBuilderTrait; use DefaultBuilderTrait; use DownloadFromTrait; public function __construct( GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, + WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, ) { $this->client = $gotenbergClient; $this->asset = $asset; + $this->webhookConfigurationRegistry = $webhookConfigurationRegistry; $this->normalizers = [ 'metadata' => function (mixed $value): array { diff --git a/src/Builder/Pdf/MergePdfBuilder.php b/src/Builder/Pdf/MergePdfBuilder.php index 6ff9b8b..25dc40a 100644 --- a/src/Builder/Pdf/MergePdfBuilder.php +++ b/src/Builder/Pdf/MergePdfBuilder.php @@ -8,6 +8,9 @@ use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\File as DataPartFile; +/** + * Merge `n` pdf files into a single one. + */ final class MergePdfBuilder extends AbstractPdfBuilder { private const ENDPOINT = '/forms/pdfengines/merge'; diff --git a/src/Builder/Pdf/UrlPdfBuilder.php b/src/Builder/Pdf/UrlPdfBuilder.php index 707b6a0..e72cc8e 100644 --- a/src/Builder/Pdf/UrlPdfBuilder.php +++ b/src/Builder/Pdf/UrlPdfBuilder.php @@ -5,6 +5,7 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Exception\MissingRequiredFieldException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RequestContext; @@ -19,11 +20,12 @@ final class UrlPdfBuilder extends AbstractChromiumPdfBuilder public function __construct( GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, + WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, RequestStack $requestStack, Environment|null $twig = null, private readonly UrlGeneratorInterface|null $urlGenerator = null, ) { - parent::__construct($gotenbergClient, $asset, $requestStack, $twig); + parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $requestStack, $twig); $this->addNormalizer('route', $this->generateUrlFromRoute(...)); } diff --git a/src/Builder/Screenshot/AbstractChromiumScreenshotBuilder.php b/src/Builder/Screenshot/AbstractChromiumScreenshotBuilder.php index b41ba85..82d592c 100644 --- a/src/Builder/Screenshot/AbstractChromiumScreenshotBuilder.php +++ b/src/Builder/Screenshot/AbstractChromiumScreenshotBuilder.php @@ -11,6 +11,7 @@ use Sensiolabs\GotenbergBundle\Exception\InvalidBuilderConfiguration; use Sensiolabs\GotenbergBundle\Exception\ScreenshotPartRenderingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Mime\Part\DataPart; @@ -24,10 +25,11 @@ abstract class AbstractChromiumScreenshotBuilder extends AbstractScreenshotBuild public function __construct( GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, + WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, private readonly RequestStack $requestStack, private readonly Environment|null $twig = null, ) { - parent::__construct($gotenbergClient, $asset); + parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry); $normalizers = [ 'extraHttpHeaders' => function (mixed $value): array { diff --git a/src/Builder/Screenshot/AbstractScreenshotBuilder.php b/src/Builder/Screenshot/AbstractScreenshotBuilder.php index ecfc50f..0d68221 100644 --- a/src/Builder/Screenshot/AbstractScreenshotBuilder.php +++ b/src/Builder/Screenshot/AbstractScreenshotBuilder.php @@ -2,22 +2,28 @@ namespace Sensiolabs\GotenbergBundle\Builder\Screenshot; +use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderInterface; +use Sensiolabs\GotenbergBundle\Builder\AsyncBuilderTrait; use Sensiolabs\GotenbergBundle\Builder\DefaultBuilderTrait; use Sensiolabs\GotenbergBundle\Builder\DownloadFromTrait; use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; -abstract class AbstractScreenshotBuilder implements ScreenshotBuilderInterface +abstract class AbstractScreenshotBuilder implements ScreenshotBuilderInterface, AsyncBuilderInterface { + use AsyncBuilderTrait; use DefaultBuilderTrait; use DownloadFromTrait; public function __construct( GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, + WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, ) { $this->client = $gotenbergClient; $this->asset = $asset; + $this->webhookConfigurationRegistry = $webhookConfigurationRegistry; $this->normalizers = [ 'downloadFrom' => fn (array $value): array => $this->downloadFromNormalizer($value, $this->encodeData(...)), diff --git a/src/Builder/Screenshot/UrlScreenshotBuilder.php b/src/Builder/Screenshot/UrlScreenshotBuilder.php index 274e268..3442aff 100644 --- a/src/Builder/Screenshot/UrlScreenshotBuilder.php +++ b/src/Builder/Screenshot/UrlScreenshotBuilder.php @@ -5,6 +5,7 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Exception\MissingRequiredFieldException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RequestContext; @@ -19,11 +20,12 @@ final class UrlScreenshotBuilder extends AbstractChromiumScreenshotBuilder public function __construct( GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, + WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, RequestStack $requestStack, Environment|null $twig = null, private readonly UrlGeneratorInterface|null $urlGenerator = null, ) { - parent::__construct($gotenbergClient, $asset, $requestStack, $twig); + parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry, $requestStack, $twig); $this->addNormalizer('route', $this->generateUrlFromRoute(...)); } diff --git a/src/Client/GotenbergClient.php b/src/Client/GotenbergClient.php index 96d3ad5..3131227 100644 --- a/src/Client/GotenbergClient.php +++ b/src/Client/GotenbergClient.php @@ -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()); } diff --git a/src/Debug/Builder/TraceablePdfBuilder.php b/src/Debug/Builder/TraceablePdfBuilder.php index ccb5e78..5a19090 100644 --- a/src/Debug/Builder/TraceablePdfBuilder.php +++ b/src/Debug/Builder/TraceablePdfBuilder.php @@ -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; @@ -50,6 +51,30 @@ public function generate(): GotenbergFileResult return $response; } + public function generateAsync(): void + { + 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'); + $this->inner->generateAsync(); + $swEvent?->stop(); + + $this->pdfs[] = [ + 'calls' => $this->calls, + 'time' => $swEvent?->getDuration(), + 'memory' => $swEvent?->getMemory(), + 'size' => null, + 'fileName' => null, + ]; + + ++$this->totalGenerated; + } + /** * @param array $arguments */ diff --git a/src/Debug/Builder/TraceableScreenshotBuilder.php b/src/Debug/Builder/TraceableScreenshotBuilder.php index e2352d3..ba2c99c 100644 --- a/src/Debug/Builder/TraceableScreenshotBuilder.php +++ b/src/Debug/Builder/TraceableScreenshotBuilder.php @@ -2,6 +2,7 @@ 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; @@ -51,6 +52,30 @@ public function generate(): GotenbergFileResult return $response; } + public function generateAsync(): void + { + 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'); + $this->inner->generateAsync(); + $swEvent?->stop(); + + $this->screenshots[] = [ + 'calls' => $this->calls, + 'time' => $swEvent?->getDuration(), + 'memory' => $swEvent?->getMemory(), + 'size' => null, + 'fileName' => null, + ]; + + ++$this->totalGenerated; + } + /** * @param array $arguments */ diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 32c5c3a..ec08916 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -42,9 +42,13 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultTrue() ->info('Enables the listener on kernel.view to stream GotenbergFileResult object.') ->end() + ->append($this->addNamedWebhookDefinition()) ->arrayNode('default_options') ->addDefaultsIfNotSet() ->children() + ->scalarNode('webhook') + ->info('Webhook configuration name.') + ->end() ->arrayNode('pdf') ->addDefaultsIfNotSet() ->append($this->addPdfHtmlNode()) @@ -78,6 +82,7 @@ private function addPdfHtmlNode(): NodeDefinition ; $this->addChromiumPdfOptionsNode($treebuilder->getRootNode()); + $this->addWebhookDeclarationNode($treebuilder->getRootNode()); return $treebuilder->getRootNode(); } @@ -92,6 +97,7 @@ private function addPdfUrlNode(): NodeDefinition ; $this->addChromiumPdfOptionsNode($treebuilder->getRootNode()); + $this->addWebhookDeclarationNode($treebuilder->getRootNode()); return $treebuilder->getRootNode(); } @@ -106,6 +112,7 @@ private function addPdfMarkdownNode(): NodeDefinition ; $this->addChromiumPdfOptionsNode($treebuilder->getRootNode()); + $this->addWebhookDeclarationNode($treebuilder->getRootNode()); return $treebuilder->getRootNode(); } @@ -120,6 +127,7 @@ private function addScreenshotHtmlNode(): NodeDefinition ; $this->addChromiumScreenshotOptionsNode($treebuilder->getRootNode()); + $this->addWebhookDeclarationNode($treebuilder->getRootNode()); return $treebuilder->getRootNode(); } @@ -134,6 +142,7 @@ private function addScreenshotUrlNode(): NodeDefinition ; $this->addChromiumScreenshotOptionsNode($treebuilder->getRootNode()); + $this->addWebhookDeclarationNode($treebuilder->getRootNode()); return $treebuilder->getRootNode(); } @@ -148,6 +157,7 @@ private function addScreenshotMarkdownNode(): NodeDefinition ; $this->addChromiumScreenshotOptionsNode($treebuilder->getRootNode()); + $this->addWebhookDeclarationNode($treebuilder->getRootNode()); return $treebuilder->getRootNode(); } @@ -631,6 +641,148 @@ private function addPdfMetadata(): NodeDefinition ; } + private function addNamedWebhookDefinition(): NodeDefinition + { + $treeBuilder = new TreeBuilder('webhook'); + + return $treeBuilder->getRootNode() + ->defaultValue([]) + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('name') + ->validate() + ->ifTrue(static function (mixed $option): bool { + return !\is_string($option); + }) + ->thenInvalid('Invalid webhook configuration name %s') + ->end() + ->end() + ->append($this->addWebhookConfigurationNode('success')) + ->append($this->addWebhookConfigurationNode('error')) + ->arrayNode('extra_http_headers') + ->info('HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('name') + ->validate() + ->ifTrue(static function ($option) { + return !\is_string($option); + }) + ->thenInvalid('Invalid header name %s') + ->end() + ->end() + ->scalarNode('value') + ->validate() + ->ifTrue(static function ($option) { + return !\is_string($option); + }) + ->thenInvalid('Invalid header value %s') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(static function (mixed $option): bool { + return !isset($option['success']); + }) + ->thenInvalid('Invalid webhook configuration : At least a "success" key is required.') + ->end() + ->end(); + } + + private function addWebhookDeclarationNode(ArrayNodeDefinition $parent): void + { + $parent + ->children() + ->arrayNode('webhook') + ->info('Webhook configuration name or definition.') + ->beforeNormalization() + ->ifString() + ->then(static function (string $v): array { + return ['config_name' => $v]; + }) + ->end() + ->children() + ->scalarNode('config_name') + ->info('The name of the webhook configuration to use.') + ->end() + ->append($this->addWebhookConfigurationNode('success')) + ->append($this->addWebhookConfigurationNode('error')) + ->arrayNode('extra_http_headers') + ->info('HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('name') + ->validate() + ->ifTrue(static function ($option) { + return !\is_string($option); + }) + ->thenInvalid('Invalid header name %s') + ->end() + ->end() + ->scalarNode('value') + ->validate() + ->ifTrue(static function ($option) { + return !\is_string($option); + }) + ->thenInvalid('Invalid header value %s') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(static function ($option): bool { + return !isset($option['config_name']) && !isset($option['success']); + }) + ->thenInvalid('Invalid webhook configuration : either reference an existing webhook configuration or declare a new one with "success" and optionally "error" keys.') + ->end() + ->end(); + } + + private function addWebhookConfigurationNode(string $name): NodeDefinition + { + $treeBuilder = new TreeBuilder($name); + + return $treeBuilder->getRootNode() + ->children() + ->scalarNode('url') + ->info('The URL to call.') + ->end() + ->variableNode('route') + ->info('Route configuration.') + ->beforeNormalization() + ->ifArray() + ->then(function (array $v): array { + return [$v[0], $v[1] ?? []]; + }) + ->ifString() + ->then(function (string $v): array { + return [$v, []]; + }) + ->end() + ->validate() + ->ifTrue(function ($v): bool { + return !\is_array($v) || \count($v) !== 2 || !\is_string($v[0]) || !\is_array($v[1]); + }) + ->thenInvalid('The "route" parameter must be a string or an array containing a string and an array.') + ->end() + ->end() + ->enumNode('method') + ->info('HTTP method to use on that endpoint.') + ->values(['POST', 'PUT', 'PATCH']) + ->defaultNull() + ->end() + ->end() + ; + } + private function addDownloadFrom(): NodeDefinition { $treeBuilder = new TreeBuilder('download_from'); diff --git a/src/DependencyInjection/SensiolabsGotenbergExtension.php b/src/DependencyInjection/SensiolabsGotenbergExtension.php index 2b4fc80..bd1288d 100644 --- a/src/DependencyInjection/SensiolabsGotenbergExtension.php +++ b/src/DependencyInjection/SensiolabsGotenbergExtension.php @@ -12,13 +12,16 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\Routing\RequestContext; +/** + * @phpstan-type WebhookDefinition array{url?: string, route?: array{0: string, 1: array}, method?: 'POST'|'PUT'|'PATCH'|null} + */ class SensiolabsGotenbergExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); - /** @var array{base_uri: string, http_client: string, controller_listener: bool, request_context?: array{base_uri?: string}, assets_directory: string, default_options: array{pdf: array{html: array, url: array, markdown: array, office: array, merge: array, convert: array}, screenshot: array{html: array, url: array, markdown: array}}} $config */ + /** @var array{base_uri: string, http_client: string, controller_listener: bool, request_context?: array{base_uri?: string}, assets_directory: string, webhook: array, default_options: array{pdf: array{html: array, url: array, markdown: array, office: array, merge: array, convert: array}, screenshot: array{html: array, url: array, markdown: array}, webhook?: string}} $config */ $config = $this->processConfiguration($configuration, $configs); $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); @@ -55,6 +58,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->setAlias('sensiolabs_gotenberg.http_client', new Alias($config['http_client'], false)); $baseUri = $config['request_context']['base_uri'] ?? null; + $defaultWebhookConfig = $config['default_options']['webhook'] ?? null; if (null !== $baseUri) { $requestContextDefinition = new Definition(RequestContext::class); @@ -64,32 +68,28 @@ public function load(array $configs, ContainerBuilder $container): void $container->setDefinition('.sensiolabs_gotenberg.request_context', $requestContextDefinition); } - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.html'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['html'])]); + foreach ($config['webhook'] as $name => $configuration) { + $container->getDefinition('sensiolabs_gotenberg.webhook_configuration_registry') + ->addMethodCall('add', [$name, $configuration]); + } + + $this->processDefaultOptions('.sensiolabs_gotenberg.pdf_builder.html', $container, $config['default_options']['pdf']['html'], $defaultWebhookConfig); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.url'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['url'])]); + $this->processDefaultOptions('.sensiolabs_gotenberg.pdf_builder.url', $container, $config['default_options']['pdf']['url'], $defaultWebhookConfig); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.markdown'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['markdown'])]); + $this->processDefaultOptions('.sensiolabs_gotenberg.pdf_builder.markdown', $container, $config['default_options']['pdf']['markdown'], $defaultWebhookConfig); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.office'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['office'])]); + $this->processDefaultOptions('.sensiolabs_gotenberg.pdf_builder.office', $container, $config['default_options']['pdf']['office'], $defaultWebhookConfig); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.merge'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['merge'])]); + $this->processDefaultOptions('.sensiolabs_gotenberg.pdf_builder.merge', $container, $config['default_options']['pdf']['merge'], $defaultWebhookConfig); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.convert'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['convert'])]); + $this->processDefaultOptions('.sensiolabs_gotenberg.pdf_builder.convert', $container, $config['default_options']['pdf']['convert'], $defaultWebhookConfig); - $definition = $container->getDefinition('.sensiolabs_gotenberg.screenshot_builder.html'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['screenshot']['html'])]); + $this->processDefaultOptions('.sensiolabs_gotenberg.screenshot_builder.html', $container, $config['default_options']['screenshot']['html'], $defaultWebhookConfig); - $definition = $container->getDefinition('.sensiolabs_gotenberg.screenshot_builder.url'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['screenshot']['url'])]); + $this->processDefaultOptions('.sensiolabs_gotenberg.screenshot_builder.url', $container, $config['default_options']['screenshot']['url'], $defaultWebhookConfig); - $definition = $container->getDefinition('.sensiolabs_gotenberg.screenshot_builder.markdown'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['screenshot']['markdown'])]); + $this->processDefaultOptions('.sensiolabs_gotenberg.screenshot_builder.markdown', $container, $config['default_options']['screenshot']['markdown'], $defaultWebhookConfig); $definition = $container->getDefinition('sensiolabs_gotenberg.asset.base_dir_formatter'); $definition->replaceArgument(2, $config['assets_directory']); @@ -102,8 +102,37 @@ public function load(array $configs, ContainerBuilder $container): void */ private function cleanUserOptions(array $userConfigurations): array { - return array_filter($userConfigurations, static function ($config): bool { - return null !== $config; - }); + return array_filter($userConfigurations, static function ($config, $configName): bool { + return null !== $config && 'webhook' !== $configName; + }, \ARRAY_FILTER_USE_BOTH); + } + + /** + * @param array $config + */ + private function processDefaultOptions(string $serviceId, ContainerBuilder $container, array $config, string|null $defaultWebhookName): void + { + $definition = $container->getDefinition($serviceId); + $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config)]); + + $webhookConfig = $config['webhook'] ?? null; + if (null === $webhookConfig && null === $defaultWebhookName) { + return; + } + + if (null !== $defaultWebhookName) { + $definition->addMethodCall('webhookConfiguration', [$defaultWebhookName], true); + + return; + } + + if (\array_key_exists('config_name', $webhookConfig) && \is_string($webhookConfig['config_name'])) { + $name = $webhookConfig['config_name']; + } else { + $name = $serviceId.'_webhook_config'; + $container->getDefinition('sensiolabs_gotenberg.webhook_configuration_registry') + ->addMethodCall('add', [$name, $webhookConfig]); + } + $definition->addMethodCall('webhookConfiguration', [$name], true); } } diff --git a/src/EventListener/ProcessBuilderOnControllerResponse.php b/src/EventListener/ProcessBuilderOnControllerResponse.php index c24d6c8..5de2dad 100644 --- a/src/EventListener/ProcessBuilderOnControllerResponse.php +++ b/src/EventListener/ProcessBuilderOnControllerResponse.php @@ -1,7 +1,5 @@ + * }> + */ + private array $configurations = []; + + public function __construct( + private readonly UrlGeneratorInterface $urlGenerator, + private readonly RequestContext|null $requestContext, + ) { + } + + /** + * @param array{success: WebhookDefinition, error?: WebhookDefinition, extra_http_headers?: array} $configuration + */ + public function add(string $name, array $configuration): void + { + $requestContext = $this->urlGenerator->getContext(); + if (null !== $this->requestContext) { + $this->urlGenerator->setContext($this->requestContext); + } + + try { + $success = [ + 'url' => $this->processWebhookConfiguration($configuration['success']), + 'method' => $configuration['success']['method'] ?? null, + ]; + $error = $success; + + if (isset($configuration['error'])) { + $error = [ + 'url' => $this->processWebhookConfiguration($configuration['error']), + 'method' => $configuration['error']['method'] ?? null, + ]; + } + + $namedConfiguration = ['success' => $success, 'error' => $error]; + + if (\array_key_exists('extra_http_headers', $configuration) && [] !== $configuration['extra_http_headers']) { + $namedConfiguration['extra_http_headers'] = $configuration['extra_http_headers']; + } + + $this->configurations[$name] = $namedConfiguration; + } finally { + $this->urlGenerator->setContext($requestContext); + } + } + + public function get(string $name): array + { + if (!\array_key_exists($name, $this->configurations)) { + throw new WebhookConfigurationException("Webhook configuration \"{$name}\" not found."); + } + + return $this->configurations[$name]; + } + + /** + * @param WebhookDefinition $webhookDefinition + * + * @throws WebhookConfigurationException + */ + private function processWebhookConfiguration(array $webhookDefinition): string + { + if (isset($webhookDefinition['url'])) { + return $webhookDefinition['url']; + } + + if (isset($webhookDefinition['route'])) { + return $this->urlGenerator->generate($webhookDefinition['route'][0], $webhookDefinition['route'][1], UrlGeneratorInterface::ABSOLUTE_URL); + } + + throw new WebhookConfigurationException('Invalid webhook configuration'); + } +} diff --git a/src/Webhook/WebhookConfigurationRegistryInterface.php b/src/Webhook/WebhookConfigurationRegistryInterface.php new file mode 100644 index 0000000..48f2dd2 --- /dev/null +++ b/src/Webhook/WebhookConfigurationRegistryInterface.php @@ -0,0 +1,33 @@ +}, method?: 'POST'|'PUT'|'PATCH'|null} + */ +interface WebhookConfigurationRegistryInterface +{ + /** + * @param array{success: WebhookDefinition, error?: WebhookDefinition} $configuration + */ + public function add(string $name, array $configuration): void; + + /** + * @return array{ + * success: array{ + * url: string, + * method: 'POST'|'PUT'|'PATCH'|null, + * }, + * error: array{ + * url: string, + * method: 'POST'|'PUT'|'PATCH'|null, + * }, + * extra_http_headers?: array + * } + * + * @throws WebhookConfigurationException if configuration not found + */ + public function get(string $name): array; +} diff --git a/tests/Builder/AbstractBuilderTestCase.php b/tests/Builder/AbstractBuilderTestCase.php index 241bbdf..da3e0d2 100644 --- a/tests/Builder/AbstractBuilderTestCase.php +++ b/tests/Builder/AbstractBuilderTestCase.php @@ -7,6 +7,7 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Sensiolabs\GotenbergBundle\Twig\GotenbergAssetExtension; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Mime\Part\DataPart; use Twig\Environment; @@ -25,6 +26,11 @@ abstract class AbstractBuilderTestCase extends TestCase */ protected GotenbergClientInterface $gotenbergClient; + /** + * @var MockObject&WebhookConfigurationRegistryInterface + */ + protected WebhookConfigurationRegistryInterface $webhookConfigurationRegistry; + public static function setUpBeforeClass(): void { self::$twig = new Environment(new FilesystemLoader(self::FIXTURE_DIR), [ @@ -37,6 +43,7 @@ public static function setUpBeforeClass(): void protected function setUp(): void { $this->gotenbergClient = $this->createMock(GotenbergClientInterface::class); + $this->webhookConfigurationRegistry = $this->createMock(WebhookConfigurationRegistryInterface::class); } /** diff --git a/tests/Builder/Pdf/AbstractChromiumPdfBuilderTest.php b/tests/Builder/Pdf/AbstractChromiumPdfBuilderTest.php index 3d068f5..5bcc7ff 100644 --- a/tests/Builder/Pdf/AbstractChromiumPdfBuilderTest.php +++ b/tests/Builder/Pdf/AbstractChromiumPdfBuilderTest.php @@ -453,7 +453,7 @@ public function testThrowIfTwigTemplateIsInvalid(): void private function getChromiumPdfBuilder(bool $twig = true, RequestStack $requestStack = new RequestStack()): AbstractChromiumPdfBuilder { - return new class($this->gotenbergClient, self::$assetBaseDirFormatter, $requestStack, true === $twig ? self::$twig : null) extends AbstractChromiumPdfBuilder { + return new class($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry, $requestStack, true === $twig ? self::$twig : null) extends AbstractChromiumPdfBuilder { protected function getEndpoint(): string { return '/fake/endpoint'; diff --git a/tests/Builder/Pdf/AbstractPdfBuilderTest.php b/tests/Builder/Pdf/AbstractPdfBuilderTest.php index c69de4f..d9a6364 100644 --- a/tests/Builder/Pdf/AbstractPdfBuilderTest.php +++ b/tests/Builder/Pdf/AbstractPdfBuilderTest.php @@ -14,6 +14,7 @@ use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Sensiolabs\GotenbergBundle\Processor\NullProcessor; use Sensiolabs\GotenbergBundle\Tests\Builder\AbstractBuilderTestCase; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\HeaderUtils; @@ -76,13 +77,13 @@ public function testNativeNormalizers(string $key, mixed $raw, mixed $expected): */ private function getPdfBuilder(array $formFields = []): AbstractPdfBuilder { - return (new class($this->gotenbergClient, self::$assetBaseDirFormatter, $formFields) extends AbstractPdfBuilder { + return (new class($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry, $formFields) extends AbstractPdfBuilder { /** * @param array $formFields */ - public function __construct(GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, array $formFields = []) + public function __construct(GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, array $formFields = []) { - parent::__construct($gotenbergClient, $asset); + parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry); $this->formFields = $formFields; } diff --git a/tests/Builder/Pdf/ConvertPdfBuilderTest.php b/tests/Builder/Pdf/ConvertPdfBuilderTest.php index b2384d6..ce7cc4a 100644 --- a/tests/Builder/Pdf/ConvertPdfBuilderTest.php +++ b/tests/Builder/Pdf/ConvertPdfBuilderTest.php @@ -91,7 +91,7 @@ public function testRequiredPdfFile(): void private function getConvertPdfBuilder(): ConvertPdfBuilder { - return (new ConvertPdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter)) + return (new ConvertPdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry)) ->processor(new NullProcessor()) ; } diff --git a/tests/Builder/Pdf/HtmlPdfBuilderTest.php b/tests/Builder/Pdf/HtmlPdfBuilderTest.php index 1ce3dca..1366fd5 100644 --- a/tests/Builder/Pdf/HtmlPdfBuilderTest.php +++ b/tests/Builder/Pdf/HtmlPdfBuilderTest.php @@ -111,7 +111,7 @@ public function testRequiredFormData(): void private function getHtmlPdfBuilder(bool $twig = true): HtmlPdfBuilder { - return (new HtmlPdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, new RequestStack(), true === $twig ? self::$twig : null)) + return (new HtmlPdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry, new RequestStack(), true === $twig ? self::$twig : null)) ->processor(new NullProcessor()) ; } diff --git a/tests/Builder/Pdf/LibreOfficePdfBuilderTest.php b/tests/Builder/Pdf/LibreOfficePdfBuilderTest.php index 9a0b0f1..db2274b 100644 --- a/tests/Builder/Pdf/LibreOfficePdfBuilderTest.php +++ b/tests/Builder/Pdf/LibreOfficePdfBuilderTest.php @@ -168,7 +168,7 @@ public function testRequiredFormData(): void private function getLibreOfficePdfBuilder(): LibreOfficePdfBuilder { - return (new LibreOfficePdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter)) + return (new LibreOfficePdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry)) ->processor(new NullProcessor()) ; } diff --git a/tests/Builder/Pdf/MarkdownPdfBuilderTest.php b/tests/Builder/Pdf/MarkdownPdfBuilderTest.php index cccfc05..668fdde 100644 --- a/tests/Builder/Pdf/MarkdownPdfBuilderTest.php +++ b/tests/Builder/Pdf/MarkdownPdfBuilderTest.php @@ -68,7 +68,7 @@ public function testRequiredMarkdownFile(): void private function getMarkdownPdfBuilder(bool $twig = true): MarkdownPdfBuilder { - return (new MarkdownPdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, new RequestStack(), true === $twig ? self::$twig : null)) + return (new MarkdownPdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry, new RequestStack(), true === $twig ? self::$twig : null)) ->processor(new NullProcessor()) ; } diff --git a/tests/Builder/Pdf/MergePdfBuilderTest.php b/tests/Builder/Pdf/MergePdfBuilderTest.php index d1fff0d..89cbc2a 100644 --- a/tests/Builder/Pdf/MergePdfBuilderTest.php +++ b/tests/Builder/Pdf/MergePdfBuilderTest.php @@ -85,7 +85,7 @@ public function testRequiredFormData(): void private function getMergePdfBuilder(): MergePdfBuilder { - return (new MergePdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter)) + return (new MergePdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry)) ->processor(new NullProcessor()) ; } diff --git a/tests/Builder/Pdf/UrlPdfBuilderTest.php b/tests/Builder/Pdf/UrlPdfBuilderTest.php index f16b38e..b4fa54c 100644 --- a/tests/Builder/Pdf/UrlPdfBuilderTest.php +++ b/tests/Builder/Pdf/UrlPdfBuilderTest.php @@ -132,7 +132,14 @@ public function testRequiredEitherUrlOrRouteNotBoth(): void private function getUrlPdfBuilder(UrlGeneratorInterface|null $urlGenerator = null): UrlPdfBuilder { - return (new UrlPdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, new RequestStack(), urlGenerator: $urlGenerator)) + return (new UrlPdfBuilder( + $this->gotenbergClient, + self::$assetBaseDirFormatter, + $this->webhookConfigurationRegistry, + new RequestStack(), + null, + $urlGenerator) + ) ->processor(new NullProcessor()) ; } diff --git a/tests/Builder/Screenshot/AbstractChromiumScreenshotBuilderTest.php b/tests/Builder/Screenshot/AbstractChromiumScreenshotBuilderTest.php index 0fb5030..a9a1bf6 100644 --- a/tests/Builder/Screenshot/AbstractChromiumScreenshotBuilderTest.php +++ b/tests/Builder/Screenshot/AbstractChromiumScreenshotBuilderTest.php @@ -85,7 +85,7 @@ public function testConfigurationIsCorrectlySet(string $key, mixed $value, array private function getChromiumScreenshotBuilder(bool $twig = true): AbstractChromiumScreenshotBuilder { - return new class($this->gotenbergClient, self::$assetBaseDirFormatter, new RequestStack(), true === $twig ? self::$twig : null) extends AbstractChromiumScreenshotBuilder { + return new class($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry, new RequestStack(), true === $twig ? self::$twig : null) extends AbstractChromiumScreenshotBuilder { protected function getEndpoint(): string { return '/fake/endpoint'; diff --git a/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php b/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php index f34bf50..c47f291 100644 --- a/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php +++ b/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php @@ -14,6 +14,7 @@ use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Sensiolabs\GotenbergBundle\Processor\NullProcessor; use Sensiolabs\GotenbergBundle\Tests\Builder\AbstractBuilderTestCase; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistryInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\HeaderUtils; @@ -74,13 +75,13 @@ public function testNativeNormalizers(string $key, mixed $raw, mixed $expected): */ private function getScreenshotBuilder(array $formFields = []): AbstractScreenshotBuilder { - return (new class($this->gotenbergClient, self::$assetBaseDirFormatter, $formFields) extends AbstractScreenshotBuilder { + return (new class($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry, $formFields) extends AbstractScreenshotBuilder { /** * @param array $formFields */ - public function __construct(GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, array $formFields = []) + public function __construct(GotenbergClientInterface $gotenbergClient, AssetBaseDirFormatter $asset, WebhookConfigurationRegistryInterface $webhookConfigurationRegistry, array $formFields = []) { - parent::__construct($gotenbergClient, $asset); + parent::__construct($gotenbergClient, $asset, $webhookConfigurationRegistry); $this->formFields = $formFields; } diff --git a/tests/Builder/Screenshot/HtmlScreenshotBuilderTest.php b/tests/Builder/Screenshot/HtmlScreenshotBuilderTest.php index 8fe7aea..63f5990 100644 --- a/tests/Builder/Screenshot/HtmlScreenshotBuilderTest.php +++ b/tests/Builder/Screenshot/HtmlScreenshotBuilderTest.php @@ -111,7 +111,7 @@ public function testRequiredFormData(): void private function getHtmlScreenshotBuilder(bool $twig = true): HtmlScreenshotBuilder { - return (new HtmlScreenshotBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, new RequestStack(), true === $twig ? self::$twig : null)) + return (new HtmlScreenshotBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry, new RequestStack(), true === $twig ? self::$twig : null)) ->processor(new NullProcessor()) ; } diff --git a/tests/Builder/Screenshot/MarkdownScreenshotBuilderTest.php b/tests/Builder/Screenshot/MarkdownScreenshotBuilderTest.php index d8511b2..7d7edaa 100644 --- a/tests/Builder/Screenshot/MarkdownScreenshotBuilderTest.php +++ b/tests/Builder/Screenshot/MarkdownScreenshotBuilderTest.php @@ -96,7 +96,7 @@ public function testRequiredMarkdownFile(): void private function getMarkdownScreenshotBuilder(bool $twig = true): MarkdownScreenshotBuilder { - return (new MarkdownScreenshotBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, new RequestStack(), true === $twig ? self::$twig : null)) + return (new MarkdownScreenshotBuilder($this->gotenbergClient, self::$assetBaseDirFormatter, $this->webhookConfigurationRegistry, new RequestStack(), true === $twig ? self::$twig : null)) ->processor(new NullProcessor()) ; } diff --git a/tests/Builder/Screenshot/UrlScreenshotBuilderTest.php b/tests/Builder/Screenshot/UrlScreenshotBuilderTest.php index 9cc4e7c..0f70d25 100644 --- a/tests/Builder/Screenshot/UrlScreenshotBuilderTest.php +++ b/tests/Builder/Screenshot/UrlScreenshotBuilderTest.php @@ -77,6 +77,7 @@ private function getUrlScreenshotBuilder(bool $twig = true): UrlScreenshotBuilde return (new UrlScreenshotBuilder( $this->gotenbergClient, self::$assetBaseDirFormatter, + $this->webhookConfigurationRegistry, new RequestStack(), true === $twig ? self::$twig : null, $this->router, diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 79cc05b..7b13884 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -148,7 +148,7 @@ public static function providePaperSizesConfigurations(): iterable #[DataProvider('providePaperSizesConfigurations')] public function testExceptionOnPaperSizesConfigurations(array $configuration): void { - self::expectException(InvalidConfigurationException::class); + $this->expectException(InvalidConfigurationException::class); $processor = new Processor(); $processor->processConfiguration(new Configuration(), [ @@ -163,8 +163,55 @@ public function testExceptionOnPaperSizesConfigurations(array $configuration): v ]); } + /** + * @return \Generator>}}> + */ + public static function invalidWebhookConfigurationProvider(): \Generator + { + yield 'webhook definition without "success" and "error" keys' => [ + [['webhook' => ['foo' => ['some_key' => ['url' => 'http://example.com']]]]], + ]; + yield 'webhook definition without "success" key' => [ + [['webhook' => ['foo' => ['error' => ['url' => 'http://example.com']]]]], + ]; + yield 'webhook definition without name' => [ + [['webhook' => [['success' => ['url' => 'http://example.com']], ['error' => ['url' => 'http://example.com/error']]]]], + ]; + yield 'webhook definition with invalid "url" key' => [ + [['webhook' => ['foo' => ['success' => ['url' => ['array_element']]]]]], + ]; + yield 'webhook definition with array of string as "route" key' => [ + [['webhook' => ['foo' => ['success' => ['route' => ['array_element']]]]]], + ]; + yield 'webhook definition with array of two strings as "route" key' => [ + [['webhook' => ['foo' => ['success' => ['route' => ['array_element', 'array_element_2']]]]]], + ]; + yield 'webhook definition in default webhook declaration' => [ + [['default_options' => ['webhook' => ['success' => ['url' => 'http://example.com']]]]], + ]; + } + + /** + * @param array> $config + * + * @dataProvider invalidWebhookConfigurationProvider + */ + #[DataProvider('invalidWebhookConfigurationProvider')] + public function testInvalidWebhookConfiguration(array $config): void + { + $this->expectException(InvalidConfigurationException::class); + $processor = new Processor(); + $processor->processConfiguration( + new Configuration(), + $config, + ); + } + /** * @return array{ + * 'assets_directory': string, + * 'http_client': string, + * 'webhook': array, * 'default_options': array{ * 'pdf': array{ * 'html': array, @@ -182,6 +229,7 @@ private static function getBundleDefaultConfig(): array return [ 'assets_directory' => '%kernel.project_dir%/assets', 'http_client' => 'http_client', + 'webhook' => [], 'controller_listener' => true, 'default_options' => [ 'pdf' => [ diff --git a/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php b/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php index b7b97b2..9769eb6 100644 --- a/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php +++ b/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php @@ -29,7 +29,8 @@ public function testGotenbergConfiguredWithValidConfig(): void $extension = new SensiolabsGotenbergExtension(); $containerBuilder = $this->getContainerBuilder(); - $extension->load(self::getValidConfig(), $containerBuilder); + $validConfig = self::getValidConfig(); + $extension->load($validConfig, $containerBuilder); $list = [ 'pdf' => [ @@ -407,9 +408,124 @@ public function testDataCollectorIsProperlyConfiguredIfEnabled(): void ], $dataCollectorOptions); } + public function testBuilderWebhookConfiguredWithDefaultConfiguration(): void + { + $extension = new SensiolabsGotenbergExtension(); + + $containerBuilder = $this->getContainerBuilder(); + $extension->load([['http_client' => 'http_client']], $containerBuilder); + + self::assertEmpty($containerBuilder->getDefinition('sensiolabs_gotenberg.webhook_configuration_registry')->getMethodCalls()); + + $buildersIds = [ + '.sensiolabs_gotenberg.pdf_builder.html', + '.sensiolabs_gotenberg.pdf_builder.url', + '.sensiolabs_gotenberg.pdf_builder.markdown', + '.sensiolabs_gotenberg.pdf_builder.office', + '.sensiolabs_gotenberg.screenshot_builder.html', + '.sensiolabs_gotenberg.screenshot_builder.url', + '.sensiolabs_gotenberg.screenshot_builder.markdown', + ]; + + foreach ($buildersIds as $builderId) { + $builderDefinition = $containerBuilder->getDefinition($builderId); + $methodCalls = $builderDefinition->getMethodCalls(); + self::assertNotContains('webhookConfiguration', $methodCalls); + } + } + + public function testBuilderWebhookConfiguredWithValidConfiguration(): void + { + $extension = new SensiolabsGotenbergExtension(); + + $containerBuilder = $this->getContainerBuilder(); + $extension->load([[ + 'http_client' => 'http_client', + 'webhook' => [ + 'foo' => ['success' => ['url' => 'https://sensiolabs.com/webhook'], 'error' => ['route' => 'simple_route']], + 'baz' => ['success' => ['route' => ['array_route', ['param1', 'param2']]]], + ], + 'default_options' => [ + 'webhook' => 'foo', + 'pdf' => [ + 'html' => ['webhook' => 'bar'], + 'url' => ['webhook' => 'baz'], + 'markdown' => ['webhook' => ['success' => ['url' => 'https://sensiolabs.com/webhook-on-the-fly']]], + ], + 'screenshot' => [ + 'html' => ['webhook' => 'foo'], + 'url' => ['webhook' => 'bar'], + 'markdown' => ['webhook' => 'baz'], + ], + ], + ]], $containerBuilder); + + $expectedConfigurationMapping = [ + '.sensiolabs_gotenberg.pdf_builder.html' => 'bar', + '.sensiolabs_gotenberg.pdf_builder.url' => 'baz', + '.sensiolabs_gotenberg.pdf_builder.markdown' => '.sensiolabs_gotenberg.pdf_builder.markdown_webhook_config', + '.sensiolabs_gotenberg.pdf_builder.office' => 'foo', + '.sensiolabs_gotenberg.screenshot_builder.html' => 'foo', + '.sensiolabs_gotenberg.screenshot_builder.url' => 'bar', + '.sensiolabs_gotenberg.screenshot_builder.markdown' => 'baz', + ]; + array_map(static function (string $builderId, string $expectedConfigurationName) use ($containerBuilder): void { + foreach ($containerBuilder->getDefinition($builderId)->getMethodCalls() as $methodCall) { + [$name, $arguments] = $methodCall; + if ('webhookConfiguration' === $name) { + self::assertSame($expectedConfigurationName, $arguments[0]); + + return; + } + } + }, array_keys($expectedConfigurationMapping), array_values($expectedConfigurationMapping)); + + $webhookConfigurationRegistryDefinition = $containerBuilder->getDefinition('sensiolabs_gotenberg.webhook_configuration_registry'); + $methodCalls = $webhookConfigurationRegistryDefinition->getMethodCalls(); + self::assertCount(3, $methodCalls); + foreach ($methodCalls as $methodCall) { + [$name, $arguments] = $methodCall; + self::assertSame('add', $name); + self::assertContains($arguments[0], ['foo', 'baz', '.sensiolabs_gotenberg.pdf_builder.markdown_webhook_config']); + self::assertSame(match ($arguments[0]) { + 'foo' => [ + 'success' => [ + 'url' => 'https://sensiolabs.com/webhook', + 'method' => null, + ], + 'error' => [ + 'route' => ['simple_route', []], + 'method' => null, + ], + 'extra_http_headers' => [], + ], + 'baz' => [ + 'success' => [ + 'route' => ['array_route', ['param1', 'param2']], + 'method' => null, + ], + 'extra_http_headers' => [], + ], + '.sensiolabs_gotenberg.pdf_builder.markdown_webhook_config' => [ + 'success' => [ + 'url' => 'https://sensiolabs.com/webhook-on-the-fly', + 'method' => null, + ], + 'extra_http_headers' => [], + ], + default => self::fail('Unexpected webhook configuration'), + }, $arguments[1]); + } + } + /** * @return array}, 'webhook'?: string}, + * 'error'?: array{'url'?: string, 'route'?: string|array{0: string, 1: list}, 'webhook'?: string} + * }>, * 'default_options': array{ + * 'webhook': string, * 'pdf': array{ * 'html': array, * 'url': array, @@ -431,7 +547,12 @@ private static function getValidConfig(): array return [ [ 'http_client' => 'http_client', + 'webhook' => [ + 'foo' => ['success' => ['url' => 'https://sensiolabs.com/webhook'], 'error' => ['route' => 'simple_route']], + 'baz' => ['success' => ['url' => 'https://sensiolabs.com/single-url-webhook']], + ], 'default_options' => [ + 'webhook' => 'foo', 'pdf' => [ 'html' => [ 'paper_standard_size' => 'A4', @@ -462,6 +583,7 @@ private static function getValidConfig(): array 'skip_network_idle_event' => true, 'pdf_format' => PdfFormat::Pdf1b->value, 'pdf_universal_access' => true, + 'webhook' => 'bar', ], 'url' => [ 'paper_width' => 21, @@ -485,6 +607,7 @@ private static function getValidConfig(): array 'skip_network_idle_event' => false, 'pdf_format' => PdfFormat::Pdf2b->value, 'pdf_universal_access' => false, + // 'webhook' => ['success' => ''] ], 'markdown' => [ 'paper_width' => 30, diff --git a/tests/DependencyInjection/WebhookConfigurationRegistryTest.php b/tests/DependencyInjection/WebhookConfigurationRegistryTest.php new file mode 100644 index 0000000..7ad4c67 --- /dev/null +++ b/tests/DependencyInjection/WebhookConfigurationRegistryTest.php @@ -0,0 +1,114 @@ +}} + */ +#[CoversClass(WebhookConfigurationRegistry::class)] +final class WebhookConfigurationRegistryTest extends TestCase +{ + public function testGetUndefinedConfiguration(): void + { + $this->expectException(WebhookConfigurationException::class); + $this->expectExceptionMessage('Webhook configuration "undefined" not found.'); + + $registry = new WebhookConfigurationRegistry($this->createMock(UrlGeneratorInterface::class), null); + $registry->get('undefined'); + } + + public function testAddConfigurationUsingCustomContext(): void + { + $requestContext = $this->createMock(RequestContext::class); + $urlGenerator = $this->getUrlGenerator($requestContext); + $registry = new WebhookConfigurationRegistry($urlGenerator, $requestContext); + $registry->add('test', ['success' => ['url' => 'http://example.com/success']]); + } + + public function testOverrideConfiguration(): void + { + $registry = new WebhookConfigurationRegistry($this->createMock(UrlGeneratorInterface::class), null); + $registry->add('test', ['success' => ['url' => 'http://example.com/success']]); + $this->assertSame(['success' => ['url' => 'http://example.com/success', 'method' => null], 'error' => ['url' => 'http://example.com/success', 'method' => null]], $registry->get('test')); + $registry->add('test', ['success' => ['url' => 'http://example.com/override']]); + $this->assertSame(['success' => ['url' => 'http://example.com/override', 'method' => null], 'error' => ['url' => 'http://example.com/override', 'method' => null]], $registry->get('test')); + } + + /** + * @return \Generator + */ + public static function configurationProvider(): \Generator + { + yield 'full definition with urls' => [ + ['success' => ['url' => 'http://example.com/success'], 'error' => ['url' => 'http://example.com/error']], + ['success' => ['url' => 'http://example.com/success', 'method' => null], 'error' => ['url' => 'http://example.com/error', 'method' => null]], + ]; + yield 'full definition with routes' => [ + ['success' => ['route' => ['test_route_success', ['param' => 'value']]], 'error' => ['route' => ['test_route_error', ['param' => 'value']]]], + ['success' => ['url' => 'http://localhost/test_route?param=value', 'method' => null], 'error' => ['url' => 'http://localhost/test_route?param=value', 'method' => null]], + ]; + yield 'partial definition with urls' => [ + ['success' => ['url' => 'http://example.com/success']], + ['success' => ['url' => 'http://example.com/success', 'method' => null], 'error' => ['url' => 'http://example.com/success', 'method' => null]], + ]; + yield 'partial definition with routes' => [ + ['success' => ['route' => ['test_route_success', ['param' => 'value']]], + 'error' => ['route' => ['test_route_error', ['param' => 'value']]], + ], + ['success' => ['url' => 'http://localhost/test_route?param=value', 'method' => null], 'error' => ['url' => 'http://localhost/test_route?param=value', 'method' => null]], + ]; + yield 'mixed definition with url and route' => [ + ['success' => ['url' => 'http://example.com/success'], 'error' => ['route' => ['test_route_error', ['param' => 'value']]], + ], + ['success' => ['url' => 'http://example.com/success', 'method' => null], 'error' => ['url' => 'http://localhost/test_route?param=value', 'method' => null]], + ]; + } + + /** + * @param array{success: WebhookDefinition, error?: WebhookDefinition} $configuration + * @param array{success: string, error: string} $expectedUrls + * + * @throws Exception + */ + #[DataProvider('configurationProvider')] + public function testAddConfiguration(array $configuration, array $expectedUrls): void + { + $registry = new WebhookConfigurationRegistry($this->getUrlGenerator(), null); + $registry->add('test', $configuration); + + $this->assertSame($expectedUrls, $registry->get('test')); + } + + private function getUrlGenerator(RequestContext|null $requestContext = null): UrlGeneratorInterface&MockObject + { + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $originalContext = $this->createMock(RequestContext::class); + $urlGenerator->expects(self::once())->method('getContext')->willReturn($originalContext); + $urlGenerator->expects(self::exactly(null !== $requestContext ? 2 : 1)) + ->method('setContext') + ->willReturnCallback(function (RequestContext $context) use ($originalContext, $requestContext): void { + match ($context) { + $requestContext, $originalContext => null, + default => self::fail('setContext was called with an unexpected context.'), + }; + }); + $urlGenerator->method('generate')->willReturnMap([ + ['test_route_success', ['param' => 'value'], UrlGeneratorInterface::ABSOLUTE_URL, 'http://localhost/test_route?param=value'], + ['test_route_error', ['param' => 'value'], UrlGeneratorInterface::ABSOLUTE_URL, 'http://localhost/test_route?param=value'], + ['_webhook_controller', ['type' => 'my_success_webhook'], UrlGeneratorInterface::ABSOLUTE_URL, 'http://localhost/webhook/success'], + ['_webhook_controller', ['type' => 'my_error_webhook'], UrlGeneratorInterface::ABSOLUTE_URL, 'http://localhost/webhook/error'], + ]); + + return $urlGenerator; + } +} diff --git a/tests/GotenbergPdfTest.php b/tests/GotenbergPdfTest.php index e8b099b..0c7d56a 100644 --- a/tests/GotenbergPdfTest.php +++ b/tests/GotenbergPdfTest.php @@ -21,6 +21,7 @@ use Sensiolabs\GotenbergBundle\GotenbergPdf; use Sensiolabs\GotenbergBundle\GotenbergPdfInterface; use Sensiolabs\GotenbergBundle\SensiolabsGotenbergBundle; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistry; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Mime\Part\DataPart; @@ -42,6 +43,7 @@ #[UsesClass(SensiolabsGotenbergExtension::class)] #[UsesClass(SensiolabsGotenbergBundle::class)] #[UsesClass(Unit::class)] +#[UsesClass(WebhookConfigurationRegistry::class)] final class GotenbergPdfTest extends KernelTestCase { public function testUrlBuilderFactory(): void diff --git a/tests/GotenbergScreenshotTest.php b/tests/GotenbergScreenshotTest.php index 7309bb4..82fcb68 100644 --- a/tests/GotenbergScreenshotTest.php +++ b/tests/GotenbergScreenshotTest.php @@ -15,6 +15,7 @@ use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Sensiolabs\GotenbergBundle\GotenbergScreenshot; use Sensiolabs\GotenbergBundle\GotenbergScreenshotInterface; +use Sensiolabs\GotenbergBundle\Webhook\WebhookConfigurationRegistry; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Mime\Part\DataPart; @@ -30,6 +31,7 @@ #[UsesClass(Filesystem::class)] #[UsesClass(TraceableScreenshotBuilder::class)] #[UsesClass(TraceableGotenbergScreenshot::class)] +#[UsesClass(WebhookConfigurationRegistry::class)] final class GotenbergScreenshotTest extends KernelTestCase { public function testUrlBuilderFactory(): void