diff --git a/.gitignore b/.gitignore index 1a16dd58..f32bcb1e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .php-cs-fixer.cache .phpunit.result.cache composer.lock +phpstan.neon diff --git a/README.md b/README.md index 6a85ce5e..a115ae25 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ class YourController 1. [Configuration](./docs/configuration.md) 2. [Working with assets](./docs/assets.md) 3. [Builders API](./docs/builders_api.md) +4. [Async & Webhooks](./docs/webhook.md) #### PDF diff --git a/config/builder_pdf.php b/config/builder_pdf.php index a150f137..349c960d 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 7e04c109..7ecb4cb2 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 caebd154..422b6e73 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,12 @@ $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(), + ]) + ->alias(WebhookConfigurationRegistryInterface::class, '.sensiolabs_gotenberg.webhook_configuration_registry') + ; }; diff --git a/docs/async/native.md b/docs/async/native.md new file mode 100644 index 00000000..3200fc5d --- /dev/null +++ b/docs/async/native.md @@ -0,0 +1,199 @@ +## Using the native feature + +Gotenberg [allows](https://gotenberg.dev/docs/configuration#webhook) to defer the generation of your files through webhooks. +When it is done creating your file, it calls back whatever header you sent. + +To use this feature you need two things : +- Send the appropriate headers +- use `->generateAsync()` method + +### Through Bundle configuration + +Using bundle configuration you can define : +- named configurations +- default named configuration +- per context (PDF+HTML, PDF+URL, SCREENSHOT+MARKDOWN) + +```yaml +# config/packages/sensiolabs_gotenberg.yaml + +sensiolabs_gotenberg: + + webhook: + + # Prototype + name: + name: ~ + success: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + error: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + + # HTTP headers to send back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook + extra_http_headers: + + # Prototype + name: + name: ~ + value: ~ + default_options: + + # Webhook configuration name. + webhook: ~ +``` + +Each named configuration requires at least a `success` URL which can be set either through a plain URL (`sensiolabs_gotenberg.webhook.{name}.success.url`) or by using a defined route in your application (`sensiolabs_gotenberg.webhook.{name}.success.route`). + +Here are some examples : + +```yaml +sensiolabs_gotenberg: + webhook: + default: + success: + url: 'https://webhook.site/#!/view/{some-uuid}' +``` +or +```yaml +sensiolabs_gotenberg: + webhook: + default: + success: + route: ['my_route', {'param1': 'value1'}] +``` + +Once a named configuration has been set, you can set it as a global default for all your builders : + +```yaml +sensiolabs_gotenberg: + default_options: + webhook: 'default' +``` + +or set it per builder : + +```yaml +sensiolabs_gotenberg: + webhook: + default: + success: + url: 'https://webhook.site/#!/view/{some-uuid}' + pdf_html: + success: + url: 'https://webhook.site/#!/view/{some-other-uuid}' + default_options: + pdf: + html: + webhook: + config_name: 'pdf_html' +``` + +finally you can do it like so : + +```yaml +sensiolabs_gotenberg: + default_options: + pdf: + html: + webhook: + success: + url: 'https://webhook.site/#!/view/{some-uuid}' +``` + +> [!WARNING] +> When using both `config_name` and a custom configuration on a builder, +> it will load the named configuration and merge it with the builder's configuration. +> +> See the following example : + +```yaml +sensiolabs_gotenberg: + webhook: + default: + success: + url: 'https://webhook.site/#!/view/{some-success-uuid}' + error: + url: 'https://webhook.site/#!/view/{some-error-uuid}' + default_options: + pdf: + html: + webhook: + config_name: 'default' + success: + url: 'https://webhook.site/#!/view/{some-other-uuid}' +``` + +is equivalent to : + +```yaml +sensiolabs_gotenberg: + default_options: + pdf: + html: + webhook: + success: + url: 'https://webhook.site/#!/view/{some-other-uuid}' + error: + url: 'https://webhook.site/#!/view/{some-error-uuid}' +``` + +### At runtime + +You can define webhook configuration at runtime. + +If you defined some named configuration like seen earlier, the simplest way is then to do the following: + +```diff +$builder = $this->gotenberg->pdf()->html() ++ ->webhookConfiguration('default') + ->header('header.html.twig') + ->content('html.html.twig', ['name' => 'Plop']) + ->fileName('html.pdf') +; +``` + +Or you can also define manually using : + +```diff +$builder = $this->gotenberg->pdf()->html() ++ ->webhookUrl($this->router->generate('my_route')) + ->header('header.html.twig') + ->content('html.html.twig', ['name' => 'Plop']) + ->fileName('html.pdf') +; +``` + +> [!WARNING] +> If combining both `->webhookConfiguration()` & `->webhookUrl()`, the order is important : +> +> If calling `->webhookConfiguration()` first then `->webhookUrl()` will override only the "success" part. +> +> If calling `->webhookUrl()` first then `->webhookConfiguration()` totally overrides previously set values. + + +> [!NOTE] +> If only success URL is set, error URL will fallback to the success one. diff --git a/docs/builders_api.md b/docs/builders_api.md index 55a2b193..5842431b 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/configuration.md b/docs/configuration.md index e34802c1..5fc78fda 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,210 +24,1177 @@ Then ```yaml # app/config/sensiolabs_gotenberg.yaml +# Default configuration for extension with alias: "sensiolabs_gotenberg" sensiolabs_gotenberg: - assets_directory: '%kernel.project_dir%/assets' - controller_listener: true # Will convert any GotenbergFileResult to stream automatically as a controller return. - http_client: 'gotenberg.client' # Required and must have a `base_uri`. + + # Base directory will be used for assets, files, markdown + assets_directory: '%kernel.project_dir%/assets' + + # HTTP Client reference to use. (Must have a base_uri) + http_client: ~ # Required + # Override the request Gotenberg will make to call one of your routes. request_context: + # Used only when using `->route()`. Overrides the guessed `base_url` from the request. May be useful in CLI. - base_uri: null # None + base_uri: ~ + + # Enables the listener on kernel.view to stream GotenbergFileResult object. + controller_listener: true + webhook: + + # Prototype + name: + name: ~ + success: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + error: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + + # HTTP headers to send back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook + extra_http_headers: + + # Prototype + name: + name: ~ + value: ~ default_options: + + # Webhook configuration name. + webhook: ~ pdf: html: + + # Add default header to the builder. header: - template: null # None - context: null # None + + # Default header twig template to apply. + template: null + + # Default context for header twig template. + context: [] + + # Add default footer to the builder. footer: - template: null # None - context: null # None - single_page: null # false - paper_standard_size: null # None - paper_width: null # 8.5 - paper_height: null # 11 - margin_top: null # 0.39 - margin_bottom: null # 0.39 - margin_left: null # 0.39 - margin_right: null # 0.39 - prefer_css_page_size: null # false - print_background: null # false - omit_background: null # false - landscape: null # false - scale: null # 1.0 - native_page_ranges: null # All pages - wait_delay: null # None - wait_for_expression: null # None - emulated_media_type: null # 'print' - cookies: null # None - user_agent: null # None - extra_http_headers: null # None - fail_on_http_status_codes: null # [499-599] - fail_on_console_exceptions: null # false - skip_network_idle_event: null # false - pdf_format: null # None - pdf_universal_access: null # false - metadata: null # None - download_from: null # None + + # Default footer twig template to apply. + template: null + + # Default context for footer twig template. + context: [] + + # Define whether to print the entire content in one single page. - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + single_page: null + + # The standard paper size to use, either "letter", "legal", "tabloid", "ledger", "A0", "A1", "A2", "A3", "A4", "A5", "A6" - default None. + paper_standard_size: null # One of "letter"; "legal"; "tabloid"; "ledger"; "A0"; "A1"; "A2"; "A3"; "A4"; "A5"; "A6" + + # Paper width, in inches - default 8.5. https://gotenberg.dev/docs/routes#page-properties-chromium + paper_width: null + + # Paper height, in inches - default 11. https://gotenberg.dev/docs/routes#page-properties-chromium + paper_height: null + + # Top margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_top: null + + # Bottom margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_bottom: null + + # Left margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_left: null + + # Right margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_right: null + + # Define whether to prefer page size as defined by CSS - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + prefer_css_page_size: null + + # Print the background graphics - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + print_background: null + + # Hide the default white background and allow generating PDFs with transparency - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + omit_background: null + + # The paper orientation to landscape - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + landscape: null + + # The scale of the page rendering (e.g., 1.0) - default 1.0. https://gotenberg.dev/docs/routes#page-properties-chromium + scale: null + + # Page ranges to print, e.g., "1-5, 8, 11-13" - default All pages. https://gotenberg.dev/docs/routes#page-properties-chromium + native_page_ranges: null + + # Duration (e.g, "5s") to wait when loading an HTML document before converting it into PDF - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_delay: null + + # The JavaScript expression to wait before converting an HTML document into PDF until it returns true - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_for_expression: null + + # The media type to emulate, either "screen" or "print" - default "print". https://gotenberg.dev/docs/routes#emulated-media-type + emulated_media_type: null # One of "print"; "screen" + + # Cookies to store in the Chromium cookie jar - default None. https://gotenberg.dev/docs/routes#cookies-chromium + cookies: + + # Prototype + - + name: ~ + value: ~ + domain: ~ + path: null + secure: null + httpOnly: null + + # Accepted values are "Strict", "Lax" or "None". https://gotenberg.dev/docs/routes#cookies-chromium + sameSite: null # One of "Strict"; "Lax"; "None" + + # Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium + user_agent: null + + # HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers + extra_http_headers: + + # Prototype + name: + name: ~ + value: ~ + + # Return a 409 Conflict response if the HTTP status code from the main page is not acceptable. - default [499,599]. https://gotenberg.dev/docs/routes#invalid-http-status-codes-chromium + fail_on_http_status_codes: + + # Defaults: + - 499 + - 599 + + # Return a 409 Conflict response if there are exceptions in the Chromium console - default false. https://gotenberg.dev/docs/routes#console-exceptions + fail_on_console_exceptions: null + + # Do not wait for Chromium network to be idle. - default false. https://gotenberg.dev/docs/routes#performance-mode-chromium + skip_network_idle_event: null + + # The metadata to write. Not all metadata are writable. Consider taking a look at https://exiftool.org/TagNames/XMP.html#pdf for an (exhaustive?) list of available metadata. + metadata: + Author: ~ + Copyright: ~ + CreationDate: ~ + Creator: ~ + Keywords: ~ + Marked: ~ + ModDate: ~ + PDFVersion: ~ + Producer: ~ + Subject: ~ + Title: ~ + Trapped: ~ # One of "True"; "False"; "Unknown" + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ + + # Convert PDF into the given PDF/A format - default None. + pdf_format: null # One of "PDF\/A-1b"; "PDF\/A-2b"; "PDF\/A-3b" + + # Enable PDF for Universal Access for optimal accessibility - default false. + pdf_universal_access: null + + # Webhook configuration name or definition. + webhook: + + # The name of the webhook configuration to use. + config_name: ~ + success: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + error: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + + # HTTP headers to send back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook + extra_http_headers: + + # Example: + # - { name: X-Custom-Header, value: custom-header-value } + + # Prototype + name: + name: ~ + value: ~ url: + + # Add default header to the builder. header: - template: null # None - context: null # None + + # Default header twig template to apply. + template: null + + # Default context for header twig template. + context: [] + + # Add default footer to the builder. footer: - template: null # None - context: null # None - single_page: null # false - paper_standard_size: null # None - paper_width: null # 8.5 - paper_height: null # 11 - margin_top: null # 0.39 - margin_bottom: null # 0.39 - margin_left: null # 0.39 - margin_right: null # 0.39 - prefer_css_page_size: null # false - print_background: null # false - omit_background: null # false - landscape: null # false - scale: null # 1.0 - native_page_ranges: null # All pages - wait_delay: null # None - wait_for_expression: null # None - emulated_media_type: null # 'print' - cookies: null # None - user_agent: null # None - extra_http_headers: null # None - fail_on_http_status_codes: null # [499-599] - fail_on_console_exceptions: null # false - skip_network_idle_event: null # false - pdf_format: null # None - pdf_universal_access: null # false - metadata: null # None - download_from: null # None + + # Default footer twig template to apply. + template: null + + # Default context for footer twig template. + context: [] + + # Define whether to print the entire content in one single page. - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + single_page: null + + # The standard paper size to use, either "letter", "legal", "tabloid", "ledger", "A0", "A1", "A2", "A3", "A4", "A5", "A6" - default None. + paper_standard_size: null # One of "letter"; "legal"; "tabloid"; "ledger"; "A0"; "A1"; "A2"; "A3"; "A4"; "A5"; "A6" + + # Paper width, in inches - default 8.5. https://gotenberg.dev/docs/routes#page-properties-chromium + paper_width: null + + # Paper height, in inches - default 11. https://gotenberg.dev/docs/routes#page-properties-chromium + paper_height: null + + # Top margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_top: null + + # Bottom margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_bottom: null + + # Left margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_left: null + + # Right margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_right: null + + # Define whether to prefer page size as defined by CSS - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + prefer_css_page_size: null + + # Print the background graphics - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + print_background: null + + # Hide the default white background and allow generating PDFs with transparency - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + omit_background: null + + # The paper orientation to landscape - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + landscape: null + + # The scale of the page rendering (e.g., 1.0) - default 1.0. https://gotenberg.dev/docs/routes#page-properties-chromium + scale: null + + # Page ranges to print, e.g., "1-5, 8, 11-13" - default All pages. https://gotenberg.dev/docs/routes#page-properties-chromium + native_page_ranges: null + + # Duration (e.g, "5s") to wait when loading an HTML document before converting it into PDF - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_delay: null + + # The JavaScript expression to wait before converting an HTML document into PDF until it returns true - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_for_expression: null + + # The media type to emulate, either "screen" or "print" - default "print". https://gotenberg.dev/docs/routes#emulated-media-type + emulated_media_type: null # One of "print"; "screen" + + # Cookies to store in the Chromium cookie jar - default None. https://gotenberg.dev/docs/routes#cookies-chromium + cookies: + + # Prototype + - + name: ~ + value: ~ + domain: ~ + path: null + secure: null + httpOnly: null + + # Accepted values are "Strict", "Lax" or "None". https://gotenberg.dev/docs/routes#cookies-chromium + sameSite: null # One of "Strict"; "Lax"; "None" + + # Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium + user_agent: null + + # HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers + extra_http_headers: + + # Prototype + name: + name: ~ + value: ~ + + # Return a 409 Conflict response if the HTTP status code from the main page is not acceptable. - default [499,599]. https://gotenberg.dev/docs/routes#invalid-http-status-codes-chromium + fail_on_http_status_codes: + + # Defaults: + - 499 + - 599 + + # Return a 409 Conflict response if there are exceptions in the Chromium console - default false. https://gotenberg.dev/docs/routes#console-exceptions + fail_on_console_exceptions: null + + # Do not wait for Chromium network to be idle. - default false. https://gotenberg.dev/docs/routes#performance-mode-chromium + skip_network_idle_event: null + + # The metadata to write. Not all metadata are writable. Consider taking a look at https://exiftool.org/TagNames/XMP.html#pdf for an (exhaustive?) list of available metadata. + metadata: + Author: ~ + Copyright: ~ + CreationDate: ~ + Creator: ~ + Keywords: ~ + Marked: ~ + ModDate: ~ + PDFVersion: ~ + Producer: ~ + Subject: ~ + Title: ~ + Trapped: ~ # One of "True"; "False"; "Unknown" + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ + + # Convert PDF into the given PDF/A format - default None. + pdf_format: null # One of "PDF\/A-1b"; "PDF\/A-2b"; "PDF\/A-3b" + + # Enable PDF for Universal Access for optimal accessibility - default false. + pdf_universal_access: null + + # Webhook configuration name or definition. + webhook: + + # The name of the webhook configuration to use. + config_name: ~ + success: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + error: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + + # HTTP headers to send back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook + extra_http_headers: + + # Example: + # - { name: X-Custom-Header, value: custom-header-value } + + # Prototype + name: + name: ~ + value: ~ markdown: + + # Add default header to the builder. header: - template: null # None - context: null # None + + # Default header twig template to apply. + template: null + + # Default context for header twig template. + context: [] + + # Add default footer to the builder. footer: - template: null # None - context: null # None - single_page: null # false - paper_standard_size: null # None - paper_width: null # 8.5 - paper_height: null # 11 - margin_top: null # 0.39 - margin_bottom: null # 0.39 - margin_left: null # 0.39 - margin_right: null # 0.39 - prefer_css_page_size: null # false - print_background: null # false - omit_background: null # false - landscape: null # false - scale: null # 1.0 - native_page_ranges: null # All pages - wait_delay: null # None - wait_for_expression: null # None - emulated_media_type: null # 'print' - cookies: null # None - user_agent: null # None - extra_http_headers: null # None - fail_on_http_status_codes: null # [499-599] - fail_on_console_exceptions: null # false - skip_network_idle_event: null # false - pdf_format: null # None - pdf_universal_access: null # false - metadata: null # None - download_from: null # None + + # Default footer twig template to apply. + template: null + + # Default context for footer twig template. + context: [] + + # Define whether to print the entire content in one single page. - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + single_page: null + + # The standard paper size to use, either "letter", "legal", "tabloid", "ledger", "A0", "A1", "A2", "A3", "A4", "A5", "A6" - default None. + paper_standard_size: null # One of "letter"; "legal"; "tabloid"; "ledger"; "A0"; "A1"; "A2"; "A3"; "A4"; "A5"; "A6" + + # Paper width, in inches - default 8.5. https://gotenberg.dev/docs/routes#page-properties-chromium + paper_width: null + + # Paper height, in inches - default 11. https://gotenberg.dev/docs/routes#page-properties-chromium + paper_height: null + + # Top margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_top: null + + # Bottom margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_bottom: null + + # Left margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_left: null + + # Right margin, in inches - default 0.39. https://gotenberg.dev/docs/routes#page-properties-chromium + margin_right: null + + # Define whether to prefer page size as defined by CSS - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + prefer_css_page_size: null + + # Print the background graphics - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + print_background: null + + # Hide the default white background and allow generating PDFs with transparency - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + omit_background: null + + # The paper orientation to landscape - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + landscape: null + + # The scale of the page rendering (e.g., 1.0) - default 1.0. https://gotenberg.dev/docs/routes#page-properties-chromium + scale: null + + # Page ranges to print, e.g., "1-5, 8, 11-13" - default All pages. https://gotenberg.dev/docs/routes#page-properties-chromium + native_page_ranges: null + + # Duration (e.g, "5s") to wait when loading an HTML document before converting it into PDF - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_delay: null + + # The JavaScript expression to wait before converting an HTML document into PDF until it returns true - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_for_expression: null + + # The media type to emulate, either "screen" or "print" - default "print". https://gotenberg.dev/docs/routes#emulated-media-type + emulated_media_type: null # One of "print"; "screen" + + # Cookies to store in the Chromium cookie jar - default None. https://gotenberg.dev/docs/routes#cookies-chromium + cookies: + + # Prototype + - + name: ~ + value: ~ + domain: ~ + path: null + secure: null + httpOnly: null + + # Accepted values are "Strict", "Lax" or "None". https://gotenberg.dev/docs/routes#cookies-chromium + sameSite: null # One of "Strict"; "Lax"; "None" + + # Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium + user_agent: null + + # HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers + extra_http_headers: + + # Prototype + name: + name: ~ + value: ~ + + # Return a 409 Conflict response if the HTTP status code from the main page is not acceptable. - default [499,599]. https://gotenberg.dev/docs/routes#invalid-http-status-codes-chromium + fail_on_http_status_codes: + + # Defaults: + - 499 + - 599 + + # Return a 409 Conflict response if there are exceptions in the Chromium console - default false. https://gotenberg.dev/docs/routes#console-exceptions + fail_on_console_exceptions: null + + # Do not wait for Chromium network to be idle. - default false. https://gotenberg.dev/docs/routes#performance-mode-chromium + skip_network_idle_event: null + + # The metadata to write. Not all metadata are writable. Consider taking a look at https://exiftool.org/TagNames/XMP.html#pdf for an (exhaustive?) list of available metadata. + metadata: + Author: ~ + Copyright: ~ + CreationDate: ~ + Creator: ~ + Keywords: ~ + Marked: ~ + ModDate: ~ + PDFVersion: ~ + Producer: ~ + Subject: ~ + Title: ~ + Trapped: ~ # One of "True"; "False"; "Unknown" + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ + + # Convert PDF into the given PDF/A format - default None. + pdf_format: null # One of "PDF\/A-1b"; "PDF\/A-2b"; "PDF\/A-3b" + + # Enable PDF for Universal Access for optimal accessibility - default false. + pdf_universal_access: null + + # Webhook configuration name or definition. + webhook: + + # The name of the webhook configuration to use. + config_name: ~ + success: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + error: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + + # HTTP headers to send back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook + extra_http_headers: + + # Example: + # - { name: X-Custom-Header, value: custom-header-value } + + # Prototype + name: + name: ~ + value: ~ office: - landscape: null # false - native_page_ranges: null # All pages - do_not_export_form_fields: null # true - single_page_sheets: null # false - merge: null # false - pdf_format: null # None - pdf_universal_access: null # false - metadata: null # None - allow_duplicate_field_names: null # false - do_not_export_bookmarks: null # true - export_bookmarks_to_pdf_destination: null # false - export_placeholders: null # false - export_notes: null # false - export_notes_pages: null # false - export_only_notes_pages: null # false - export_notes_in_margin: null # false - convert_ooo_target_to_pdf_target: null # false - export_links_relative_fsys: null # false - export_hidden_slides: null # false - skip_empty_pages: null # false - add_original_document_as_stream: null # false - lossless_image_compression: null # false - quality: null # 90 - reduce_image_resolution: null # false - max_image_resolution: null # 300 - password: null # None - download_from: null # None + + # Set the password for opening the source file. https://gotenberg.dev/docs/routes#page-properties-libreoffice + password: null + + # The paper orientation to landscape - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + landscape: null + + # Page ranges to print, e.g., "1-5, 8, 11-13" - default All pages. https://gotenberg.dev/docs/routes#page-properties-chromium + native_page_ranges: null + + # Set whether to export the form fields or to use the inputted/selected content of the fields. - default true. https://gotenberg.dev/docs/routes#page-properties-libreoffice + do_not_export_form_fields: null + + # Set whether to render the entire spreadsheet as a single page. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + single_page_sheets: null + + # Merge alphanumerically the resulting PDFs. - default false. https://gotenberg.dev/docs/routes#merge-libreoffice + merge: null + + # The metadata to write. Not all metadata are writable. Consider taking a look at https://exiftool.org/TagNames/XMP.html#pdf for an (exhaustive?) list of available metadata. + metadata: + Author: ~ + Copyright: ~ + CreationDate: ~ + Creator: ~ + Keywords: ~ + Marked: ~ + ModDate: ~ + PDFVersion: ~ + Producer: ~ + Subject: ~ + Title: ~ + Trapped: ~ # One of "True"; "False"; "Unknown" + + # Specify whether multiple form fields exported are allowed to have the same field name. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + allow_duplicate_field_names: null + + # Specify if bookmarks are exported to PDF. - default true. https://gotenberg.dev/docs/routes#page-properties-libreoffice + do_not_export_bookmarks: null + + # Specify that the bookmarks contained in the source LibreOffice file should be exported to the PDF file as Named Destination. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + export_bookmarks_to_pdf_destination: null + + # Export the placeholders fields visual markings only. The exported placeholder is ineffective. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + export_placeholders: null + + # Specify if notes are exported to PDF. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + export_notes: null + + # Specify if notes pages are exported to PDF. Notes pages are available in Impress documents only. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + export_notes_pages: null + + # Specify, if the form field exportNotesPages is set to true, if only notes pages are exported to PDF. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + export_only_notes_pages: null + + # Specify if notes in margin are exported to PDF. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + export_notes_in_margin: null + + # Specify that the target documents with .od[tpgs] extension, will have that extension changed to .pdf when the link is exported to PDF. The source document remains untouched. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + convert_ooo_target_to_pdf_target: null + + # Specify that the file system related hyperlinks (file:// protocol) present in the document will be exported as relative to the source document location. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + export_links_relative_fsys: null + + # Export, for LibreOffice Impress, slides that are not included in slide shows. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + export_hidden_slides: null + + # Specify that automatically inserted empty pages are suppressed. This option is active only if storing Writer documents. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + skip_empty_pages: null + + # Specify that a stream is inserted to the PDF file which contains the original document for archiving purposes. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice + add_original_document_as_stream: null + + # Specify if images are exported to PDF using a lossless compression format like PNG or compressed using the JPEG format. - default false. https://gotenberg.dev/docs/routes#images-libreoffice + lossless_image_compression: null + + # Specify the quality of the JPG export. A higher value produces a higher-quality image and a larger file. Between 1 and 100. - default 90. https://gotenberg.dev/docs/routes#images-libreoffice + quality: null + + # Specify if the resolution of each image is reduced to the resolution specified by the form field maxImageResolution. - default false. https://gotenberg.dev/docs/routes#images-libreoffice + reduce_image_resolution: null + + # If the form field reduceImageResolution is set to true, tell if all images will be reduced to the given value in DPI. Possible values are: 75, 150, 300, 600 and 1200. - default 300. https://gotenberg.dev/docs/routes#images-libreoffice + max_image_resolution: null # One of 75; 150; 300; 600; 1200 + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ + + # Convert PDF into the given PDF/A format - default None. + pdf_format: null # One of "PDF\/A-1b"; "PDF\/A-2b"; "PDF\/A-3b" + + # Enable PDF for Universal Access for optimal accessibility - default false. + pdf_universal_access: null merge: - pdf_format: null # None - pdf_universal_access: null # false - metadata: null # None - download_from: null # None + + # Convert PDF into the given PDF/A format - default None. + pdf_format: null # One of "PDF\/A-1b"; "PDF\/A-2b"; "PDF\/A-3b" + + # Enable PDF for Universal Access for optimal accessibility - default false. + pdf_universal_access: null + + # The metadata to write. Not all metadata are writable. Consider taking a look at https://exiftool.org/TagNames/XMP.html#pdf for an (exhaustive?) list of available metadata. + metadata: + Author: ~ + Copyright: ~ + CreationDate: ~ + Creator: ~ + Keywords: ~ + Marked: ~ + ModDate: ~ + PDFVersion: ~ + Producer: ~ + Subject: ~ + Title: ~ + Trapped: ~ # One of "True"; "False"; "Unknown" + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ convert: - pdf_format: null # None - pdf_universal_access: null # false - download_from: null # None + + # Convert PDF into the given PDF/A format - default None. + pdf_format: null # One of "PDF\/A-1b"; "PDF\/A-2b"; "PDF\/A-3b" + + # Enable PDF for Universal Access for optimal accessibility - default false. + pdf_universal_access: null + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ screenshot: html: - width: null # 800 - height: null # 600 - clip: null # false - format: null # png - quality: null # 100 - omit_background: null # false - optimize_for_speed: null # false - wait_delay: null # None - wait_for_expression: null # None - emulated_media_type: null # 'print' - cookies: null # None - user_agent: null # None - extra_http_headers: null # None - fail_on_http_status_codes: null # [499-599] - fail_on_console_exceptions: null # false - skip_network_idle_event: null # false - download_from: null # None + + # The device screen width in pixels. - default 800. https://gotenberg.dev/docs/routes#screenshots-route + width: null + + # The device screen height in pixels. - default 600. https://gotenberg.dev/docs/routes#screenshots-route + height: null + + # Define whether to clip the screenshot according to the device dimensions - default false. https://gotenberg.dev/docs/routes#screenshots-route + clip: null + + # The image compression format, either "png", "jpeg" or "webp" - default png. https://gotenberg.dev/docs/routes#screenshots-route + format: null # One of "png"; "jpeg"; "webp" + + # The compression quality from range 0 to 100 (jpeg only) - default 100. https://gotenberg.dev/docs/routes#screenshots-route + quality: null + + # Hide the default white background and allow generating PDFs with transparency - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + omit_background: null + + # Define whether to optimize image encoding for speed, not for resulting size. - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + optimize_for_speed: null + + # Duration (e.g, "5s") to wait when loading an HTML document before converting it into PDF - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_delay: null + + # The JavaScript expression to wait before converting an HTML document into PDF until it returns true - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_for_expression: null + + # The media type to emulate, either "screen" or "print" - default "print". https://gotenberg.dev/docs/routes#emulated-media-type + emulated_media_type: null # One of "print"; "screen" + + # Cookies to store in the Chromium cookie jar - default None. https://gotenberg.dev/docs/routes#cookies-chromium + cookies: + + # Prototype + - + name: ~ + value: ~ + domain: ~ + path: null + secure: null + httpOnly: null + + # Accepted values are "Strict", "Lax" or "None". https://gotenberg.dev/docs/routes#cookies-chromium + sameSite: null # One of "Strict"; "Lax"; "None" + + # Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium + user_agent: null + + # HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers + extra_http_headers: + + # Prototype + name: + name: ~ + value: ~ + + # Return a 409 Conflict response if the HTTP status code from the main page is not acceptable. - default [499,599]. https://gotenberg.dev/docs/routes#invalid-http-status-codes-chromium + fail_on_http_status_codes: + + # Defaults: + - 499 + - 599 + + # Return a 409 Conflict response if there are exceptions in the Chromium console - default false. https://gotenberg.dev/docs/routes#console-exceptions + fail_on_console_exceptions: null + + # Do not wait for Chromium network to be idle. - default false. https://gotenberg.dev/docs/routes#performance-mode-chromium + skip_network_idle_event: null + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ + + # Webhook configuration name or definition. + webhook: + + # The name of the webhook configuration to use. + config_name: ~ + success: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + error: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + + # HTTP headers to send back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook + extra_http_headers: + + # Example: + # - { name: X-Custom-Header, value: custom-header-value } + + # Prototype + name: + name: ~ + value: ~ url: - width: null # 800 - height: null # 600 - clip: null # false - format: null # png - quality: null # 100 - omit_background: null # false - optimize_for_speed: null # false - wait_delay: null # None - wait_for_expression: null # None - emulated_media_type: null # 'print' - cookies: null # None - user_agent: null # None - extra_http_headers: null # None - fail_on_http_status_codes: null # [499-599] - fail_on_console_exceptions: null # false - skip_network_idle_event: null # false - download_from: null # None + + # The device screen width in pixels. - default 800. https://gotenberg.dev/docs/routes#screenshots-route + width: null + + # The device screen height in pixels. - default 600. https://gotenberg.dev/docs/routes#screenshots-route + height: null + + # Define whether to clip the screenshot according to the device dimensions - default false. https://gotenberg.dev/docs/routes#screenshots-route + clip: null + + # The image compression format, either "png", "jpeg" or "webp" - default png. https://gotenberg.dev/docs/routes#screenshots-route + format: null # One of "png"; "jpeg"; "webp" + + # The compression quality from range 0 to 100 (jpeg only) - default 100. https://gotenberg.dev/docs/routes#screenshots-route + quality: null + + # Hide the default white background and allow generating PDFs with transparency - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + omit_background: null + + # Define whether to optimize image encoding for speed, not for resulting size. - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + optimize_for_speed: null + + # Duration (e.g, "5s") to wait when loading an HTML document before converting it into PDF - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_delay: null + + # The JavaScript expression to wait before converting an HTML document into PDF until it returns true - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_for_expression: null + + # The media type to emulate, either "screen" or "print" - default "print". https://gotenberg.dev/docs/routes#emulated-media-type + emulated_media_type: null # One of "print"; "screen" + + # Cookies to store in the Chromium cookie jar - default None. https://gotenberg.dev/docs/routes#cookies-chromium + cookies: + + # Prototype + - + name: ~ + value: ~ + domain: ~ + path: null + secure: null + httpOnly: null + + # Accepted values are "Strict", "Lax" or "None". https://gotenberg.dev/docs/routes#cookies-chromium + sameSite: null # One of "Strict"; "Lax"; "None" + + # Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium + user_agent: null + + # HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers + extra_http_headers: + + # Prototype + name: + name: ~ + value: ~ + + # Return a 409 Conflict response if the HTTP status code from the main page is not acceptable. - default [499,599]. https://gotenberg.dev/docs/routes#invalid-http-status-codes-chromium + fail_on_http_status_codes: + + # Defaults: + - 499 + - 599 + + # Return a 409 Conflict response if there are exceptions in the Chromium console - default false. https://gotenberg.dev/docs/routes#console-exceptions + fail_on_console_exceptions: null + + # Do not wait for Chromium network to be idle. - default false. https://gotenberg.dev/docs/routes#performance-mode-chromium + skip_network_idle_event: null + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ + + # Webhook configuration name or definition. + webhook: + + # The name of the webhook configuration to use. + config_name: ~ + success: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + error: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + + # HTTP headers to send back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook + extra_http_headers: + + # Example: + # - { name: X-Custom-Header, value: custom-header-value } + + # Prototype + name: + name: ~ + value: ~ markdown: - width: null # 800 - height: null # 600 - clip: null # false - format: null # png - quality: null # 100 - omit_background: null # false - optimize_for_speed: null # false - wait_delay: null # None - wait_for_expression: null # None - emulated_media_type: null # 'print' - cookies: null # None - user_agent: null # None - extra_http_headers: null # None - fail_on_http_status_codes: null # [499-599] - fail_on_console_exceptions: null # false - skip_network_idle_event: null # false - download_from: null # None + + # The device screen width in pixels. - default 800. https://gotenberg.dev/docs/routes#screenshots-route + width: null + + # The device screen height in pixels. - default 600. https://gotenberg.dev/docs/routes#screenshots-route + height: null + + # Define whether to clip the screenshot according to the device dimensions - default false. https://gotenberg.dev/docs/routes#screenshots-route + clip: null + + # The image compression format, either "png", "jpeg" or "webp" - default png. https://gotenberg.dev/docs/routes#screenshots-route + format: null # One of "png"; "jpeg"; "webp" + + # The compression quality from range 0 to 100 (jpeg only) - default 100. https://gotenberg.dev/docs/routes#screenshots-route + quality: null + + # Hide the default white background and allow generating PDFs with transparency - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + omit_background: null + + # Define whether to optimize image encoding for speed, not for resulting size. - default false. https://gotenberg.dev/docs/routes#page-properties-chromium + optimize_for_speed: null + + # Duration (e.g, "5s") to wait when loading an HTML document before converting it into PDF - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_delay: null + + # The JavaScript expression to wait before converting an HTML document into PDF until it returns true - default None. https://gotenberg.dev/docs/routes#wait-before-rendering + wait_for_expression: null + + # The media type to emulate, either "screen" or "print" - default "print". https://gotenberg.dev/docs/routes#emulated-media-type + emulated_media_type: null # One of "print"; "screen" + + # Cookies to store in the Chromium cookie jar - default None. https://gotenberg.dev/docs/routes#cookies-chromium + cookies: + + # Prototype + - + name: ~ + value: ~ + domain: ~ + path: null + secure: null + httpOnly: null + + # Accepted values are "Strict", "Lax" or "None". https://gotenberg.dev/docs/routes#cookies-chromium + sameSite: null # One of "Strict"; "Lax"; "None" + + # Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium + user_agent: null + + # HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers + extra_http_headers: + + # Prototype + name: + name: ~ + value: ~ + + # Return a 409 Conflict response if the HTTP status code from the main page is not acceptable. - default [499,599]. https://gotenberg.dev/docs/routes#invalid-http-status-codes-chromium + fail_on_http_status_codes: + + # Defaults: + - 499 + - 599 + + # Return a 409 Conflict response if there are exceptions in the Chromium console - default false. https://gotenberg.dev/docs/routes#console-exceptions + fail_on_console_exceptions: null + + # Do not wait for Chromium network to be idle. - default false. https://gotenberg.dev/docs/routes#performance-mode-chromium + skip_network_idle_event: null + + # URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from + download_from: + + # Prototype + - + url: ~ + extraHttpHeaders: + + # Prototype + name: + name: ~ + value: ~ + + # Webhook configuration name or definition. + webhook: + + # The name of the webhook configuration to use. + config_name: ~ + success: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + error: + + # The URL to call. + url: ~ + + # Route configuration. + route: ~ + + # Examples: + # - 'https://webhook.site/#!/view/{some-token}' + # - [my_route, { param1: value1, param2: value2 }] + + # HTTP method to use on that endpoint. + method: null # One of "POST"; "PUT"; "PATCH" + + # HTTP headers to send back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook + extra_http_headers: + + # Example: + # - { name: X-Custom-Header, value: custom-header-value } + + # Prototype + name: + name: ~ + value: ~ ``` > [!TIP] @@ -247,8 +1214,18 @@ sensiolabs_gotenberg: - { name: 'My-Header', value: 'MyValue' } ``` +Or Headers to send to your webhook endpoint + +```yaml +sensiolabs_gotenberg: + webhook: + default: + extra_http_headers: + - { name: 'My-Header', value: 'MyValue' } +``` + > [!TIP] -> For more information about [custom HTTP headers](https://gotenberg.dev/docs/routes#custom-http-headers). +> For more information about [custom HTTP headers](https://gotenberg.dev/docs/routes#custom-http-headers) & [webhook custom HTTP headers](https://gotenberg.dev/docs/configuration#webhook). ## Invalid HTTP Status Codes @@ -334,4 +1311,3 @@ sensiolabs_gotenberg: > [!TIP] > For more information go to [Gotenberg documentations](https://gotenberg.dev/docs/routes#download-from). - diff --git a/docs/generate.php b/docs/generate.php index 7e11acfd..0b826131 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 00000000..c507ac59 --- /dev/null +++ b/docs/pdf/builders_api/ConvertPdfBuilder.md @@ -0,0 +1,26 @@ +# 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. + +* `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 28100c2e..b9f22407 100644 --- a/docs/pdf/builders_api/HtmlPdfBuilder.md +++ b/docs/pdf/builders_api/HtmlPdfBuilder.md @@ -129,9 +129,19 @@ 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. + +* `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 18390398..d4570045 100644 --- a/docs/pdf/builders_api/LibreOfficePdfBuilder.md +++ b/docs/pdf/builders_api/LibreOfficePdfBuilder.md @@ -87,7 +87,17 @@ 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. + +* `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 8ac0c84f..ce31f9c8 100644 --- a/docs/pdf/builders_api/MarkdownPdfBuilder.md +++ b/docs/pdf/builders_api/MarkdownPdfBuilder.md @@ -132,9 +132,19 @@ 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. + +* `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 00000000..477c8815 --- /dev/null +++ b/docs/pdf/builders_api/MergePdfBuilder.md @@ -0,0 +1,34 @@ +# 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. + +* `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 6aedc168..22219eea 100644 --- a/docs/pdf/builders_api/UrlPdfBuilder.md +++ b/docs/pdf/builders_api/UrlPdfBuilder.md @@ -131,9 +131,19 @@ 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. + +* `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 d70c992c..8a87f455 100644 --- a/docs/screenshot/builders_api/HtmlScreenshotBuilder.md +++ b/docs/screenshot/builders_api/HtmlScreenshotBuilder.md @@ -74,9 +74,19 @@ 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. + +* `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 3627b5a1..3f7df2e1 100644 --- a/docs/screenshot/builders_api/MarkdownScreenshotBuilder.md +++ b/docs/screenshot/builders_api/MarkdownScreenshotBuilder.md @@ -77,9 +77,19 @@ 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. + +* `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 35e20de4..09185dcd 100644 --- a/docs/screenshot/builders_api/UrlScreenshotBuilder.md +++ b/docs/screenshot/builders_api/UrlScreenshotBuilder.md @@ -76,9 +76,19 @@ 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. + +* `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/webhook.md b/docs/webhook.md new file mode 100644 index 00000000..75a349ac --- /dev/null +++ b/docs/webhook.md @@ -0,0 +1,3 @@ +# Going async + +* [Native](./async/native.md) diff --git a/phpstan.neon b/phpstan.dist.neon similarity index 78% rename from phpstan.neon rename to phpstan.dist.neon index 2c0de7e2..d8c860ef 100644 --- a/phpstan.neon +++ b/phpstan.dist.neon @@ -53,3 +53,23 @@ parameters: message: "#^Parameter \\#2 \\$cookie of method Sensiolabs\\\\GotenbergBundle\\\\Builder\\\\Screenshot\\\\AbstractChromiumScreenshotBuilder\\:\\:setCookie\\(\\) expects array\\{name\\: string, value\\: string, domain\\: string, path\\?\\: string\\|null, secure\\?\\: bool\\|null, httpOnly\\?\\: bool\\|null, sameSite\\?\\: 'Lax'\\|'Strict'\\|null\\}\\|Symfony\\\\Component\\\\HttpFoundation\\\\Cookie, array\\{name\\: string, value\\: string\\|null, domain\\: string\\} given\\.$#" count: 1 path: src/Builder/Screenshot/AbstractChromiumScreenshotBuilder.php + + - + message: "#^Call to an undefined method Sensiolabs\\\\GotenbergBundle\\\\Builder\\\\AsyncBuilderInterface\\:\\:errorWebhookUrl\\(\\)\\.$#" + count: 2 + path: tests/Builder/AsyncBuilderTraitTest.php + + - + message: "#^Call to an undefined method Sensiolabs\\\\GotenbergBundle\\\\Builder\\\\AsyncBuilderInterface\\:\\:webhookConfiguration\\(\\)\\.$#" + count: 1 + path: tests/Builder/AsyncBuilderTraitTest.php + + - + message: "#^Call to an undefined method Sensiolabs\\\\GotenbergBundle\\\\Builder\\\\AsyncBuilderInterface\\:\\:webhookExtraHeaders\\(\\)\\.$#" + count: 1 + path: tests/Builder/AsyncBuilderTraitTest.php + + - + message: "#^Call to an undefined method Sensiolabs\\\\GotenbergBundle\\\\Builder\\\\AsyncBuilderInterface\\:\\:webhookUrl\\(\\)\\.$#" + count: 4 + path: tests/Builder/AsyncBuilderTraitTest.php diff --git a/phpunit.xml b/phpunit.xml index 7fb1daf5..db970995 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 00000000..3daedb50 --- /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('->webhookUrl() 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 + ->webhookUrl( + $webhookConfiguration['success']['url'], + $webhookConfiguration['success']['method'], + ) + ->errorWebhookUrl( + $webhookConfiguration['error']['url'], + $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; + } + + /** + * 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 90c454b6..4cb8968b 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 658cab32..7215dfe8 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 6ff9b8b5..25dc40a9 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 707b6a09..e72cc8e3 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 b41ba859..82d592c4 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 ecfc50f5..0d68221a 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 274e268b..3442affa 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 96d3ad5b..31312279 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/DataCollector/GotenbergDataCollector.php b/src/DataCollector/GotenbergDataCollector.php index 99e44301..a047b679 100644 --- a/src/DataCollector/GotenbergDataCollector.php +++ b/src/DataCollector/GotenbergDataCollector.php @@ -87,6 +87,7 @@ private function lateCollectFiles(array $builders, string $type): void 'default_options' => $this->cloneVar($this->defaultOptions[$id] ?? []), ], 'type' => $type, + 'request_type' => $request['type'], 'time' => $request['time'], 'memory' => $request['memory'], 'size' => $this->formatSize($request['size'] ?? 0), diff --git a/src/Debug/Builder/TraceablePdfBuilder.php b/src/Debug/Builder/TraceablePdfBuilder.php index ccb5e785..318c45bf 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; @@ -9,7 +10,7 @@ final class TraceablePdfBuilder implements PdfBuilderInterface { /** - * @var list|null, 'fileName': string|null, 'calls': list, 'arguments': array}>}> + * @var list|null, 'fileName': string|null, 'calls': list, 'arguments': array}>}> */ private array $pdfs = []; @@ -38,6 +39,7 @@ public function generate(): GotenbergFileResult $swEvent?->stop(); $this->pdfs[] = [ + 'type' => 'sync', 'calls' => $this->calls, 'time' => $swEvent?->getDuration(), 'memory' => $swEvent?->getMemory(), @@ -50,6 +52,31 @@ 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[] = [ + 'type' => 'async', + 'calls' => $this->calls, + 'time' => $swEvent?->getDuration(), + 'memory' => $swEvent?->getMemory(), + 'size' => null, + 'fileName' => null, + ]; + + ++$this->totalGenerated; + } + /** * @param array $arguments */ @@ -71,7 +98,7 @@ public function __call(string $name, array $arguments): mixed } /** - * @return list|null, 'fileName': string|null, 'calls': list, 'method': string, 'arguments': array}>}> + * @return list|null, 'fileName': string|null, 'calls': list, 'method': string, 'arguments': array}>}> */ public function getFiles(): array { diff --git a/src/Debug/Builder/TraceableScreenshotBuilder.php b/src/Debug/Builder/TraceableScreenshotBuilder.php index e2352d3e..fdedbaec 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; @@ -9,7 +10,7 @@ final class TraceableScreenshotBuilder implements ScreenshotBuilderInterface { /** - * @var list|null, 'fileName': string|null, 'calls': list, 'arguments': array}>}> + * @var list|null, 'fileName': string|null, 'calls': list, 'arguments': array}>}> */ private array $screenshots = []; @@ -38,6 +39,7 @@ public function generate(): GotenbergFileResult $swEvent?->stop(); $this->screenshots[] = [ + 'type' => 'sync', 'calls' => $this->calls, 'time' => $swEvent?->getDuration(), 'memory' => $swEvent?->getMemory(), @@ -51,6 +53,31 @@ 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[] = [ + 'type' => 'async', + 'calls' => $this->calls, + 'time' => $swEvent?->getDuration(), + 'memory' => $swEvent?->getMemory(), + 'size' => null, + 'fileName' => null, + ]; + + ++$this->totalGenerated; + } + /** * @param array $arguments */ @@ -72,7 +99,7 @@ public function __call(string $name, array $arguments): mixed } /** - * @return list|null, 'fileName': string|null, 'calls': list, 'method': string, 'arguments': array}>}> + * @return list|null, 'fileName': string|null, 'calls': list, 'method': string, 'arguments': array}>}> */ public function getFiles(): array { diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 32c5c3a9..c069e323 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,155 @@ 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 back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook') + ->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 back to both success and error endpoints - default None. https://gotenberg.dev/docs/webhook') + ->example([ + ['name' => 'X-Custom-Header', 'value' => 'custom-header-value'], + ]) + ->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() + ->example([ + 'https://webhook.site/#!/view/{some-token}', + ['my_route', ['param1' => 'value1', 'param2' => 'value2']], + ]) + ->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 2b4fc804..b9c490fa 100644 --- a/src/DependencyInjection/SensiolabsGotenbergExtension.php +++ b/src/DependencyInjection/SensiolabsGotenbergExtension.php @@ -12,13 +12,44 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\Routing\RequestContext; +/** + * @phpstan-type SensiolabsGotenbergConfiguration array{ + * assets_directory: string, + * http_client: string, + * request_context?: array{base_uri?: string}, + * controller_listener: bool, + * webhook: array + * }>, + * default_options: array{ + * webhook?: string, + * pdf: array{ + * html: array, + * url: array, + * markdown: array, + * office: array, + * merge: array, + * convert: array + * }, + * screenshot: array{ + * html: array, + * url: array, + * markdown: array + * } + * } + * } + * @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 SensiolabsGotenbergConfiguration $config + */ $config = $this->processConfiguration($configuration, $configs); $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); @@ -64,32 +95,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($container, $config, '.sensiolabs_gotenberg.pdf_builder.html', $config['default_options']['pdf']['html']); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.url'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['url'])]); + $this->processDefaultOptions($container, $config, '.sensiolabs_gotenberg.pdf_builder.url', $config['default_options']['pdf']['url']); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.markdown'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['markdown'])]); + $this->processDefaultOptions($container, $config, '.sensiolabs_gotenberg.pdf_builder.markdown', $config['default_options']['pdf']['markdown']); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.office'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['office'])]); + $this->processDefaultOptions($container, $config, '.sensiolabs_gotenberg.pdf_builder.office', $config['default_options']['pdf']['office']); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.merge'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['merge'])]); + $this->processDefaultOptions($container, $config, '.sensiolabs_gotenberg.pdf_builder.merge', $config['default_options']['pdf']['merge']); - $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.convert'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['convert'])]); + $this->processDefaultOptions($container, $config, '.sensiolabs_gotenberg.pdf_builder.convert', $config['default_options']['pdf']['convert']); - $definition = $container->getDefinition('.sensiolabs_gotenberg.screenshot_builder.html'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['screenshot']['html'])]); + $this->processDefaultOptions($container, $config, '.sensiolabs_gotenberg.screenshot_builder.html', $config['default_options']['screenshot']['html']); - $definition = $container->getDefinition('.sensiolabs_gotenberg.screenshot_builder.url'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['screenshot']['url'])]); + $this->processDefaultOptions($container, $config, '.sensiolabs_gotenberg.screenshot_builder.url', $config['default_options']['screenshot']['url']); - $definition = $container->getDefinition('.sensiolabs_gotenberg.screenshot_builder.markdown'); - $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['screenshot']['markdown'])]); + $this->processDefaultOptions($container, $config, '.sensiolabs_gotenberg.screenshot_builder.markdown', $config['default_options']['screenshot']['markdown']); $definition = $container->getDefinition('sensiolabs_gotenberg.asset.base_dir_formatter'); $definition->replaceArgument(2, $config['assets_directory']); @@ -102,8 +129,53 @@ 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 SensiolabsGotenbergConfiguration $config + * @param array $serviceConfig + */ + private function processDefaultOptions(ContainerBuilder $container, array $config, string $serviceId, array $serviceConfig): void + { + $definition = $container->getDefinition($serviceId); + + $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($serviceConfig)]); + + $this->processWebhookOptions($container, $serviceId, $config['webhook'], $config['default_options']['webhook'] ?? null, $serviceConfig); + } + + /** + * @param array}> $webhookConfig + * @param array $config + */ + private function processWebhookOptions(ContainerBuilder $container, string $serviceId, array $webhookConfig, string|null $webhookDefaultConfigName, array $config): void + { + $definition = $container->getDefinition($serviceId); + + $serviceWebhookConfig = $config['webhook'] ?? []; + $webhookConfigName = $serviceWebhookConfig['config_name'] ?? $webhookDefaultConfigName ?? null; + unset($serviceWebhookConfig['config_name']); + + if ([] !== $serviceWebhookConfig && ['extra_http_headers' => []] !== $serviceWebhookConfig) { + $webhookConfig = array_merge($webhookConfig[$webhookConfigName] ?? [], $serviceWebhookConfig); + + $webhookConfigName = ltrim($serviceId, '.'); + $webhookConfigName = ".{$webhookConfigName}.webhook_configuration"; + + $registryDefinition = $container->getDefinition('.sensiolabs_gotenberg.webhook_configuration_registry'); + $registryDefinition->addMethodCall('add', [ + $webhookConfigName, + $webhookConfig, + ]); + } + + if (null === $webhookConfigName) { + return; + } + + $definition->addMethodCall('webhookConfiguration', [$webhookConfigName]); } } diff --git a/src/EventListener/ProcessBuilderOnControllerResponse.php b/src/EventListener/ProcessBuilderOnControllerResponse.php index c24d6c89..5de2dad0 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 00000000..48f2dd22 --- /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/templates/Collector/sensiolabs_gotenberg.html.twig b/templates/Collector/sensiolabs_gotenberg.html.twig index f239df90..2156a138 100644 --- a/templates/Collector/sensiolabs_gotenberg.html.twig +++ b/templates/Collector/sensiolabs_gotenberg.html.twig @@ -102,13 +102,16 @@ {% for index, file in collector.files %} - - {% if file.type == 'pdf' %} - {{ source('@SensiolabsGotenberg/Icon/file-type-pdf.svg') }} - {% elseif file.type == 'screenshot' %} - {{ source('@SensiolabsGotenberg/Icon/file-type-screenshot.svg') }} - {% endif %} - + + {% if file.type == 'pdf' %} + {{ source('@SensiolabsGotenberg/Icon/file-type-pdf.svg') }} + {% elseif file.type == 'screenshot' %} + {{ source('@SensiolabsGotenberg/Icon/file-type-screenshot.svg') }} + {% endif %} + + + {{ source('@SensiolabsGotenberg/Icon/file-' ~ file.request_type ~ '.svg') }} + {{ file.fileName }} diff --git a/templates/Icon/file-async.svg b/templates/Icon/file-async.svg new file mode 100644 index 00000000..b4a49c3a --- /dev/null +++ b/templates/Icon/file-async.svg @@ -0,0 +1 @@ +Async diff --git a/templates/Icon/file-sync.svg b/templates/Icon/file-sync.svg new file mode 100644 index 00000000..9c704b8f --- /dev/null +++ b/templates/Icon/file-sync.svg @@ -0,0 +1 @@ +Sync diff --git a/tests/Builder/AbstractBuilderTestCase.php b/tests/Builder/AbstractBuilderTestCase.php index 241bbdfa..da3e0d2c 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/AsyncBuilderTraitTest.php b/tests/Builder/AsyncBuilderTraitTest.php new file mode 100644 index 00000000..18801ba6 --- /dev/null +++ b/tests/Builder/AsyncBuilderTraitTest.php @@ -0,0 +1,225 @@ +getBuilder(new MockHttpClient([])); + + $this->expectException(MissingRequiredFieldException::class); + $this->expectExceptionMessage('->webhookUrl() was never called.'); + + $builder->generateAsync(); + } + + public function testItGenerateWithJustTheSuccessWebhookUrlSet(): void + { + $callback = function ($method, $url, $options): MockResponse { + $this->assertSame('POST', $method); + $this->assertSame('https://example.com/fake/endpoint', $url); + $this->assertContains('Gotenberg-Webhook-Url: https://webhook.local', $options['headers']); + $this->assertContains('Gotenberg-Webhook-Error-Url: https://webhook.local', $options['headers']); + $this->assertArrayNotHasKey('gotenberg-webhook-method', $options['normalized_headers']); + $this->assertArrayNotHasKey('gotenberg-webhook-error-method', $options['normalized_headers']); + + return new MockResponse('', [ + 'response_headers' => [ + 'Content-Type' => 'text/plain; charset=UTF-8', + 'Gotenberg-Trace' => '{trace}', + ], + ]); + }; + + $builder = $this->getBuilder(new MockHttpClient($callback)); + $builder->webhookUrl('https://webhook.local'); + + $builder->generateAsync(); + } + + public function testItAlsoAcceptsADifferentErrorWebhookUrl(): void + { + $callback = function ($method, $url, $options): MockResponse { + $this->assertContains('Gotenberg-Webhook-Url: https://webhook.local', $options['headers']); + $this->assertContains('Gotenberg-Webhook-Error-Url: https://webhook.local/error', $options['headers']); + + return new MockResponse('', [ + 'response_headers' => [ + 'Content-Type' => 'text/plain; charset=UTF-8', + 'Gotenberg-Trace' => '{trace}', + ], + ]); + }; + + $builder = $this->getBuilder(new MockHttpClient($callback)); + $builder->webhookUrl('https://webhook.local'); + $builder->errorWebhookUrl('https://webhook.local/error'); + + $builder->generateAsync(); + } + + public function testWebhookUrlsCanChangeTheirRespectiveHttpMethods(): void + { + $callback = function ($method, $url, $options): MockResponse { + $this->assertContains('Gotenberg-Webhook-Url: https://webhook.local', $options['headers']); + $this->assertContains('Gotenberg-Webhook-Method: PUT', $options['headers']); + $this->assertContains('Gotenberg-Webhook-Error-Url: https://webhook.local/error', $options['headers']); + $this->assertContains('Gotenberg-Webhook-Error-Method: PATCH', $options['headers']); + + return new MockResponse('', [ + 'response_headers' => [ + 'Content-Type' => 'text/plain; charset=UTF-8', + 'Gotenberg-Trace' => '{trace}', + ], + ]); + }; + + $builder = $this->getBuilder(new MockHttpClient($callback)); + $builder->webhookUrl('https://webhook.local', 'PUT'); + $builder->errorWebhookUrl('https://webhook.local/error', 'PATCH'); + + $builder->generateAsync(); + } + + public function testWebhookUrlsCanSendCustomHttpHeaderToEndpoint(): void + { + $callback = function ($method, $url, $options): MockResponse { + $this->assertContains('Gotenberg-Webhook-Extra-Http-Headers: {"plop":"plop"}', $options['headers']); + + return new MockResponse('', [ + 'response_headers' => [ + 'Content-Type' => 'text/plain; charset=UTF-8', + 'Gotenberg-Trace' => '{trace}', + ], + ]); + }; + + $builder = $this->getBuilder(new MockHttpClient($callback)); + $builder->webhookUrl('https://webhook.local'); + $builder->webhookExtraHeaders(['plop' => 'plop']); + + $builder->generateAsync(); + } + + public function testWebhookUrlsCanBeSetUsingTheRegistry(): void + { + $registry = new class($this) implements WebhookConfigurationRegistryInterface { + public function __construct(private AsyncBuilderTraitTest $assert) + { + } + + public function add(string $name, array $configuration): void + { + // TODO: Implement add() method. + } + + public function get(string $name): array + { + $this->assert->assertSame('fake', $name); + + return [ + 'success' => [ + 'url' => 'https://webhook.local', + 'method' => 'PUT', + ], + 'error' => [ + 'url' => 'https://webhook.local/error', + 'method' => 'PATCH', + ], + 'extra_http_headers' => [ + 'plop' => 'plop', + ], + ]; + } + }; + + $callback = function ($method, $url, $options): MockResponse { + $this->assertContains('Gotenberg-Webhook-Url: https://webhook.local', $options['headers']); + $this->assertContains('Gotenberg-Webhook-Method: PUT', $options['headers']); + $this->assertContains('Gotenberg-Webhook-Error-Url: https://webhook.local/error', $options['headers']); + $this->assertContains('Gotenberg-Webhook-Error-Method: PATCH', $options['headers']); + + return new MockResponse('', [ + 'response_headers' => [ + 'Content-Type' => 'text/plain; charset=UTF-8', + 'Gotenberg-Trace' => '{trace}', + ], + ]); + }; + + $builder = $this->getBuilder(new MockHttpClient($callback), $registry); + $builder->webhookConfiguration('fake'); + + $builder->generateAsync(); + } + + private function getBuilder(MockHttpClient $httpClient, WebhookConfigurationRegistryInterface|null $registry = null): AsyncBuilderInterface + { + $registry ??= new class implements WebhookConfigurationRegistryInterface { + public function add(string $name, array $configuration): void + { + // TODO: Implement add() method. + } + + public function get(string $name): array + { + return [ + 'success' => [ + 'url' => 'https://webhook.local', + 'method' => 'POST', + ], + 'error' => [ + 'url' => 'https://webhook.local/error', + 'method' => 'POST', + ], + ]; + } + }; + + return new class($httpClient, $registry) implements AsyncBuilderInterface { + use AsyncBuilderTrait; + + public function __construct(HttpClientInterface $httpClient, WebhookConfigurationRegistryInterface $registry) + { + $this->client = new GotenbergClient($httpClient); + $this->webhookConfigurationRegistry = $registry; + $this->asset = new AssetBaseDirFormatter(new Filesystem(), '', ''); + } + + protected function getEndpoint(): string + { + return '/fake/endpoint'; + } + + /** + * @param array $configurations + */ + public function setConfigurations(array $configurations): static + { + return $this; + } + }; + } +} diff --git a/tests/Builder/Pdf/AbstractChromiumPdfBuilderTest.php b/tests/Builder/Pdf/AbstractChromiumPdfBuilderTest.php index 3d068f57..5bcc7ffc 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 c69de4f4..d9a63641 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 b2384d63..ce7cc4a4 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 1ce3dcab..1366fd5b 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 9a0b0f1b..db2274b7 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 cccfc05d..668fddef 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 d1fff0d5..89cbc2ad 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 f16b38e2..b4fa54ca 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 0fb50301..a9a1bf61 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 f34bf50d..c47f2911 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 8fe7aeae..63f59909 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 d8511b2d..7d7edaa8 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 9cc4e7ca..0f70d257 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 79cc05b1..7b138844 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 b7b97b2d..5a55ebd1 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,128 @@ 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_configuration', + '.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], "Wrong expected configuration for builder '{$builderId}'."); + + 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_configuration']); + 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_configuration' => [ + 'success' => [ + 'url' => 'https://sensiolabs.com/webhook-on-the-fly', + 'method' => null, + ], + 'error' => [ + 'route' => ['simple_route', []], + 'method' => null, + ], + 'extra_http_headers' => [], + ], + default => self::fail('Unexpected webhook configuration'), + }, $arguments[1], "Configuration mismatch for webhook '{$arguments[0]}'."); + } + } + /** * @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 +551,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 +587,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 +611,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 00000000..7ad4c678 --- /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 e8b099b5..0c7d56a5 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 7309bb42..82fcb68e 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