From 134fe7506ff38826d536945f3ed0693bbdd890d8 Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Mon, 18 Mar 2024 23:05:48 +0100 Subject: [PATCH 1/6] Update configuration --- docs/basic-usage.rst | 16 +- docs/configuration.rst | 9 +- docs/custom-builders.rst | 2 +- docs/customization.rst | 38 ++-- src/Builder/AbstractChromiumPdfBuilder.php | 181 +++++------------- src/Builder/AbstractPdfBuilder.php | 80 +++++++- src/Builder/HtmlPdfBuilder.php | 16 +- src/Builder/LibreOfficePdfBuilder.php | 49 ++--- src/Builder/MarkdownPdfBuilder.php | 26 ++- src/Builder/PdfBuilderInterface.php | 5 + src/Builder/UrlPdfBuilder.php | 7 +- ...ndPropertyInMultipartFormDataException.php | 7 + tests/Builder/HtmlPdfBuilderTest.php | 79 +++----- tests/Builder/MarkdownPdfBuilderTest.php | 2 +- tests/Pdf/GotenbergTest.php | 22 +-- 15 files changed, 249 insertions(+), 290 deletions(-) create mode 100644 src/Exception/NotFoundPropertyInMultipartFormDataException.php diff --git a/docs/basic-usage.rst b/docs/basic-usage.rst index 59738b89..349513a9 100644 --- a/docs/basic-usage.rst +++ b/docs/basic-usage.rst @@ -66,7 +66,7 @@ to the method ``content``. { $urlPdfBuilder = $gotenberg->url(); return $urlPdfBuilder - ->content('https://gotenberg.dev/docs/routes') + ->twigContent('https://gotenberg.dev/docs/routes') ->generate(); // will return directly a stream response } } @@ -100,9 +100,9 @@ variables to the templates through the second argument of the methods $twigPdfBuilder = $gotenberg->twig(); return $twigPdfBuilder - ->content('pdf/body.html.twig', ['invoice' => $invoiceReadModel]) - ->header('pdf/header.html.twig', ['invoice' => $invoiceReadModel]) - ->footer('pdf/footer.html.twig') + ->twigContent('pdf/body.html.twig', ['invoice' => $invoiceReadModel]) + ->twigHeader('pdf/header.html.twig', ['invoice' => $invoiceReadModel]) + ->twigFooter('pdf/footer.html.twig') ->generate(); // will return directly a stream response } } @@ -137,7 +137,7 @@ that expects the local path of the Markdown file to convert. { $markdownBuilder = $gotenberg->markdown(); return $markdownBuilder - ->content('pdf/markdown.html.twig', ['invoice' => $invoiceReadModel]) + ->twigContent('pdf/markdown.html.twig', ['invoice' => $invoiceReadModel]) ->markdownFile('templates/intranet/pdf/file.md') ->generate(); // will return directly a stream response } @@ -229,9 +229,9 @@ dumped into this file. $twigPdfBuilder = $gotenberg->twig(); $twigPdfBuilder - ->content('pdf/body.html.twig', ['datas' => $datas]) - ->header('pdf/header.html.twig', ['datas' => $datas]) - ->footer('pdf/footer.html.twig') + ->twigContent('pdf/body.html.twig', ['datas' => $datas]) + ->twigHeader('pdf/header.html.twig', ['datas' => $datas]) + ->twigFooter('pdf/footer.html.twig') ->assets( 'assets/images/profiles/ceo.jpeg', 'assets/images/profiles/admin.jpeg', diff --git a/docs/configuration.rst b/docs/configuration.rst index 5929c63e..02ae2d3d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -102,10 +102,11 @@ HTTP headers to send by Chromium while loading the HTML document. sensiolabs_gotenberg: base_uri: 'http://localhost:3000' - options: - extra_http_headers: - - { name: 'My-Header', value: 'MyValue' } - - { name: 'User-Agent', value: 'MyValue' } + default_options: + html: + extra_http_headers: + - { name: 'My-Header', value: 'MyValue' } + - { name: 'User-Agent', value: 'MyValue' } .. tip:: diff --git a/docs/custom-builders.rst b/docs/custom-builders.rst index d9ab876d..c0a5634b 100644 --- a/docs/custom-builders.rst +++ b/docs/custom-builders.rst @@ -78,7 +78,7 @@ simply pass it as an argument to the ``generate`` method of Gotenberg. public function yourControllerMethod(Gotenberg $gotenberg): Response { $myBuilder = new HtmlPdfBuilder(); - $myBuilder->content('path/to/my-awesome-template.html'); + $myBuilder->twigContent('path/to/my-awesome-template.html'); return $gotenberg->generate($myBuilder); } diff --git a/docs/customization.rst b/docs/customization.rst index f93ddca9..51dc9bc3 100644 --- a/docs/customization.rst +++ b/docs/customization.rst @@ -48,7 +48,7 @@ The path provided can be relative as well as absolute. $twigPdfBuilder = $gotenberg->twig(); $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->generate() .. tip:: @@ -66,7 +66,7 @@ You can override the default paper size with width and height (in inches): $twigPdfBuilder = $gotenberg->twig(); $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->paperSize(8.5, 11); * Letter - 8.5 x 11 (default) @@ -91,7 +91,7 @@ Define whether to prefer page size as defined by CSS. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->preferCssPageSize(); Print the background graphics @@ -102,7 +102,7 @@ Print the background graphics .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->printBackground(); @@ -116,7 +116,7 @@ Hide the default white background and allow generating PDFs with transparency. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->omitBackground(); .. warning:: @@ -145,7 +145,7 @@ The paper orientation to landscape. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->landscape(); Scale @@ -158,7 +158,7 @@ The scale of the page rendering. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->scale(2.0); Page ranges @@ -171,7 +171,7 @@ Page ranges to print (e.g. 1-5, 8, 11-13). .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->nativePageRanges('1-3'); .. warning:: @@ -186,9 +186,9 @@ You can add a header and/or a footer to each page of the PDF: .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') - ->header('path/to/header.html.twig') - ->footer('path/to/footer.html.twig'); + ->twigContent('path/to/template.html.twig') + ->twigHeader('path/to/header.html.twig') + ->twigFooter('path/to/footer.html.twig'); .. tip:: @@ -206,7 +206,7 @@ you're trying to generate. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->waitDelay('5s'); .. tip:: @@ -223,7 +223,7 @@ You may also wait until a given JavaScript expression. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->waitForExpression("window.globalVar === 'ready'"); .. tip:: @@ -240,7 +240,7 @@ Some websites have dedicated CSS rules for print. Using ``screen`` allows you to .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->emulatedMediaType('screen'); .. tip:: @@ -257,7 +257,7 @@ Override the default User-Agent header. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->userAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"); .. tip:: @@ -274,7 +274,7 @@ HTTP headers to send by Chromium while loading the HTML document. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->extraHttpHeaders([ 'MyHeader' => 'MyValue' ]); @@ -293,7 +293,7 @@ Return a 409 Conflict response if there are exceptions in the Chromium console. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->failOnConsoleExceptions(); .. tip:: @@ -310,7 +310,7 @@ Convert the resulting PDF into the given PDF/A format. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->pdfFormat('PDF/A-2b'); .. tip:: @@ -327,7 +327,7 @@ Enable PDF for Universal Access for optimal accessibility. .. code-block:: php $twigPdfBuilder - ->content('path/to/template.html.twig') + ->twigContent('path/to/template.html.twig') ->pdfUniversalAccess(); .. tip:: diff --git a/src/Builder/AbstractChromiumPdfBuilder.php b/src/Builder/AbstractChromiumPdfBuilder.php index d645993e..29ff0239 100644 --- a/src/Builder/AbstractChromiumPdfBuilder.php +++ b/src/Builder/AbstractChromiumPdfBuilder.php @@ -4,7 +4,6 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Enum\PdfPart; -use Sensiolabs\GotenbergBundle\Exception\ExtraHttpHeadersJsonEncodingException; use Sensiolabs\GotenbergBundle\Exception\InvalidBuilderConfiguration; use Sensiolabs\GotenbergBundle\Exception\PdfPartRenderingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; @@ -65,16 +64,12 @@ public function paperSize(float $width, float $height): static public function paperWidth(float $width): static { - $this->formFields['paperWidth'] = $width; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('paperWidth', $width); } public function paperHeight(float $height): static { - $this->formFields['paperHeight'] = $height; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('paperHeight', $height); } /** @@ -94,30 +89,22 @@ public function margins(float $top, float $bottom, float $left, float $right): s public function marginTop(float $top): static { - $this->formFields['marginTop'] = $top; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('marginTop', $top); } public function marginBottom(float $bottom): static { - $this->formFields['marginBottom'] = $bottom; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('marginBottom', $bottom); } public function marginLeft(float $left): static { - $this->formFields['marginLeft'] = $left; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('marginLeft', $left); } public function marginRight(float $right): static { - $this->formFields['marginRight'] = $right; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('marginRight', $right); } /** @@ -127,9 +114,7 @@ public function marginRight(float $right): static */ public function preferCssPageSize(bool $bool = true): static { - $this->formFields['preferCssPageSize'] = $bool; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('preferCssPageSize', $bool); } /** @@ -139,9 +124,7 @@ public function preferCssPageSize(bool $bool = true): static */ public function printBackground(bool $bool = true): static { - $this->formFields['printBackground'] = $bool; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('printBackground', $bool); } /** @@ -152,9 +135,7 @@ public function printBackground(bool $bool = true): static */ public function omitBackground(bool $bool = true): static { - $this->formFields['omitBackground'] = $bool; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('omitBackground', $bool); } /** @@ -164,9 +145,7 @@ public function omitBackground(bool $bool = true): static */ public function landscape(bool $bool = true): static { - $this->formFields['landscape'] = $bool; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('landscape', $bool); } /** @@ -176,9 +155,7 @@ public function landscape(bool $bool = true): static */ public function scale(float $scale): static { - $this->formFields['scale'] = $scale; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('scale', $scale); } /** @@ -188,9 +165,7 @@ public function scale(float $scale): static */ public function nativePageRanges(string $range): static { - $this->formFields['nativePageRanges'] = $range; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('nativePageRanges', $range); } /** @@ -198,9 +173,9 @@ public function nativePageRanges(string $range): static * * @throws PdfPartRenderingException if the template could not be rendered */ - public function header(string $template, array $context = []): static + public function twigHeader(string $template, array $context = []): static { - return $this->withRenderedPart(PdfPart::HeaderPart, $template, $context); + return $this->addTwigTemplate(PdfPart::HeaderPart, $template, $context); } /** @@ -208,25 +183,25 @@ public function header(string $template, array $context = []): static * * @throws PdfPartRenderingException if the template could not be rendered */ - public function footer(string $template, array $context = []): static + public function twigFooter(string $template, array $context = []): static { - return $this->withRenderedPart(PdfPart::FooterPart, $template, $context); + return $this->addTwigTemplate(PdfPart::FooterPart, $template, $context); } /** * HTML file containing the header. (default None). */ - public function headerFile(string $path): static + public function htmlHeader(string $path): static { - return $this->withPdfPartFile(PdfPart::HeaderPart, $path); + return $this->addHtmlTemplate(PdfPart::HeaderPart, $path); } /** * HTML file containing the footer. (default None). */ - public function footerFile(string $path): static + public function htmlFooter(string $path): static { - return $this->withPdfPartFile(PdfPart::FooterPart, $path); + return $this->addHtmlTemplate(PdfPart::FooterPart, $path); } /** @@ -234,8 +209,6 @@ public function footerFile(string $path): static */ public function assets(string ...$paths): static { - $this->formFields['assets'] = []; - foreach ($paths as $path) { $this->addAsset($path); } @@ -251,8 +224,7 @@ public function addAsset(string $path): static $resolvedPath = $this->asset->resolve($path); $dataPart = new DataPart(new DataPartFile($resolvedPath)); - - $this->formFields['assets'][$resolvedPath] = $dataPart; + $this->multipartFormData[] = ['files' => $dataPart]; return $this; } @@ -265,9 +237,7 @@ public function addAsset(string $path): static */ public function waitDelay(string $delay): static { - $this->formFields['waitDelay'] = $delay; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('waitDelay', $delay); } /** @@ -280,9 +250,7 @@ public function waitDelay(string $delay): static */ public function waitForExpression(string $expression): static { - $this->formFields['waitForExpression'] = $expression; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('waitForExpression', $expression); } /** @@ -292,9 +260,7 @@ public function waitForExpression(string $expression): static */ public function emulatedMediaType(string $mediaType): static { - $this->formFields['emulatedMediaType'] = $mediaType; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('emulatedMediaType', $mediaType); } /** @@ -304,9 +270,7 @@ public function emulatedMediaType(string $mediaType): static */ public function userAgent(string $userAgent): static { - $this->formFields['userAgent'] = $userAgent; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('userAgent', $userAgent); } /** @@ -319,25 +283,19 @@ public function userAgent(string $userAgent): static */ public function extraHttpHeaders(array $headers): static { - $this->formFields['extraHttpHeaders'] = $headers; + $hasProperty = $this->multipartFormDataPropertyExistenceChecker('extraHttpHeaders'); - return $this; - } + if (!$hasProperty) { + $this->multipartFormData[] = ['extraHttpHeaders' => $headers]; - /** - * Adds extra HTTP headers that Chromium will send when loading the HTML - * document. (default None). - * - * @see https://gotenberg.dev/docs/routes#custom-http-headers - * - * @param array $headers - */ - public function addExtraHttpHeaders(array $headers): static - { - $this->formFields['extraHttpHeaders'] = [ - ...$this->formFields['extraHttpHeaders'], - ...$headers, - ]; + return $this; + } + + $index = $this->getIndexForExistingPropertyToOverride('extraHttpHeaders'); + + $existingHeadersConfig = $this->multipartFormData[$index]; + + $this->multipartFormData[$index] = ['extraHttpHeaders' => [...$existingHeadersConfig['extraHttpHeaders'], ...$headers]]; return $this; } @@ -350,9 +308,7 @@ public function addExtraHttpHeaders(array $headers): static */ public function failOnConsoleExceptions(bool $bool = true): static { - $this->formFields['failOnConsoleExceptions'] = $bool; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('failOnConsoleExceptions', $bool); } /** @@ -362,9 +318,7 @@ public function failOnConsoleExceptions(bool $bool = true): static */ public function pdfFormat(string $format): static { - $this->formFields['pdfa'] = $format; - - return $this; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('pdfa', $format); } /** @@ -374,66 +328,17 @@ public function pdfFormat(string $format): static */ public function pdfUniversalAccess(bool $bool = true): static { - $this->formFields['pdfua'] = $bool; - - return $this; - } - - /** - * @throws ExtraHttpHeadersJsonEncodingException - */ - public function getMultipartFormData(): array - { - $formFields = $this->formFields; - $multipartFormData = []; - - $extraHttpHeaders = $this->formFields['extraHttpHeaders'] ?? []; - if ([] !== $extraHttpHeaders) { - try { - $extraHttpHeaders = json_encode($extraHttpHeaders, \JSON_THROW_ON_ERROR); - } catch (\JsonException $exception) { - throw new ExtraHttpHeadersJsonEncodingException('Could not encode extra HTTP headers into JSON', previous: $exception); - } - - $multipartFormData[] = [ - 'extraHttpHeaders' => $extraHttpHeaders, - ]; - unset($formFields['extraHttpHeaders']); - } - - foreach ($formFields as $key => $value) { - if (\is_bool($value)) { - $multipartFormData[] = [ - $key => $value ? 'true' : 'false', - ]; - continue; - } - - if (\is_array($value)) { - foreach ($value as $nestedValue) { - $multipartFormData[] = [ - ($nestedValue instanceof DataPart ? 'files' : $key) => $nestedValue, - ]; - } - continue; - } - - $multipartFormData[] = [ - ($value instanceof DataPart ? 'files' : $key) => $value, - ]; - } - - return $multipartFormData; + return $this->addPropertyToMultipartFormDataWithExistenceCheck('pdfua', $bool); } - protected function withPdfPartFile(PdfPart $pdfPart, string $path): static + protected function addHtmlTemplate(PdfPart $pdfPart, string $path): static { $dataPart = new DataPart( new DataPartFile($this->asset->resolve($path)), $pdfPart->value, ); - $this->formFields[$pdfPart->value] = $dataPart; + $this->multipartFormData[] = ['files' => $dataPart]; return $this; } @@ -443,7 +348,7 @@ protected function withPdfPartFile(PdfPart $pdfPart, string $path): static * * @throws PdfPartRenderingException if the template could not be rendered */ - protected function withRenderedPart(PdfPart $pdfPart, string $template, array $context = []): static + protected function addTwigTemplate(PdfPart $pdfPart, string $template, array $context = []): static { if (!$this->twig instanceof Environment) { throw new \LogicException(sprintf('Twig is required to use "%s" method. Try to run "composer require symfony/twig-bundle".', __METHOD__)); @@ -455,7 +360,7 @@ protected function withRenderedPart(PdfPart $pdfPart, string $template, array $c throw new PdfPartRenderingException(sprintf('Could not render template "%s" into PDF part "%s". %s', $template, $pdfPart->value, $error->getMessage()), previous: $error); } - $this->formFields[$pdfPart->value] = new DataPart($html, $pdfPart->value, 'text/html'); + $this->multipartFormData[] = ['files' => new DataPart($html, $pdfPart->value, 'text/html')]; return $this; } diff --git a/src/Builder/AbstractPdfBuilder.php b/src/Builder/AbstractPdfBuilder.php index c9ded560..27e0ed7f 100644 --- a/src/Builder/AbstractPdfBuilder.php +++ b/src/Builder/AbstractPdfBuilder.php @@ -4,16 +4,19 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Client\PdfResponse; +use Sensiolabs\GotenbergBundle\Exception\ExtraHttpHeadersJsonEncodingException; use Sensiolabs\GotenbergBundle\Exception\MissingRequiredFieldException; +use Sensiolabs\GotenbergBundle\Exception\NotFoundPropertyInMultipartFormDataException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\Mime\Part\DataPart; abstract class AbstractPdfBuilder implements PdfBuilderInterface { /** - * @var array + * @var list> */ - protected array $formFields = []; + protected array $multipartFormData = []; public function __construct( protected readonly GotenbergClientInterface $gotenbergClient, @@ -22,13 +25,14 @@ public function __construct( } /** - * Compiles the form values into a multipart form data array to send to the HTTP client. - * - * @return array> + * @return array> * * @throws MissingRequiredFieldException */ - abstract public function getMultipartFormData(): array; + public function getMultipartFormData(): array + { + return $this->formatMultipartFormData($this->multipartFormData); + } /** * The Gotenberg API endpoint path. @@ -57,4 +61,68 @@ protected function assertFileExtension(string $path, array $validExtensions): vo throw new \InvalidArgumentException(sprintf('The file extension "%s" is not available in Gotenberg.', $extension)); } } + + protected function addPropertyToMultipartFormDataWithExistenceCheck(string $property, mixed $value): static + { + $hasProperty = $this->multipartFormDataPropertyExistenceChecker($property); + + if (!$hasProperty) { + $this->multipartFormData[] = [$property => $value]; + + return $this; + } + + $this->multipartFormData[$this->getIndexForExistingPropertyToOverride($property)] = [$property => $value]; + + return $this; + } + + protected function getIndexForExistingPropertyToOverride(string $property): int + { + foreach ($this->multipartFormData as $index => $data) { + if (\array_key_exists($property, $data)) { + return $index; + } + } + + throw new NotFoundPropertyInMultipartFormDataException(sprintf('Property %s not found in multipartFormData.', $property)); + } + + protected function multipartFormDataPropertyExistenceChecker(string $property): bool + { + foreach ($this->multipartFormData as $data) { + if (\array_key_exists($property, $data)) { + return true; + } + } + + return false; + } + + /** + * @param array> $multipartFormData + * + * @return array> + */ + private function formatMultipartFormData(array $multipartFormData): array + { + foreach ($multipartFormData as $index => $data) { + foreach ($data as $key => $value) { + if (!$value instanceof DataPart) { + if (\is_array($value)) { + try { + $extraHttpHeaders = json_encode($value, \JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ExtraHttpHeadersJsonEncodingException('Could not encode extra HTTP headers into JSON', previous: $exception); + } + $multipartFormData[$index] = [$key => $extraHttpHeaders]; + } else { + $multipartFormData[$index] = [$key => (string) $value]; + } + } + } + } + + return $multipartFormData; + } } diff --git a/src/Builder/HtmlPdfBuilder.php b/src/Builder/HtmlPdfBuilder.php index 12396e43..e3fd4482 100644 --- a/src/Builder/HtmlPdfBuilder.php +++ b/src/Builder/HtmlPdfBuilder.php @@ -10,27 +10,33 @@ final class HtmlPdfBuilder extends AbstractChromiumPdfBuilder { private const ENDPOINT = '/forms/chromium/convert/html'; + private bool $hasContent = false; + /** * @param array $context * * @throws PdfPartRenderingException if the template could not be rendered */ - public function content(string $template, array $context = []): self + public function twigContent(string $template, array $context = []): self { - return $this->withRenderedPart(PdfPart::BodyPart, $template, $context); + $this->hasContent = true; + + return $this->addTwigTemplate(PdfPart::BodyPart, $template, $context); } /** * The HTML file to convert into PDF. */ - public function contentFile(string $path): self + public function htmlContent(string $path): self { - return $this->withPdfPartFile(PdfPart::BodyPart, $path); + $this->hasContent = true; + + return $this->addHtmlTemplate(PdfPart::BodyPart, $path); } public function getMultipartFormData(): array { - if (!\array_key_exists(PdfPart::BodyPart->value, $this->formFields)) { + if (!$this->hasContent) { throw new MissingRequiredFieldException('Content is required'); } diff --git a/src/Builder/LibreOfficePdfBuilder.php b/src/Builder/LibreOfficePdfBuilder.php index 463582e1..071b8c96 100644 --- a/src/Builder/LibreOfficePdfBuilder.php +++ b/src/Builder/LibreOfficePdfBuilder.php @@ -11,6 +11,8 @@ final class LibreOfficePdfBuilder extends AbstractPdfBuilder { private const ENDPOINT = '/forms/libreoffice/convert'; + private bool $hasOfficeFile = false; + private const AVAILABLE_EXTENSIONS = [ 'bib', 'doc', 'xml', 'docx', 'fodt', 'html', 'ltx', 'txt', 'odt', 'ott', 'pdb', 'pdf', 'psw', 'rtf', 'sdw', 'stw', 'sxw', 'uot', 'vor', 'wps', 'epub', 'png', 'bmp', 'emf', 'eps', 'fodg', 'gif', 'jpg', 'met', 'odd', @@ -39,7 +41,7 @@ public function setConfigurations(array $configurations): self */ public function landscape(bool $bool = true): self { - $this->formFields['landscape'] = $bool; + $this->multipartFormData[] = ['landscape' => $bool]; return $this; } @@ -51,7 +53,7 @@ public function landscape(bool $bool = true): self */ public function nativePageRanges(string $range): self { - $this->formFields['nativePageRanges'] = $range; + $this->multipartFormData[] = ['nativePageRanges' => $range]; return $this; } @@ -61,7 +63,7 @@ public function nativePageRanges(string $range): self */ public function pdfFormat(string $format): self { - $this->formFields['pdfa'] = $format; + $this->multipartFormData[] = ['pdfa' => $format]; return $this; } @@ -71,7 +73,7 @@ public function pdfFormat(string $format): self */ public function pdfUniversalAccess(bool $bool = true): self { - $this->formFields['pdfua'] = $bool; + $this->multipartFormData[] = ['pdfua' => $bool]; return $this; } @@ -81,7 +83,7 @@ public function pdfUniversalAccess(bool $bool = true): self */ public function merge(bool $bool = true): self { - $this->formFields['merge'] = $bool; + $this->multipartFormData[] = ['merge' => $bool]; return $this; } @@ -91,52 +93,25 @@ public function merge(bool $bool = true): self */ public function files(string ...$paths): self { - $this->formFields['files'] = []; - foreach ($paths as $path) { $this->assertFileExtension($path, self::AVAILABLE_EXTENSIONS); $dataPart = new DataPart(new DataPartFile($this->asset->resolve($path))); - - $this->formFields['files'][$path] = $dataPart; + $this->multipartFormData[] = ['files' => $dataPart]; } + $this->hasOfficeFile = true; + return $this; } public function getMultipartFormData(): array { - if ([] === ($this->formFields['files'] ?? [])) { + if (!$this->hasOfficeFile) { throw new MissingRequiredFieldException('At least one office file is required'); } - $formFields = $this->formFields; - $multipartFormData = []; - - $files = $this->formFields['files'] ?? []; - if ([] !== $files) { - foreach ($files as $dataPart) { - $multipartFormData[] = [ - 'files' => $dataPart, - ]; - } - unset($formFields['files']); - } - - foreach ($formFields as $key => $value) { - if (\is_bool($value)) { - $multipartFormData[] = [ - $key => $value ? 'true' : 'false', - ]; - continue; - } - - $multipartFormData[] = [ - $key => $value, - ]; - } - - return $multipartFormData; + return parent::getMultipartFormData(); } protected function getEndpoint(): string diff --git a/src/Builder/MarkdownPdfBuilder.php b/src/Builder/MarkdownPdfBuilder.php index a7a8a1fe..8d66e9e5 100644 --- a/src/Builder/MarkdownPdfBuilder.php +++ b/src/Builder/MarkdownPdfBuilder.php @@ -12,6 +12,10 @@ final class MarkdownPdfBuilder extends AbstractChromiumPdfBuilder { private const ENDPOINT = '/forms/chromium/convert/markdown'; + private bool $hasWrapper = false; + + private bool $hasMdFile = false; + /** * The HTML file that wraps the markdown content, rendered from a Twig template. * @@ -19,41 +23,45 @@ final class MarkdownPdfBuilder extends AbstractChromiumPdfBuilder * * @throws PdfPartRenderingException if the template could not be rendered */ - public function wrapper(string $template, array $context = []): self + public function twigWrapper(string $template, array $context = []): self { - return $this->withRenderedPart(PdfPart::BodyPart, $template, $context); + $this->hasWrapper = true; + + return $this->addTwigTemplate(PdfPart::BodyPart, $template, $context); } /** * The HTML file that wraps the markdown content. */ - public function wrapperFile(string $path): self + public function htmlWrapper(string $path): self { - return $this->withPdfPartFile(PdfPart::BodyPart, $path); + $this->hasWrapper = true; + + return $this->addHtmlTemplate(PdfPart::BodyPart, $path); } public function files(string ...$paths): self { - $this->formFields['files'] = []; - foreach ($paths as $path) { $this->assertFileExtension($path, ['md']); $dataPart = new DataPart(new DataPartFile($this->asset->resolve($path))); - $this->formFields['files'][$path] = $dataPart; + $this->multipartFormData[] = ['files' => $dataPart]; } + $this->hasMdFile = true; + return $this; } public function getMultipartFormData(): array { - if (!\array_key_exists(PdfPart::BodyPart->value, $this->formFields)) { + if (!$this->hasWrapper) { throw new MissingRequiredFieldException('HTML template is required'); } - if ([] === ($this->formFields['files'] ?? [])) { + if (!$this->hasMdFile) { throw new MissingRequiredFieldException('At least one markdown file is required'); } diff --git a/src/Builder/PdfBuilderInterface.php b/src/Builder/PdfBuilderInterface.php index d39d80b1..e3e320d0 100644 --- a/src/Builder/PdfBuilderInterface.php +++ b/src/Builder/PdfBuilderInterface.php @@ -6,6 +6,11 @@ interface PdfBuilderInterface { + /** + * @return list> + */ + public function getMultipartFormData(): array; + /** * Generates the PDF and returns the response. */ diff --git a/src/Builder/UrlPdfBuilder.php b/src/Builder/UrlPdfBuilder.php index 7e493de0..2704fead 100644 --- a/src/Builder/UrlPdfBuilder.php +++ b/src/Builder/UrlPdfBuilder.php @@ -8,19 +8,22 @@ final class UrlPdfBuilder extends AbstractChromiumPdfBuilder { private const ENDPOINT = '/forms/chromium/convert/url'; + private bool $hasUrl = false; + /** * URL of the page you want to convert into PDF. */ public function url(string $url): self { - $this->formFields['url'] = $url; + $this->multipartFormData[] = ['url' => $url]; + $this->hasUrl = true; return $this; } public function getMultipartFormData(): array { - if (!\array_key_exists('url', $this->formFields)) { + if (!$this->hasUrl) { throw new MissingRequiredFieldException('URL is required'); } diff --git a/src/Exception/NotFoundPropertyInMultipartFormDataException.php b/src/Exception/NotFoundPropertyInMultipartFormDataException.php new file mode 100644 index 00000000..db48998e --- /dev/null +++ b/src/Exception/NotFoundPropertyInMultipartFormDataException.php @@ -0,0 +1,7 @@ +contentFile('content.html'); + $builder->htmlContent('content.html'); $builder->setConfigurations(self::getUserConfig()); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(21, $multipartFormData); - self::assertSame(['extraHttpHeaders' => '{"MyHeader":"Value","User-Agent":"MyValue"}'], $multipartFormData[0]); - self::assertSame(['paperWidth' => 33.1], $multipartFormData[2]); - self::assertSame(['paperHeight' => 46.8], $multipartFormData[3]); - self::assertSame(['marginTop' => 1.0], $multipartFormData[4]); - self::assertSame(['marginBottom' => 1.0], $multipartFormData[5]); - self::assertSame(['marginLeft' => 1.0], $multipartFormData[6]); - self::assertSame(['marginRight' => 1.0], $multipartFormData[7]); - self::assertSame(['preferCssPageSize' => 'true'], $multipartFormData[8]); - self::assertSame(['printBackground' => 'true'], $multipartFormData[9]); - self::assertSame(['omitBackground' => 'true'], $multipartFormData[10]); - self::assertSame(['landscape' => 'true'], $multipartFormData[11]); - self::assertSame(['scale' => 1.5], $multipartFormData[12]); - self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[13]); - self::assertSame(['waitDelay' => '10s'], $multipartFormData[14]); - self::assertSame(['waitForExpression' => 'window.globalVar === "ready"'], $multipartFormData[15]); - self::assertSame(['emulatedMediaType' => 'screen'], $multipartFormData[16]); - self::assertSame(['userAgent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML => like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'], $multipartFormData[17]); - self::assertSame(['failOnConsoleExceptions' => 'true'], $multipartFormData[18]); + self::assertSame(['paperWidth' => '33.1'], $multipartFormData[1]); + self::assertSame(['paperHeight' => '46.8'], $multipartFormData[2]); + self::assertSame(['marginTop' => '1'], $multipartFormData[3]); + self::assertSame(['marginBottom' => '1'], $multipartFormData[4]); + self::assertSame(['marginLeft' => '1'], $multipartFormData[5]); + self::assertSame(['marginRight' => '1'], $multipartFormData[6]); + self::assertSame(['preferCssPageSize' => '1'], $multipartFormData[7]); + self::assertSame(['printBackground' => '1'], $multipartFormData[8]); + self::assertSame(['omitBackground' => '1'], $multipartFormData[9]); + self::assertSame(['landscape' => '1'], $multipartFormData[10]); + self::assertSame(['scale' => '1.5'], $multipartFormData[11]); + self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[12]); + self::assertSame(['waitDelay' => '10s'], $multipartFormData[13]); + self::assertSame(['waitForExpression' => 'window.globalVar === "ready"'], $multipartFormData[14]); + self::assertSame(['emulatedMediaType' => 'screen'], $multipartFormData[15]); + self::assertSame(['userAgent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML => like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'], $multipartFormData[16]); + self::assertSame(['extraHttpHeaders' => '{"MyHeader":"Value","User-Agent":"MyValue"}'], $multipartFormData[17]); + self::assertSame(['failOnConsoleExceptions' => '1'], $multipartFormData[18]); self::assertSame(['pdfa' => 'PDF/A-1a'], $multipartFormData[19]); - self::assertSame(['pdfua' => 'true'], $multipartFormData[20]); + self::assertSame(['pdfua' => '1'], $multipartFormData[20]); - self::assertIsArray($multipartFormData[1]); - self::assertCount(1, $multipartFormData[1]); - self::assertArrayHasKey('files', $multipartFormData[1]); - self::assertInstanceOf(DataPart::class, $multipartFormData[1]['files']); - self::assertSame('index.html', $multipartFormData[1]['files']->getFilename()); + self::assertIsArray($multipartFormData[0]); + self::assertCount(1, $multipartFormData[0]); + self::assertArrayHasKey('files', $multipartFormData[0]); + self::assertInstanceOf(DataPart::class, $multipartFormData[0]['files']); + self::assertSame('index.html', $multipartFormData[0]['files']->getFilename()); } public function testWithTemplate(): void @@ -64,7 +63,7 @@ public function testWithTemplate(): void $assetBaseDirFormatter = new AssetBaseDirFormatter(new Filesystem(), self::FIXTURE_DIR, self::FIXTURE_DIR); $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter, self::$twig); - $builder->content('content.html.twig'); + $builder->twigContent('content.html.twig'); $multipartFormData = $builder->getMultipartFormData(); @@ -82,7 +81,7 @@ public function testWithAssets(): void $assetBaseDirFormatter = new AssetBaseDirFormatter(new Filesystem(), self::FIXTURE_DIR, self::FIXTURE_DIR); $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter); - $builder->contentFile('content.html'); + $builder->htmlContent('content.html'); $builder->assets('assets/logo.png'); $multipartFormData = $builder->getMultipartFormData(); @@ -102,8 +101,8 @@ public function testWithHeader(): void $assetBaseDirFormatter = new AssetBaseDirFormatter(new Filesystem(), self::FIXTURE_DIR, self::FIXTURE_DIR); $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter); - $builder->headerFile('header.html'); - $builder->contentFile('content.html'); + $builder->htmlHeader('header.html'); + $builder->htmlContent('content.html'); $multipartFormData = $builder->getMultipartFormData(); @@ -126,25 +125,7 @@ public function testInvalidTwigTemplate(): void $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter, self::$twig); - $builder->content('invalid.html.twig'); - } - - public function testInvalidExtraHttpHeaders(): void - { - $this->expectException(ExtraHttpHeadersJsonEncodingException::class); - $this->expectExceptionMessage('Could not encode extra HTTP headers into JSON'); - - $client = $this->createMock(GotenbergClientInterface::class); - $assetBaseDirFormatter = new AssetBaseDirFormatter(new Filesystem(), self::FIXTURE_DIR, self::FIXTURE_DIR); - - $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter); - $builder->contentFile('content.html'); - // @phpstan-ignore-next-line - $builder->extraHttpHeaders([ - 'invalid' => tmpfile(), - ]); - - $builder->getMultipartFormData(); + $builder->twigContent('invalid.html.twig'); } /** diff --git a/tests/Builder/MarkdownPdfBuilderTest.php b/tests/Builder/MarkdownPdfBuilderTest.php index cf1b281e..87a78ab0 100644 --- a/tests/Builder/MarkdownPdfBuilderTest.php +++ b/tests/Builder/MarkdownPdfBuilderTest.php @@ -22,7 +22,7 @@ public function testMarkdownFile(): void $builder = new MarkdownPdfBuilder($client, $assetBaseDirFormatter); $builder - ->wrapperFile('template.html') + ->htmlWrapper('template.html') ->files('assets/file.md') ; diff --git a/tests/Pdf/GotenbergTest.php b/tests/Pdf/GotenbergTest.php index 84b8dc71..98af404f 100644 --- a/tests/Pdf/GotenbergTest.php +++ b/tests/Pdf/GotenbergTest.php @@ -52,16 +52,16 @@ public function testHtmlBuilderFactory(): void $twig, ); $builder = $gotenberg->html(); - $builder->contentFile('content.html'); + $builder->htmlContent('content.html'); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(3, $multipartFormData); self::assertArrayHasKey(0, $multipartFormData); - self::assertSame(['marginTop' => 3.0], $multipartFormData[0]); + self::assertSame(['marginTop' => '3'], $multipartFormData[0]); self::assertArrayHasKey(1, $multipartFormData); - self::assertSame(['marginBottom' => 1.0], $multipartFormData[1]); + self::assertSame(['marginBottom' => '1'], $multipartFormData[1]); self::assertArrayHasKey(2, $multipartFormData); self::assertIsArray($multipartFormData[2]); @@ -88,7 +88,7 @@ public function testMarkdownBuilderFactory(): void ); $builder = $gotenberg->markdown(); $builder->files('assets/file.md'); - $builder->wrapperFile('wrapper.html'); + $builder->htmlWrapper('wrapper.html'); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(2, $multipartFormData); @@ -127,13 +127,13 @@ public function testOfficeBuilderFactory(): void self::assertCount(2, $multipartFormData); - self::assertArrayHasKey(0, $multipartFormData); - self::assertIsArray($multipartFormData[0]); - self::assertArrayHasKey('files', $multipartFormData[0]); - self::assertInstanceOf(DataPart::class, $multipartFormData[0]['files']); - self::assertSame('document.odt', $multipartFormData[0]['files']->getFilename()); - self::assertArrayHasKey(1, $multipartFormData); - self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[1]); + self::assertIsArray($multipartFormData[1]); + self::assertArrayHasKey('files', $multipartFormData[1]); + self::assertInstanceOf(DataPart::class, $multipartFormData[1]['files']); + self::assertSame('document.odt', $multipartFormData[1]['files']->getFilename()); + + self::assertArrayHasKey(0, $multipartFormData); + self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[0]); } } From b7401e9d515add73ae00f26675c36b538938261f Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Tue, 19 Mar 2024 13:48:49 +0100 Subject: [PATCH 2/6] Revert "Update configuration" This reverts commit 134fe7506ff38826d536945f3ed0693bbdd890d8. --- docs/basic-usage.rst | 16 +- docs/configuration.rst | 9 +- docs/custom-builders.rst | 2 +- docs/customization.rst | 38 ++-- src/Builder/AbstractChromiumPdfBuilder.php | 181 +++++++++++++----- src/Builder/AbstractPdfBuilder.php | 80 +------- src/Builder/HtmlPdfBuilder.php | 16 +- src/Builder/LibreOfficePdfBuilder.php | 49 +++-- src/Builder/MarkdownPdfBuilder.php | 26 +-- src/Builder/PdfBuilderInterface.php | 5 - src/Builder/UrlPdfBuilder.php | 7 +- ...ndPropertyInMultipartFormDataException.php | 7 - tests/Builder/HtmlPdfBuilderTest.php | 79 +++++--- tests/Builder/MarkdownPdfBuilderTest.php | 2 +- tests/Pdf/GotenbergTest.php | 22 +-- 15 files changed, 290 insertions(+), 249 deletions(-) delete mode 100644 src/Exception/NotFoundPropertyInMultipartFormDataException.php diff --git a/docs/basic-usage.rst b/docs/basic-usage.rst index 349513a9..59738b89 100644 --- a/docs/basic-usage.rst +++ b/docs/basic-usage.rst @@ -66,7 +66,7 @@ to the method ``content``. { $urlPdfBuilder = $gotenberg->url(); return $urlPdfBuilder - ->twigContent('https://gotenberg.dev/docs/routes') + ->content('https://gotenberg.dev/docs/routes') ->generate(); // will return directly a stream response } } @@ -100,9 +100,9 @@ variables to the templates through the second argument of the methods $twigPdfBuilder = $gotenberg->twig(); return $twigPdfBuilder - ->twigContent('pdf/body.html.twig', ['invoice' => $invoiceReadModel]) - ->twigHeader('pdf/header.html.twig', ['invoice' => $invoiceReadModel]) - ->twigFooter('pdf/footer.html.twig') + ->content('pdf/body.html.twig', ['invoice' => $invoiceReadModel]) + ->header('pdf/header.html.twig', ['invoice' => $invoiceReadModel]) + ->footer('pdf/footer.html.twig') ->generate(); // will return directly a stream response } } @@ -137,7 +137,7 @@ that expects the local path of the Markdown file to convert. { $markdownBuilder = $gotenberg->markdown(); return $markdownBuilder - ->twigContent('pdf/markdown.html.twig', ['invoice' => $invoiceReadModel]) + ->content('pdf/markdown.html.twig', ['invoice' => $invoiceReadModel]) ->markdownFile('templates/intranet/pdf/file.md') ->generate(); // will return directly a stream response } @@ -229,9 +229,9 @@ dumped into this file. $twigPdfBuilder = $gotenberg->twig(); $twigPdfBuilder - ->twigContent('pdf/body.html.twig', ['datas' => $datas]) - ->twigHeader('pdf/header.html.twig', ['datas' => $datas]) - ->twigFooter('pdf/footer.html.twig') + ->content('pdf/body.html.twig', ['datas' => $datas]) + ->header('pdf/header.html.twig', ['datas' => $datas]) + ->footer('pdf/footer.html.twig') ->assets( 'assets/images/profiles/ceo.jpeg', 'assets/images/profiles/admin.jpeg', diff --git a/docs/configuration.rst b/docs/configuration.rst index 02ae2d3d..5929c63e 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -102,11 +102,10 @@ HTTP headers to send by Chromium while loading the HTML document. sensiolabs_gotenberg: base_uri: 'http://localhost:3000' - default_options: - html: - extra_http_headers: - - { name: 'My-Header', value: 'MyValue' } - - { name: 'User-Agent', value: 'MyValue' } + options: + extra_http_headers: + - { name: 'My-Header', value: 'MyValue' } + - { name: 'User-Agent', value: 'MyValue' } .. tip:: diff --git a/docs/custom-builders.rst b/docs/custom-builders.rst index c0a5634b..d9ab876d 100644 --- a/docs/custom-builders.rst +++ b/docs/custom-builders.rst @@ -78,7 +78,7 @@ simply pass it as an argument to the ``generate`` method of Gotenberg. public function yourControllerMethod(Gotenberg $gotenberg): Response { $myBuilder = new HtmlPdfBuilder(); - $myBuilder->twigContent('path/to/my-awesome-template.html'); + $myBuilder->content('path/to/my-awesome-template.html'); return $gotenberg->generate($myBuilder); } diff --git a/docs/customization.rst b/docs/customization.rst index 51dc9bc3..f93ddca9 100644 --- a/docs/customization.rst +++ b/docs/customization.rst @@ -48,7 +48,7 @@ The path provided can be relative as well as absolute. $twigPdfBuilder = $gotenberg->twig(); $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->generate() .. tip:: @@ -66,7 +66,7 @@ You can override the default paper size with width and height (in inches): $twigPdfBuilder = $gotenberg->twig(); $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->paperSize(8.5, 11); * Letter - 8.5 x 11 (default) @@ -91,7 +91,7 @@ Define whether to prefer page size as defined by CSS. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->preferCssPageSize(); Print the background graphics @@ -102,7 +102,7 @@ Print the background graphics .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->printBackground(); @@ -116,7 +116,7 @@ Hide the default white background and allow generating PDFs with transparency. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->omitBackground(); .. warning:: @@ -145,7 +145,7 @@ The paper orientation to landscape. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->landscape(); Scale @@ -158,7 +158,7 @@ The scale of the page rendering. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->scale(2.0); Page ranges @@ -171,7 +171,7 @@ Page ranges to print (e.g. 1-5, 8, 11-13). .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->nativePageRanges('1-3'); .. warning:: @@ -186,9 +186,9 @@ You can add a header and/or a footer to each page of the PDF: .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') - ->twigHeader('path/to/header.html.twig') - ->twigFooter('path/to/footer.html.twig'); + ->content('path/to/template.html.twig') + ->header('path/to/header.html.twig') + ->footer('path/to/footer.html.twig'); .. tip:: @@ -206,7 +206,7 @@ you're trying to generate. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->waitDelay('5s'); .. tip:: @@ -223,7 +223,7 @@ You may also wait until a given JavaScript expression. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->waitForExpression("window.globalVar === 'ready'"); .. tip:: @@ -240,7 +240,7 @@ Some websites have dedicated CSS rules for print. Using ``screen`` allows you to .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->emulatedMediaType('screen'); .. tip:: @@ -257,7 +257,7 @@ Override the default User-Agent header. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->userAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"); .. tip:: @@ -274,7 +274,7 @@ HTTP headers to send by Chromium while loading the HTML document. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->extraHttpHeaders([ 'MyHeader' => 'MyValue' ]); @@ -293,7 +293,7 @@ Return a 409 Conflict response if there are exceptions in the Chromium console. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->failOnConsoleExceptions(); .. tip:: @@ -310,7 +310,7 @@ Convert the resulting PDF into the given PDF/A format. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->pdfFormat('PDF/A-2b'); .. tip:: @@ -327,7 +327,7 @@ Enable PDF for Universal Access for optimal accessibility. .. code-block:: php $twigPdfBuilder - ->twigContent('path/to/template.html.twig') + ->content('path/to/template.html.twig') ->pdfUniversalAccess(); .. tip:: diff --git a/src/Builder/AbstractChromiumPdfBuilder.php b/src/Builder/AbstractChromiumPdfBuilder.php index 29ff0239..d645993e 100644 --- a/src/Builder/AbstractChromiumPdfBuilder.php +++ b/src/Builder/AbstractChromiumPdfBuilder.php @@ -4,6 +4,7 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Enum\PdfPart; +use Sensiolabs\GotenbergBundle\Exception\ExtraHttpHeadersJsonEncodingException; use Sensiolabs\GotenbergBundle\Exception\InvalidBuilderConfiguration; use Sensiolabs\GotenbergBundle\Exception\PdfPartRenderingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; @@ -64,12 +65,16 @@ public function paperSize(float $width, float $height): static public function paperWidth(float $width): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('paperWidth', $width); + $this->formFields['paperWidth'] = $width; + + return $this; } public function paperHeight(float $height): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('paperHeight', $height); + $this->formFields['paperHeight'] = $height; + + return $this; } /** @@ -89,22 +94,30 @@ public function margins(float $top, float $bottom, float $left, float $right): s public function marginTop(float $top): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('marginTop', $top); + $this->formFields['marginTop'] = $top; + + return $this; } public function marginBottom(float $bottom): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('marginBottom', $bottom); + $this->formFields['marginBottom'] = $bottom; + + return $this; } public function marginLeft(float $left): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('marginLeft', $left); + $this->formFields['marginLeft'] = $left; + + return $this; } public function marginRight(float $right): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('marginRight', $right); + $this->formFields['marginRight'] = $right; + + return $this; } /** @@ -114,7 +127,9 @@ public function marginRight(float $right): static */ public function preferCssPageSize(bool $bool = true): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('preferCssPageSize', $bool); + $this->formFields['preferCssPageSize'] = $bool; + + return $this; } /** @@ -124,7 +139,9 @@ public function preferCssPageSize(bool $bool = true): static */ public function printBackground(bool $bool = true): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('printBackground', $bool); + $this->formFields['printBackground'] = $bool; + + return $this; } /** @@ -135,7 +152,9 @@ public function printBackground(bool $bool = true): static */ public function omitBackground(bool $bool = true): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('omitBackground', $bool); + $this->formFields['omitBackground'] = $bool; + + return $this; } /** @@ -145,7 +164,9 @@ public function omitBackground(bool $bool = true): static */ public function landscape(bool $bool = true): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('landscape', $bool); + $this->formFields['landscape'] = $bool; + + return $this; } /** @@ -155,7 +176,9 @@ public function landscape(bool $bool = true): static */ public function scale(float $scale): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('scale', $scale); + $this->formFields['scale'] = $scale; + + return $this; } /** @@ -165,7 +188,9 @@ public function scale(float $scale): static */ public function nativePageRanges(string $range): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('nativePageRanges', $range); + $this->formFields['nativePageRanges'] = $range; + + return $this; } /** @@ -173,9 +198,9 @@ public function nativePageRanges(string $range): static * * @throws PdfPartRenderingException if the template could not be rendered */ - public function twigHeader(string $template, array $context = []): static + public function header(string $template, array $context = []): static { - return $this->addTwigTemplate(PdfPart::HeaderPart, $template, $context); + return $this->withRenderedPart(PdfPart::HeaderPart, $template, $context); } /** @@ -183,25 +208,25 @@ public function twigHeader(string $template, array $context = []): static * * @throws PdfPartRenderingException if the template could not be rendered */ - public function twigFooter(string $template, array $context = []): static + public function footer(string $template, array $context = []): static { - return $this->addTwigTemplate(PdfPart::FooterPart, $template, $context); + return $this->withRenderedPart(PdfPart::FooterPart, $template, $context); } /** * HTML file containing the header. (default None). */ - public function htmlHeader(string $path): static + public function headerFile(string $path): static { - return $this->addHtmlTemplate(PdfPart::HeaderPart, $path); + return $this->withPdfPartFile(PdfPart::HeaderPart, $path); } /** * HTML file containing the footer. (default None). */ - public function htmlFooter(string $path): static + public function footerFile(string $path): static { - return $this->addHtmlTemplate(PdfPart::FooterPart, $path); + return $this->withPdfPartFile(PdfPart::FooterPart, $path); } /** @@ -209,6 +234,8 @@ public function htmlFooter(string $path): static */ public function assets(string ...$paths): static { + $this->formFields['assets'] = []; + foreach ($paths as $path) { $this->addAsset($path); } @@ -224,7 +251,8 @@ public function addAsset(string $path): static $resolvedPath = $this->asset->resolve($path); $dataPart = new DataPart(new DataPartFile($resolvedPath)); - $this->multipartFormData[] = ['files' => $dataPart]; + + $this->formFields['assets'][$resolvedPath] = $dataPart; return $this; } @@ -237,7 +265,9 @@ public function addAsset(string $path): static */ public function waitDelay(string $delay): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('waitDelay', $delay); + $this->formFields['waitDelay'] = $delay; + + return $this; } /** @@ -250,7 +280,9 @@ public function waitDelay(string $delay): static */ public function waitForExpression(string $expression): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('waitForExpression', $expression); + $this->formFields['waitForExpression'] = $expression; + + return $this; } /** @@ -260,7 +292,9 @@ public function waitForExpression(string $expression): static */ public function emulatedMediaType(string $mediaType): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('emulatedMediaType', $mediaType); + $this->formFields['emulatedMediaType'] = $mediaType; + + return $this; } /** @@ -270,7 +304,9 @@ public function emulatedMediaType(string $mediaType): static */ public function userAgent(string $userAgent): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('userAgent', $userAgent); + $this->formFields['userAgent'] = $userAgent; + + return $this; } /** @@ -283,19 +319,25 @@ public function userAgent(string $userAgent): static */ public function extraHttpHeaders(array $headers): static { - $hasProperty = $this->multipartFormDataPropertyExistenceChecker('extraHttpHeaders'); - - if (!$hasProperty) { - $this->multipartFormData[] = ['extraHttpHeaders' => $headers]; + $this->formFields['extraHttpHeaders'] = $headers; - return $this; - } - - $index = $this->getIndexForExistingPropertyToOverride('extraHttpHeaders'); - - $existingHeadersConfig = $this->multipartFormData[$index]; + return $this; + } - $this->multipartFormData[$index] = ['extraHttpHeaders' => [...$existingHeadersConfig['extraHttpHeaders'], ...$headers]]; + /** + * Adds extra HTTP headers that Chromium will send when loading the HTML + * document. (default None). + * + * @see https://gotenberg.dev/docs/routes#custom-http-headers + * + * @param array $headers + */ + public function addExtraHttpHeaders(array $headers): static + { + $this->formFields['extraHttpHeaders'] = [ + ...$this->formFields['extraHttpHeaders'], + ...$headers, + ]; return $this; } @@ -308,7 +350,9 @@ public function extraHttpHeaders(array $headers): static */ public function failOnConsoleExceptions(bool $bool = true): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('failOnConsoleExceptions', $bool); + $this->formFields['failOnConsoleExceptions'] = $bool; + + return $this; } /** @@ -318,7 +362,9 @@ public function failOnConsoleExceptions(bool $bool = true): static */ public function pdfFormat(string $format): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('pdfa', $format); + $this->formFields['pdfa'] = $format; + + return $this; } /** @@ -328,17 +374,66 @@ public function pdfFormat(string $format): static */ public function pdfUniversalAccess(bool $bool = true): static { - return $this->addPropertyToMultipartFormDataWithExistenceCheck('pdfua', $bool); + $this->formFields['pdfua'] = $bool; + + return $this; + } + + /** + * @throws ExtraHttpHeadersJsonEncodingException + */ + public function getMultipartFormData(): array + { + $formFields = $this->formFields; + $multipartFormData = []; + + $extraHttpHeaders = $this->formFields['extraHttpHeaders'] ?? []; + if ([] !== $extraHttpHeaders) { + try { + $extraHttpHeaders = json_encode($extraHttpHeaders, \JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ExtraHttpHeadersJsonEncodingException('Could not encode extra HTTP headers into JSON', previous: $exception); + } + + $multipartFormData[] = [ + 'extraHttpHeaders' => $extraHttpHeaders, + ]; + unset($formFields['extraHttpHeaders']); + } + + foreach ($formFields as $key => $value) { + if (\is_bool($value)) { + $multipartFormData[] = [ + $key => $value ? 'true' : 'false', + ]; + continue; + } + + if (\is_array($value)) { + foreach ($value as $nestedValue) { + $multipartFormData[] = [ + ($nestedValue instanceof DataPart ? 'files' : $key) => $nestedValue, + ]; + } + continue; + } + + $multipartFormData[] = [ + ($value instanceof DataPart ? 'files' : $key) => $value, + ]; + } + + return $multipartFormData; } - protected function addHtmlTemplate(PdfPart $pdfPart, string $path): static + protected function withPdfPartFile(PdfPart $pdfPart, string $path): static { $dataPart = new DataPart( new DataPartFile($this->asset->resolve($path)), $pdfPart->value, ); - $this->multipartFormData[] = ['files' => $dataPart]; + $this->formFields[$pdfPart->value] = $dataPart; return $this; } @@ -348,7 +443,7 @@ protected function addHtmlTemplate(PdfPart $pdfPart, string $path): static * * @throws PdfPartRenderingException if the template could not be rendered */ - protected function addTwigTemplate(PdfPart $pdfPart, string $template, array $context = []): static + protected function withRenderedPart(PdfPart $pdfPart, string $template, array $context = []): static { if (!$this->twig instanceof Environment) { throw new \LogicException(sprintf('Twig is required to use "%s" method. Try to run "composer require symfony/twig-bundle".', __METHOD__)); @@ -360,7 +455,7 @@ protected function addTwigTemplate(PdfPart $pdfPart, string $template, array $co throw new PdfPartRenderingException(sprintf('Could not render template "%s" into PDF part "%s". %s', $template, $pdfPart->value, $error->getMessage()), previous: $error); } - $this->multipartFormData[] = ['files' => new DataPart($html, $pdfPart->value, 'text/html')]; + $this->formFields[$pdfPart->value] = new DataPart($html, $pdfPart->value, 'text/html'); return $this; } diff --git a/src/Builder/AbstractPdfBuilder.php b/src/Builder/AbstractPdfBuilder.php index 27e0ed7f..c9ded560 100644 --- a/src/Builder/AbstractPdfBuilder.php +++ b/src/Builder/AbstractPdfBuilder.php @@ -4,19 +4,16 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Client\PdfResponse; -use Sensiolabs\GotenbergBundle\Exception\ExtraHttpHeadersJsonEncodingException; use Sensiolabs\GotenbergBundle\Exception\MissingRequiredFieldException; -use Sensiolabs\GotenbergBundle\Exception\NotFoundPropertyInMultipartFormDataException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Symfony\Component\HttpFoundation\File\File; -use Symfony\Component\Mime\Part\DataPart; abstract class AbstractPdfBuilder implements PdfBuilderInterface { /** - * @var list> + * @var array */ - protected array $multipartFormData = []; + protected array $formFields = []; public function __construct( protected readonly GotenbergClientInterface $gotenbergClient, @@ -25,14 +22,13 @@ public function __construct( } /** - * @return array> + * Compiles the form values into a multipart form data array to send to the HTTP client. + * + * @return array> * * @throws MissingRequiredFieldException */ - public function getMultipartFormData(): array - { - return $this->formatMultipartFormData($this->multipartFormData); - } + abstract public function getMultipartFormData(): array; /** * The Gotenberg API endpoint path. @@ -61,68 +57,4 @@ protected function assertFileExtension(string $path, array $validExtensions): vo throw new \InvalidArgumentException(sprintf('The file extension "%s" is not available in Gotenberg.', $extension)); } } - - protected function addPropertyToMultipartFormDataWithExistenceCheck(string $property, mixed $value): static - { - $hasProperty = $this->multipartFormDataPropertyExistenceChecker($property); - - if (!$hasProperty) { - $this->multipartFormData[] = [$property => $value]; - - return $this; - } - - $this->multipartFormData[$this->getIndexForExistingPropertyToOverride($property)] = [$property => $value]; - - return $this; - } - - protected function getIndexForExistingPropertyToOverride(string $property): int - { - foreach ($this->multipartFormData as $index => $data) { - if (\array_key_exists($property, $data)) { - return $index; - } - } - - throw new NotFoundPropertyInMultipartFormDataException(sprintf('Property %s not found in multipartFormData.', $property)); - } - - protected function multipartFormDataPropertyExistenceChecker(string $property): bool - { - foreach ($this->multipartFormData as $data) { - if (\array_key_exists($property, $data)) { - return true; - } - } - - return false; - } - - /** - * @param array> $multipartFormData - * - * @return array> - */ - private function formatMultipartFormData(array $multipartFormData): array - { - foreach ($multipartFormData as $index => $data) { - foreach ($data as $key => $value) { - if (!$value instanceof DataPart) { - if (\is_array($value)) { - try { - $extraHttpHeaders = json_encode($value, \JSON_THROW_ON_ERROR); - } catch (\JsonException $exception) { - throw new ExtraHttpHeadersJsonEncodingException('Could not encode extra HTTP headers into JSON', previous: $exception); - } - $multipartFormData[$index] = [$key => $extraHttpHeaders]; - } else { - $multipartFormData[$index] = [$key => (string) $value]; - } - } - } - } - - return $multipartFormData; - } } diff --git a/src/Builder/HtmlPdfBuilder.php b/src/Builder/HtmlPdfBuilder.php index e3fd4482..12396e43 100644 --- a/src/Builder/HtmlPdfBuilder.php +++ b/src/Builder/HtmlPdfBuilder.php @@ -10,33 +10,27 @@ final class HtmlPdfBuilder extends AbstractChromiumPdfBuilder { private const ENDPOINT = '/forms/chromium/convert/html'; - private bool $hasContent = false; - /** * @param array $context * * @throws PdfPartRenderingException if the template could not be rendered */ - public function twigContent(string $template, array $context = []): self + public function content(string $template, array $context = []): self { - $this->hasContent = true; - - return $this->addTwigTemplate(PdfPart::BodyPart, $template, $context); + return $this->withRenderedPart(PdfPart::BodyPart, $template, $context); } /** * The HTML file to convert into PDF. */ - public function htmlContent(string $path): self + public function contentFile(string $path): self { - $this->hasContent = true; - - return $this->addHtmlTemplate(PdfPart::BodyPart, $path); + return $this->withPdfPartFile(PdfPart::BodyPart, $path); } public function getMultipartFormData(): array { - if (!$this->hasContent) { + if (!\array_key_exists(PdfPart::BodyPart->value, $this->formFields)) { throw new MissingRequiredFieldException('Content is required'); } diff --git a/src/Builder/LibreOfficePdfBuilder.php b/src/Builder/LibreOfficePdfBuilder.php index 071b8c96..463582e1 100644 --- a/src/Builder/LibreOfficePdfBuilder.php +++ b/src/Builder/LibreOfficePdfBuilder.php @@ -11,8 +11,6 @@ final class LibreOfficePdfBuilder extends AbstractPdfBuilder { private const ENDPOINT = '/forms/libreoffice/convert'; - private bool $hasOfficeFile = false; - private const AVAILABLE_EXTENSIONS = [ 'bib', 'doc', 'xml', 'docx', 'fodt', 'html', 'ltx', 'txt', 'odt', 'ott', 'pdb', 'pdf', 'psw', 'rtf', 'sdw', 'stw', 'sxw', 'uot', 'vor', 'wps', 'epub', 'png', 'bmp', 'emf', 'eps', 'fodg', 'gif', 'jpg', 'met', 'odd', @@ -41,7 +39,7 @@ public function setConfigurations(array $configurations): self */ public function landscape(bool $bool = true): self { - $this->multipartFormData[] = ['landscape' => $bool]; + $this->formFields['landscape'] = $bool; return $this; } @@ -53,7 +51,7 @@ public function landscape(bool $bool = true): self */ public function nativePageRanges(string $range): self { - $this->multipartFormData[] = ['nativePageRanges' => $range]; + $this->formFields['nativePageRanges'] = $range; return $this; } @@ -63,7 +61,7 @@ public function nativePageRanges(string $range): self */ public function pdfFormat(string $format): self { - $this->multipartFormData[] = ['pdfa' => $format]; + $this->formFields['pdfa'] = $format; return $this; } @@ -73,7 +71,7 @@ public function pdfFormat(string $format): self */ public function pdfUniversalAccess(bool $bool = true): self { - $this->multipartFormData[] = ['pdfua' => $bool]; + $this->formFields['pdfua'] = $bool; return $this; } @@ -83,7 +81,7 @@ public function pdfUniversalAccess(bool $bool = true): self */ public function merge(bool $bool = true): self { - $this->multipartFormData[] = ['merge' => $bool]; + $this->formFields['merge'] = $bool; return $this; } @@ -93,25 +91,52 @@ public function merge(bool $bool = true): self */ public function files(string ...$paths): self { + $this->formFields['files'] = []; + foreach ($paths as $path) { $this->assertFileExtension($path, self::AVAILABLE_EXTENSIONS); $dataPart = new DataPart(new DataPartFile($this->asset->resolve($path))); - $this->multipartFormData[] = ['files' => $dataPart]; - } - $this->hasOfficeFile = true; + $this->formFields['files'][$path] = $dataPart; + } return $this; } public function getMultipartFormData(): array { - if (!$this->hasOfficeFile) { + if ([] === ($this->formFields['files'] ?? [])) { throw new MissingRequiredFieldException('At least one office file is required'); } - return parent::getMultipartFormData(); + $formFields = $this->formFields; + $multipartFormData = []; + + $files = $this->formFields['files'] ?? []; + if ([] !== $files) { + foreach ($files as $dataPart) { + $multipartFormData[] = [ + 'files' => $dataPart, + ]; + } + unset($formFields['files']); + } + + foreach ($formFields as $key => $value) { + if (\is_bool($value)) { + $multipartFormData[] = [ + $key => $value ? 'true' : 'false', + ]; + continue; + } + + $multipartFormData[] = [ + $key => $value, + ]; + } + + return $multipartFormData; } protected function getEndpoint(): string diff --git a/src/Builder/MarkdownPdfBuilder.php b/src/Builder/MarkdownPdfBuilder.php index 8d66e9e5..a7a8a1fe 100644 --- a/src/Builder/MarkdownPdfBuilder.php +++ b/src/Builder/MarkdownPdfBuilder.php @@ -12,10 +12,6 @@ final class MarkdownPdfBuilder extends AbstractChromiumPdfBuilder { private const ENDPOINT = '/forms/chromium/convert/markdown'; - private bool $hasWrapper = false; - - private bool $hasMdFile = false; - /** * The HTML file that wraps the markdown content, rendered from a Twig template. * @@ -23,45 +19,41 @@ final class MarkdownPdfBuilder extends AbstractChromiumPdfBuilder * * @throws PdfPartRenderingException if the template could not be rendered */ - public function twigWrapper(string $template, array $context = []): self + public function wrapper(string $template, array $context = []): self { - $this->hasWrapper = true; - - return $this->addTwigTemplate(PdfPart::BodyPart, $template, $context); + return $this->withRenderedPart(PdfPart::BodyPart, $template, $context); } /** * The HTML file that wraps the markdown content. */ - public function htmlWrapper(string $path): self + public function wrapperFile(string $path): self { - $this->hasWrapper = true; - - return $this->addHtmlTemplate(PdfPart::BodyPart, $path); + return $this->withPdfPartFile(PdfPart::BodyPart, $path); } public function files(string ...$paths): self { + $this->formFields['files'] = []; + foreach ($paths as $path) { $this->assertFileExtension($path, ['md']); $dataPart = new DataPart(new DataPartFile($this->asset->resolve($path))); - $this->multipartFormData[] = ['files' => $dataPart]; + $this->formFields['files'][$path] = $dataPart; } - $this->hasMdFile = true; - return $this; } public function getMultipartFormData(): array { - if (!$this->hasWrapper) { + if (!\array_key_exists(PdfPart::BodyPart->value, $this->formFields)) { throw new MissingRequiredFieldException('HTML template is required'); } - if (!$this->hasMdFile) { + if ([] === ($this->formFields['files'] ?? [])) { throw new MissingRequiredFieldException('At least one markdown file is required'); } diff --git a/src/Builder/PdfBuilderInterface.php b/src/Builder/PdfBuilderInterface.php index e3e320d0..d39d80b1 100644 --- a/src/Builder/PdfBuilderInterface.php +++ b/src/Builder/PdfBuilderInterface.php @@ -6,11 +6,6 @@ interface PdfBuilderInterface { - /** - * @return list> - */ - public function getMultipartFormData(): array; - /** * Generates the PDF and returns the response. */ diff --git a/src/Builder/UrlPdfBuilder.php b/src/Builder/UrlPdfBuilder.php index 2704fead..7e493de0 100644 --- a/src/Builder/UrlPdfBuilder.php +++ b/src/Builder/UrlPdfBuilder.php @@ -8,22 +8,19 @@ final class UrlPdfBuilder extends AbstractChromiumPdfBuilder { private const ENDPOINT = '/forms/chromium/convert/url'; - private bool $hasUrl = false; - /** * URL of the page you want to convert into PDF. */ public function url(string $url): self { - $this->multipartFormData[] = ['url' => $url]; - $this->hasUrl = true; + $this->formFields['url'] = $url; return $this; } public function getMultipartFormData(): array { - if (!$this->hasUrl) { + if (!\array_key_exists('url', $this->formFields)) { throw new MissingRequiredFieldException('URL is required'); } diff --git a/src/Exception/NotFoundPropertyInMultipartFormDataException.php b/src/Exception/NotFoundPropertyInMultipartFormDataException.php deleted file mode 100644 index db48998e..00000000 --- a/src/Exception/NotFoundPropertyInMultipartFormDataException.php +++ /dev/null @@ -1,7 +0,0 @@ -htmlContent('content.html'); + $builder->contentFile('content.html'); $builder->setConfigurations(self::getUserConfig()); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(21, $multipartFormData); - self::assertSame(['paperWidth' => '33.1'], $multipartFormData[1]); - self::assertSame(['paperHeight' => '46.8'], $multipartFormData[2]); - self::assertSame(['marginTop' => '1'], $multipartFormData[3]); - self::assertSame(['marginBottom' => '1'], $multipartFormData[4]); - self::assertSame(['marginLeft' => '1'], $multipartFormData[5]); - self::assertSame(['marginRight' => '1'], $multipartFormData[6]); - self::assertSame(['preferCssPageSize' => '1'], $multipartFormData[7]); - self::assertSame(['printBackground' => '1'], $multipartFormData[8]); - self::assertSame(['omitBackground' => '1'], $multipartFormData[9]); - self::assertSame(['landscape' => '1'], $multipartFormData[10]); - self::assertSame(['scale' => '1.5'], $multipartFormData[11]); - self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[12]); - self::assertSame(['waitDelay' => '10s'], $multipartFormData[13]); - self::assertSame(['waitForExpression' => 'window.globalVar === "ready"'], $multipartFormData[14]); - self::assertSame(['emulatedMediaType' => 'screen'], $multipartFormData[15]); - self::assertSame(['userAgent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML => like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'], $multipartFormData[16]); - self::assertSame(['extraHttpHeaders' => '{"MyHeader":"Value","User-Agent":"MyValue"}'], $multipartFormData[17]); - self::assertSame(['failOnConsoleExceptions' => '1'], $multipartFormData[18]); + self::assertSame(['extraHttpHeaders' => '{"MyHeader":"Value","User-Agent":"MyValue"}'], $multipartFormData[0]); + self::assertSame(['paperWidth' => 33.1], $multipartFormData[2]); + self::assertSame(['paperHeight' => 46.8], $multipartFormData[3]); + self::assertSame(['marginTop' => 1.0], $multipartFormData[4]); + self::assertSame(['marginBottom' => 1.0], $multipartFormData[5]); + self::assertSame(['marginLeft' => 1.0], $multipartFormData[6]); + self::assertSame(['marginRight' => 1.0], $multipartFormData[7]); + self::assertSame(['preferCssPageSize' => 'true'], $multipartFormData[8]); + self::assertSame(['printBackground' => 'true'], $multipartFormData[9]); + self::assertSame(['omitBackground' => 'true'], $multipartFormData[10]); + self::assertSame(['landscape' => 'true'], $multipartFormData[11]); + self::assertSame(['scale' => 1.5], $multipartFormData[12]); + self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[13]); + self::assertSame(['waitDelay' => '10s'], $multipartFormData[14]); + self::assertSame(['waitForExpression' => 'window.globalVar === "ready"'], $multipartFormData[15]); + self::assertSame(['emulatedMediaType' => 'screen'], $multipartFormData[16]); + self::assertSame(['userAgent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML => like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'], $multipartFormData[17]); + self::assertSame(['failOnConsoleExceptions' => 'true'], $multipartFormData[18]); self::assertSame(['pdfa' => 'PDF/A-1a'], $multipartFormData[19]); - self::assertSame(['pdfua' => '1'], $multipartFormData[20]); + self::assertSame(['pdfua' => 'true'], $multipartFormData[20]); - self::assertIsArray($multipartFormData[0]); - self::assertCount(1, $multipartFormData[0]); - self::assertArrayHasKey('files', $multipartFormData[0]); - self::assertInstanceOf(DataPart::class, $multipartFormData[0]['files']); - self::assertSame('index.html', $multipartFormData[0]['files']->getFilename()); + self::assertIsArray($multipartFormData[1]); + self::assertCount(1, $multipartFormData[1]); + self::assertArrayHasKey('files', $multipartFormData[1]); + self::assertInstanceOf(DataPart::class, $multipartFormData[1]['files']); + self::assertSame('index.html', $multipartFormData[1]['files']->getFilename()); } public function testWithTemplate(): void @@ -63,7 +64,7 @@ public function testWithTemplate(): void $assetBaseDirFormatter = new AssetBaseDirFormatter(new Filesystem(), self::FIXTURE_DIR, self::FIXTURE_DIR); $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter, self::$twig); - $builder->twigContent('content.html.twig'); + $builder->content('content.html.twig'); $multipartFormData = $builder->getMultipartFormData(); @@ -81,7 +82,7 @@ public function testWithAssets(): void $assetBaseDirFormatter = new AssetBaseDirFormatter(new Filesystem(), self::FIXTURE_DIR, self::FIXTURE_DIR); $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter); - $builder->htmlContent('content.html'); + $builder->contentFile('content.html'); $builder->assets('assets/logo.png'); $multipartFormData = $builder->getMultipartFormData(); @@ -101,8 +102,8 @@ public function testWithHeader(): void $assetBaseDirFormatter = new AssetBaseDirFormatter(new Filesystem(), self::FIXTURE_DIR, self::FIXTURE_DIR); $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter); - $builder->htmlHeader('header.html'); - $builder->htmlContent('content.html'); + $builder->headerFile('header.html'); + $builder->contentFile('content.html'); $multipartFormData = $builder->getMultipartFormData(); @@ -125,7 +126,25 @@ public function testInvalidTwigTemplate(): void $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter, self::$twig); - $builder->twigContent('invalid.html.twig'); + $builder->content('invalid.html.twig'); + } + + public function testInvalidExtraHttpHeaders(): void + { + $this->expectException(ExtraHttpHeadersJsonEncodingException::class); + $this->expectExceptionMessage('Could not encode extra HTTP headers into JSON'); + + $client = $this->createMock(GotenbergClientInterface::class); + $assetBaseDirFormatter = new AssetBaseDirFormatter(new Filesystem(), self::FIXTURE_DIR, self::FIXTURE_DIR); + + $builder = new HtmlPdfBuilder($client, $assetBaseDirFormatter); + $builder->contentFile('content.html'); + // @phpstan-ignore-next-line + $builder->extraHttpHeaders([ + 'invalid' => tmpfile(), + ]); + + $builder->getMultipartFormData(); } /** diff --git a/tests/Builder/MarkdownPdfBuilderTest.php b/tests/Builder/MarkdownPdfBuilderTest.php index 87a78ab0..cf1b281e 100644 --- a/tests/Builder/MarkdownPdfBuilderTest.php +++ b/tests/Builder/MarkdownPdfBuilderTest.php @@ -22,7 +22,7 @@ public function testMarkdownFile(): void $builder = new MarkdownPdfBuilder($client, $assetBaseDirFormatter); $builder - ->htmlWrapper('template.html') + ->wrapperFile('template.html') ->files('assets/file.md') ; diff --git a/tests/Pdf/GotenbergTest.php b/tests/Pdf/GotenbergTest.php index 98af404f..84b8dc71 100644 --- a/tests/Pdf/GotenbergTest.php +++ b/tests/Pdf/GotenbergTest.php @@ -52,16 +52,16 @@ public function testHtmlBuilderFactory(): void $twig, ); $builder = $gotenberg->html(); - $builder->htmlContent('content.html'); + $builder->contentFile('content.html'); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(3, $multipartFormData); self::assertArrayHasKey(0, $multipartFormData); - self::assertSame(['marginTop' => '3'], $multipartFormData[0]); + self::assertSame(['marginTop' => 3.0], $multipartFormData[0]); self::assertArrayHasKey(1, $multipartFormData); - self::assertSame(['marginBottom' => '1'], $multipartFormData[1]); + self::assertSame(['marginBottom' => 1.0], $multipartFormData[1]); self::assertArrayHasKey(2, $multipartFormData); self::assertIsArray($multipartFormData[2]); @@ -88,7 +88,7 @@ public function testMarkdownBuilderFactory(): void ); $builder = $gotenberg->markdown(); $builder->files('assets/file.md'); - $builder->htmlWrapper('wrapper.html'); + $builder->wrapperFile('wrapper.html'); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(2, $multipartFormData); @@ -127,13 +127,13 @@ public function testOfficeBuilderFactory(): void self::assertCount(2, $multipartFormData); - self::assertArrayHasKey(1, $multipartFormData); - self::assertIsArray($multipartFormData[1]); - self::assertArrayHasKey('files', $multipartFormData[1]); - self::assertInstanceOf(DataPart::class, $multipartFormData[1]['files']); - self::assertSame('document.odt', $multipartFormData[1]['files']->getFilename()); - self::assertArrayHasKey(0, $multipartFormData); - self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[0]); + self::assertIsArray($multipartFormData[0]); + self::assertArrayHasKey('files', $multipartFormData[0]); + self::assertInstanceOf(DataPart::class, $multipartFormData[0]['files']); + self::assertSame('document.odt', $multipartFormData[0]['files']->getFilename()); + + self::assertArrayHasKey(1, $multipartFormData); + self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[1]); } } From a41b3c04673e90e5e1c130ca3961911b9415ada8 Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Mon, 1 Apr 2024 14:21:11 +0200 Subject: [PATCH 3/6] Update configuration with normalizer --- src/Builder/AbstractChromiumPdfBuilder.php | 62 ++--------- src/Builder/AbstractPdfBuilder.php | 122 +++++++++++++++++++-- src/Builder/LibreOfficePdfBuilder.php | 28 +---- tests/Builder/HtmlPdfBuilderTest.php | 45 ++++---- tests/Pdf/GotenbergTest.php | 15 +-- 5 files changed, 153 insertions(+), 119 deletions(-) diff --git a/src/Builder/AbstractChromiumPdfBuilder.php b/src/Builder/AbstractChromiumPdfBuilder.php index d645993e..ec9cf616 100644 --- a/src/Builder/AbstractChromiumPdfBuilder.php +++ b/src/Builder/AbstractChromiumPdfBuilder.php @@ -4,7 +4,6 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Enum\PdfPart; -use Sensiolabs\GotenbergBundle\Exception\ExtraHttpHeadersJsonEncodingException; use Sensiolabs\GotenbergBundle\Exception\InvalidBuilderConfiguration; use Sensiolabs\GotenbergBundle\Exception\PdfPartRenderingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; @@ -311,7 +310,7 @@ public function userAgent(string $userAgent): static /** * Sets extra HTTP headers that Chromium will send when loading the HTML - * document. (default None). + * document. (default None). (overrides any previous headers). * * @see https://gotenberg.dev/docs/routes#custom-http-headers * @@ -334,10 +333,14 @@ public function extraHttpHeaders(array $headers): static */ public function addExtraHttpHeaders(array $headers): static { - $this->formFields['extraHttpHeaders'] = [ - ...$this->formFields['extraHttpHeaders'], - ...$headers, - ]; + if (\array_key_exists('extraHttpHeaders', $this->formFields)) { + $this->formFields['extraHttpHeaders'] = [ + ...$this->formFields['extraHttpHeaders'], + ...$headers, + ]; + } else { + $this->extraHttpHeaders($headers); + } return $this; } @@ -379,53 +382,6 @@ public function pdfUniversalAccess(bool $bool = true): static return $this; } - /** - * @throws ExtraHttpHeadersJsonEncodingException - */ - public function getMultipartFormData(): array - { - $formFields = $this->formFields; - $multipartFormData = []; - - $extraHttpHeaders = $this->formFields['extraHttpHeaders'] ?? []; - if ([] !== $extraHttpHeaders) { - try { - $extraHttpHeaders = json_encode($extraHttpHeaders, \JSON_THROW_ON_ERROR); - } catch (\JsonException $exception) { - throw new ExtraHttpHeadersJsonEncodingException('Could not encode extra HTTP headers into JSON', previous: $exception); - } - - $multipartFormData[] = [ - 'extraHttpHeaders' => $extraHttpHeaders, - ]; - unset($formFields['extraHttpHeaders']); - } - - foreach ($formFields as $key => $value) { - if (\is_bool($value)) { - $multipartFormData[] = [ - $key => $value ? 'true' : 'false', - ]; - continue; - } - - if (\is_array($value)) { - foreach ($value as $nestedValue) { - $multipartFormData[] = [ - ($nestedValue instanceof DataPart ? 'files' : $key) => $nestedValue, - ]; - } - continue; - } - - $multipartFormData[] = [ - ($value instanceof DataPart ? 'files' : $key) => $value, - ]; - } - - return $multipartFormData; - } - protected function withPdfPartFile(PdfPart $pdfPart, string $path): static { $dataPart = new DataPart( diff --git a/src/Builder/AbstractPdfBuilder.php b/src/Builder/AbstractPdfBuilder.php index c9ded560..bf9aa044 100644 --- a/src/Builder/AbstractPdfBuilder.php +++ b/src/Builder/AbstractPdfBuilder.php @@ -4,9 +4,11 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Client\PdfResponse; -use Sensiolabs\GotenbergBundle\Exception\MissingRequiredFieldException; +use Sensiolabs\GotenbergBundle\Enum\PdfPart; +use Sensiolabs\GotenbergBundle\Exception\ExtraHttpHeadersJsonEncodingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\Mime\Part\DataPart; abstract class AbstractPdfBuilder implements PdfBuilderInterface { @@ -15,20 +17,39 @@ abstract class AbstractPdfBuilder implements PdfBuilderInterface */ protected array $formFields = []; + /** + * @var array|non-empty-string|int|float|bool|DataPart>)> + */ + private array $normalizers; + public function __construct( protected readonly GotenbergClientInterface $gotenbergClient, protected readonly AssetBaseDirFormatter $asset, ) { - } + $this->normalizers = [ + 'extraHttpHeaders' => static function (mixed $value): array { + try { + $extraHttpHeaders = json_encode($value, \JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ExtraHttpHeadersJsonEncodingException('Could not encode extra HTTP headers into JSON', previous: $exception); + } - /** - * Compiles the form values into a multipart form data array to send to the HTTP client. - * - * @return array> - * - * @throws MissingRequiredFieldException - */ - abstract public function getMultipartFormData(): array; + return ['extraHttpHeaders' => $extraHttpHeaders]; + }, + 'assets' => static function (array $value): array { + return ['files' => $value]; + }, + PdfPart::HeaderPart->value => static function (DataPart $value): array { + return ['files' => $value]; + }, + PdfPart::BodyPart->value => static function (DataPart $value): array { + return ['files' => $value]; + }, + PdfPart::FooterPart->value => static function (DataPart $value): array { + return ['files' => $value]; + }, + ]; + } /** * The Gotenberg API endpoint path. @@ -45,6 +66,87 @@ public function generate(): PdfResponse return $this->gotenbergClient->call($this->getEndpoint(), $this->getMultipartFormData()); } + /** + * Compiles the form values into a multipart form data array to send to the HTTP client. + * + * @return array> + */ + public function getMultipartFormData(): array + { + $multipartFormData = []; + + foreach ($this->formFields as $key => $value) { + foreach ($this->addToMultipart($key, $value) as $multiPart) { + $multipartFormData[] = $multiPart; + } + } + + return $multipartFormData; + } + + protected function addNormalizer(string $key, \Closure $normalizer): void + { + $this->normalizers[$key] = $normalizer; + } + + /** + * @param array|string|int|float|bool|DataPart $value + * + * @return list> + */ + private function addToMultipart(string $key, array|string|int|float|bool|DataPart $value): array + { + $multipartFormData = []; + + if (\array_key_exists($key, $this->normalizers)) { + $result = []; + foreach (($this->normalizers[$key])($value) as $key => $value) { + if ('extraHttpHeaders' !== $key) { + $result[] = $this->addToMultipart($key, $value); + + return array_merge(...$result); + } + + $multipartFormData[] = [ + $key => $value, + ]; + } + + return $multipartFormData; + } + + if (\is_bool($value)) { + $multipartFormData[] = [ + $key => $value ? 'true' : 'false', + ]; + + return $multipartFormData; + } + + if (\is_int($value) || \is_float($value)) { + $multipartFormData[] = [ + $key => (string) $value, + ]; + + return $multipartFormData; + } + + if (\is_array($value)) { + $result = []; + foreach ($value as $nestedValue) { + $result[] = $this->addToMultipart($key, $nestedValue); + } + + return array_merge(...$result); + } + + $multipartFormData[] = [ + $key => $value, + ]; + + return $multipartFormData; + } + /** * @param string[] $validExtensions */ diff --git a/src/Builder/LibreOfficePdfBuilder.php b/src/Builder/LibreOfficePdfBuilder.php index 463582e1..7bb5e462 100644 --- a/src/Builder/LibreOfficePdfBuilder.php +++ b/src/Builder/LibreOfficePdfBuilder.php @@ -110,33 +110,7 @@ public function getMultipartFormData(): array throw new MissingRequiredFieldException('At least one office file is required'); } - $formFields = $this->formFields; - $multipartFormData = []; - - $files = $this->formFields['files'] ?? []; - if ([] !== $files) { - foreach ($files as $dataPart) { - $multipartFormData[] = [ - 'files' => $dataPart, - ]; - } - unset($formFields['files']); - } - - foreach ($formFields as $key => $value) { - if (\is_bool($value)) { - $multipartFormData[] = [ - $key => $value ? 'true' : 'false', - ]; - continue; - } - - $multipartFormData[] = [ - $key => $value, - ]; - } - - return $multipartFormData; + return parent::getMultipartFormData(); } protected function getEndpoint(): string diff --git a/tests/Builder/HtmlPdfBuilderTest.php b/tests/Builder/HtmlPdfBuilderTest.php index c9054495..af3270be 100644 --- a/tests/Builder/HtmlPdfBuilderTest.php +++ b/tests/Builder/HtmlPdfBuilderTest.php @@ -30,32 +30,33 @@ public function testWithConfigurations(): void self::assertCount(21, $multipartFormData); - self::assertSame(['extraHttpHeaders' => '{"MyHeader":"Value","User-Agent":"MyValue"}'], $multipartFormData[0]); - self::assertSame(['paperWidth' => 33.1], $multipartFormData[2]); - self::assertSame(['paperHeight' => 46.8], $multipartFormData[3]); - self::assertSame(['marginTop' => 1.0], $multipartFormData[4]); - self::assertSame(['marginBottom' => 1.0], $multipartFormData[5]); - self::assertSame(['marginLeft' => 1.0], $multipartFormData[6]); - self::assertSame(['marginRight' => 1.0], $multipartFormData[7]); - self::assertSame(['preferCssPageSize' => 'true'], $multipartFormData[8]); - self::assertSame(['printBackground' => 'true'], $multipartFormData[9]); - self::assertSame(['omitBackground' => 'true'], $multipartFormData[10]); - self::assertSame(['landscape' => 'true'], $multipartFormData[11]); - self::assertSame(['scale' => 1.5], $multipartFormData[12]); - self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[13]); - self::assertSame(['waitDelay' => '10s'], $multipartFormData[14]); - self::assertSame(['waitForExpression' => 'window.globalVar === "ready"'], $multipartFormData[15]); - self::assertSame(['emulatedMediaType' => 'screen'], $multipartFormData[16]); - self::assertSame(['userAgent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML => like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'], $multipartFormData[17]); + self::assertIsArray($multipartFormData[0]); + self::assertCount(1, $multipartFormData[0]); + self::assertArrayHasKey('files', $multipartFormData[0]); + self::assertInstanceOf(DataPart::class, $multipartFormData[0]['files']); + self::assertSame('index.html', $multipartFormData[0]['files']->getFilename()); + + self::assertSame(['paperWidth' => '33.1'], $multipartFormData[1]); + self::assertSame(['paperHeight' => '46.8'], $multipartFormData[2]); + self::assertSame(['marginTop' => '1'], $multipartFormData[3]); + self::assertSame(['marginBottom' => '1'], $multipartFormData[4]); + self::assertSame(['marginLeft' => '1'], $multipartFormData[5]); + self::assertSame(['marginRight' => '1'], $multipartFormData[6]); + self::assertSame(['preferCssPageSize' => 'true'], $multipartFormData[7]); + self::assertSame(['printBackground' => 'true'], $multipartFormData[8]); + self::assertSame(['omitBackground' => 'true'], $multipartFormData[9]); + self::assertSame(['landscape' => 'true'], $multipartFormData[10]); + self::assertSame(['scale' => '1.5'], $multipartFormData[11]); + self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[12]); + self::assertSame(['waitDelay' => '10s'], $multipartFormData[13]); + self::assertSame(['waitForExpression' => 'window.globalVar === "ready"'], $multipartFormData[14]); + self::assertSame(['emulatedMediaType' => 'screen'], $multipartFormData[15]); + self::assertSame(['userAgent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML => like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'], $multipartFormData[16]); + self::assertSame(['extraHttpHeaders' => '{"MyHeader":"Value","User-Agent":"MyValue"}'], $multipartFormData[17]); self::assertSame(['failOnConsoleExceptions' => 'true'], $multipartFormData[18]); self::assertSame(['pdfa' => 'PDF/A-1a'], $multipartFormData[19]); self::assertSame(['pdfua' => 'true'], $multipartFormData[20]); - self::assertIsArray($multipartFormData[1]); - self::assertCount(1, $multipartFormData[1]); - self::assertArrayHasKey('files', $multipartFormData[1]); - self::assertInstanceOf(DataPart::class, $multipartFormData[1]['files']); - self::assertSame('index.html', $multipartFormData[1]['files']->getFilename()); } public function testWithTemplate(): void diff --git a/tests/Pdf/GotenbergTest.php b/tests/Pdf/GotenbergTest.php index 84b8dc71..03b17ec2 100644 --- a/tests/Pdf/GotenbergTest.php +++ b/tests/Pdf/GotenbergTest.php @@ -58,10 +58,10 @@ public function testHtmlBuilderFactory(): void self::assertCount(3, $multipartFormData); self::assertArrayHasKey(0, $multipartFormData); - self::assertSame(['marginTop' => 3.0], $multipartFormData[0]); + self::assertSame(['marginTop' => '3'], $multipartFormData[0]); self::assertArrayHasKey(1, $multipartFormData); - self::assertSame(['marginBottom' => 1.0], $multipartFormData[1]); + self::assertSame(['marginBottom' => '1'], $multipartFormData[1]); self::assertArrayHasKey(2, $multipartFormData); self::assertIsArray($multipartFormData[2]); @@ -128,12 +128,13 @@ public function testOfficeBuilderFactory(): void self::assertCount(2, $multipartFormData); self::assertArrayHasKey(0, $multipartFormData); - self::assertIsArray($multipartFormData[0]); - self::assertArrayHasKey('files', $multipartFormData[0]); - self::assertInstanceOf(DataPart::class, $multipartFormData[0]['files']); - self::assertSame('document.odt', $multipartFormData[0]['files']->getFilename()); + self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[0]); self::assertArrayHasKey(1, $multipartFormData); - self::assertSame(['nativePageRanges' => '1-5'], $multipartFormData[1]); + self::assertIsArray($multipartFormData[1]); + self::assertArrayHasKey('files', $multipartFormData[1]); + self::assertInstanceOf(DataPart::class, $multipartFormData[1]['files']); + self::assertSame('document.odt', $multipartFormData[1]['files']->getFilename()); + } } From ae5f34cfcb64260bce540ebe549e9aa6117d8179 Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Mon, 1 Apr 2024 14:30:39 +0200 Subject: [PATCH 4/6] Fix php-cs-fixer --- src/Builder/AbstractChromiumPdfBuilder.php | 6 +++--- src/Builder/AbstractPdfBuilder.php | 2 +- src/Builder/HtmlPdfBuilder.php | 2 +- src/Builder/MarkdownPdfBuilder.php | 2 +- src/Pdf/GotenbergInterface.php | 3 +++ tests/Builder/HtmlPdfBuilderTest.php | 1 - tests/Kernel.php | 6 +++--- tests/Pdf/GotenbergTest.php | 20 +++++++------------- 8 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/Builder/AbstractChromiumPdfBuilder.php b/src/Builder/AbstractChromiumPdfBuilder.php index bdd5cbb5..7e862597 100644 --- a/src/Builder/AbstractChromiumPdfBuilder.php +++ b/src/Builder/AbstractChromiumPdfBuilder.php @@ -193,7 +193,7 @@ public function nativePageRanges(string $range): static } /** - * @param string $template #Template + * @param string $template #Template * @param array $context * * @throws PdfPartRenderingException if the template could not be rendered @@ -204,7 +204,7 @@ public function header(string $template, array $context = []): static } /** - * @param string $template #Template + * @param string $template #Template * @param array $context * * @throws PdfPartRenderingException if the template could not be rendered @@ -397,7 +397,7 @@ protected function withPdfPartFile(PdfPart $pdfPart, string $path): static } /** - * @param string $template #Template + * @param string $template #Template * @param array $context * * @throws PdfPartRenderingException if the template could not be rendered diff --git a/src/Builder/AbstractPdfBuilder.php b/src/Builder/AbstractPdfBuilder.php index 75d6b6e4..b173e996 100644 --- a/src/Builder/AbstractPdfBuilder.php +++ b/src/Builder/AbstractPdfBuilder.php @@ -84,7 +84,7 @@ public function generate(): PdfResponse if (null !== $this->fileName) { $disposition = HeaderUtils::makeDisposition( $this->headerDisposition, - $this->fileName + $this->fileName, ); $pdfResponse diff --git a/src/Builder/HtmlPdfBuilder.php b/src/Builder/HtmlPdfBuilder.php index cd669424..455da13a 100644 --- a/src/Builder/HtmlPdfBuilder.php +++ b/src/Builder/HtmlPdfBuilder.php @@ -11,7 +11,7 @@ final class HtmlPdfBuilder extends AbstractChromiumPdfBuilder private const ENDPOINT = '/forms/chromium/convert/html'; /** - * @param string $template #Template + * @param string $template #Template * @param array $context * * @throws PdfPartRenderingException if the template could not be rendered diff --git a/src/Builder/MarkdownPdfBuilder.php b/src/Builder/MarkdownPdfBuilder.php index 09276130..719549ef 100644 --- a/src/Builder/MarkdownPdfBuilder.php +++ b/src/Builder/MarkdownPdfBuilder.php @@ -15,7 +15,7 @@ final class MarkdownPdfBuilder extends AbstractChromiumPdfBuilder /** * The HTML file that wraps the markdown content, rendered from a Twig template. * - * @param string $template #Template + * @param string $template #Template * @param array $context * * @throws PdfPartRenderingException if the template could not be rendered diff --git a/src/Pdf/GotenbergInterface.php b/src/Pdf/GotenbergInterface.php index f7035a37..2396ad1a 100644 --- a/src/Pdf/GotenbergInterface.php +++ b/src/Pdf/GotenbergInterface.php @@ -20,7 +20,10 @@ interface GotenbergInterface public function get(string $builder): PdfBuilderInterface; public function html(): HtmlPdfBuilder; + public function url(): UrlPdfBuilder; + public function office(): LibreOfficePdfBuilder; + public function markdown(): MarkdownPdfBuilder; } diff --git a/tests/Builder/HtmlPdfBuilderTest.php b/tests/Builder/HtmlPdfBuilderTest.php index af3270be..53b09157 100644 --- a/tests/Builder/HtmlPdfBuilderTest.php +++ b/tests/Builder/HtmlPdfBuilderTest.php @@ -56,7 +56,6 @@ public function testWithConfigurations(): void self::assertSame(['failOnConsoleExceptions' => 'true'], $multipartFormData[18]); self::assertSame(['pdfa' => 'PDF/A-1a'], $multipartFormData[19]); self::assertSame(['pdfua' => 'true'], $multipartFormData[20]); - } public function testWithTemplate(): void diff --git a/tests/Kernel.php b/tests/Kernel.php index e72630cf..f06e96a1 100644 --- a/tests/Kernel.php +++ b/tests/Kernel.php @@ -20,12 +20,12 @@ final class Kernel extends BaseKernel implements CompilerPassInterface public function getCacheDir(): string { - return __DIR__ . '/../var/cache'; + return __DIR__.'/../var/cache'; } public function getLogDir(): string { - return __DIR__ . '/../var/log'; + return __DIR__.'/../var/log'; } private function configureContainer(ContainerConfigurator $container, LoaderInterface $loader, ContainerBuilder $builder): void @@ -34,7 +34,7 @@ private function configureContainer(ContainerConfigurator $container, LoaderInte 'test' => true, ]); $builder->addCompilerPass($this); - } + } public function registerBundles(): iterable { diff --git a/tests/Pdf/GotenbergTest.php b/tests/Pdf/GotenbergTest.php index de49002a..026ed0e9 100644 --- a/tests/Pdf/GotenbergTest.php +++ b/tests/Pdf/GotenbergTest.php @@ -4,17 +4,12 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; -use PHPUnit\Framework\TestCase; -use Sensiolabs\GotenbergBundle\Builder\UrlPdfBuilder; -use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Sensiolabs\GotenbergBundle\Pdf\Gotenberg; use Sensiolabs\GotenbergBundle\Pdf\GotenbergInterface; -use Sensiolabs\GotenbergBundle\Tests\Kernel; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Mime\Part\DataPart; -use Twig\Environment; #[CoversClass(Gotenberg::class)] #[UsesClass(AssetBaseDirFormatter::class)] @@ -32,7 +27,7 @@ public function testUrlBuilderFactory(): void $builder = $gotenberg->url(); $builder ->setConfigurations([ - 'native_page_ranges' => '1-5' + 'native_page_ranges' => '1-5', ]) ->url('https://google.com') ; @@ -51,10 +46,10 @@ public function testHtmlBuilderFactory(): void $builder = $gotenberg->html() ->setConfigurations([ 'margin_top' => 3, - 'margin_bottom' => 1] + 'margin_bottom' => 1], ) ; - $builder->contentFile(__DIR__ . '/../Fixtures/files/content.html'); + $builder->contentFile(__DIR__.'/../Fixtures/files/content.html'); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(3, $multipartFormData); @@ -83,8 +78,8 @@ public function testMarkdownBuilderFactory(): void $gotenberg = $container->get(GotenbergInterface::class); $builder = $gotenberg->markdown(); - $builder->files(__DIR__ . '/../Fixtures/assets/file.md'); - $builder->wrapperFile(__DIR__ . '/../Fixtures/files/wrapper.html'); + $builder->files(__DIR__.'/../Fixtures/assets/file.md'); + $builder->wrapperFile(__DIR__.'/../Fixtures/files/wrapper.html'); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(2, $multipartFormData); @@ -112,10 +107,10 @@ public function testOfficeBuilderFactory(): void $gotenberg = $container->get(GotenbergInterface::class); $builder = $gotenberg->office() ->setConfigurations([ - 'native_page_ranges' => '1-5' + 'native_page_ranges' => '1-5', ]) ; - $builder->files(__DIR__ . '/../Fixtures/assets/office/document.odt'); + $builder->files(__DIR__.'/../Fixtures/assets/office/document.odt'); $multipartFormData = $builder->getMultipartFormData(); self::assertCount(2, $multipartFormData); @@ -128,6 +123,5 @@ public function testOfficeBuilderFactory(): void self::assertArrayHasKey('files', $multipartFormData[1]); self::assertInstanceOf(DataPart::class, $multipartFormData[1]['files']); self::assertSame('document.odt', $multipartFormData[1]['files']->getFilename()); - } } From 2465447f388fe4ae45e3980c2d7edb868bb4432d Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Mon, 8 Apr 2024 17:24:23 +0200 Subject: [PATCH 5/6] Add logic and . to normalized properties --- src/Builder/AbstractChromiumPdfBuilder.php | 19 +++---- src/Builder/AbstractPdfBuilder.php | 60 ++++++++++------------ src/Builder/HtmlPdfBuilder.php | 2 +- src/Builder/MarkdownPdfBuilder.php | 2 +- 4 files changed, 35 insertions(+), 48 deletions(-) diff --git a/src/Builder/AbstractChromiumPdfBuilder.php b/src/Builder/AbstractChromiumPdfBuilder.php index 7e862597..65bec83f 100644 --- a/src/Builder/AbstractChromiumPdfBuilder.php +++ b/src/Builder/AbstractChromiumPdfBuilder.php @@ -235,7 +235,7 @@ public function footerFile(string $path): static */ public function assets(string ...$paths): static { - $this->formFields['assets'] = []; + $this->formFields['.assets'] = []; foreach ($paths as $path) { $this->addAsset($path); @@ -253,7 +253,7 @@ public function addAsset(string $path): static $dataPart = new DataPart(new DataPartFile($resolvedPath)); - $this->formFields['assets'][$resolvedPath] = $dataPart; + $this->formFields['.assets'][$resolvedPath] = $dataPart; return $this; } @@ -320,7 +320,7 @@ public function userAgent(string $userAgent): static */ public function extraHttpHeaders(array $headers): static { - $this->formFields['extraHttpHeaders'] = $headers; + $this->formFields['.extraHttpHeaders'] = $headers; return $this; } @@ -335,14 +335,7 @@ public function extraHttpHeaders(array $headers): static */ public function addExtraHttpHeaders(array $headers): static { - if (\array_key_exists('extraHttpHeaders', $this->formFields)) { - $this->formFields['extraHttpHeaders'] = [ - ...$this->formFields['extraHttpHeaders'], - ...$headers, - ]; - } else { - $this->extraHttpHeaders($headers); - } + $this->formFields['.extraHttpHeaders'] = array_merge($this->formFields['.extraHttpHeaders'] ?? [], $headers); return $this; } @@ -391,7 +384,7 @@ protected function withPdfPartFile(PdfPart $pdfPart, string $path): static $pdfPart->value, ); - $this->formFields[$pdfPart->value] = $dataPart; + $this->formFields['.'.$pdfPart->value] = $dataPart; return $this; } @@ -414,7 +407,7 @@ protected function withRenderedPart(PdfPart $pdfPart, string $template, array $c throw new PdfPartRenderingException(sprintf('Could not render template "%s" into PDF part "%s". %s', $template, $pdfPart->value, $error->getMessage()), previous: $error); } - $this->formFields[$pdfPart->value] = new DataPart($html, $pdfPart->value, 'text/html'); + $this->formFields['.'.$pdfPart->value] = new DataPart($html, $pdfPart->value, 'text/html'); return $this; } diff --git a/src/Builder/AbstractPdfBuilder.php b/src/Builder/AbstractPdfBuilder.php index b173e996..52c11e79 100644 --- a/src/Builder/AbstractPdfBuilder.php +++ b/src/Builder/AbstractPdfBuilder.php @@ -32,7 +32,7 @@ public function __construct( protected readonly AssetBaseDirFormatter $asset, ) { $this->normalizers = [ - 'extraHttpHeaders' => static function (mixed $value): array { + '.extraHttpHeaders' => static function (mixed $value): array { try { $extraHttpHeaders = json_encode($value, \JSON_THROW_ON_ERROR); } catch (\JsonException $exception) { @@ -41,16 +41,16 @@ public function __construct( return ['extraHttpHeaders' => $extraHttpHeaders]; }, - 'assets' => static function (array $value): array { + '.assets' => static function (array $value): array { return ['files' => $value]; }, - PdfPart::HeaderPart->value => static function (DataPart $value): array { + '.'.PdfPart::HeaderPart->value => static function (DataPart $value): array { return ['files' => $value]; }, - PdfPart::BodyPart->value => static function (DataPart $value): array { + '.'.PdfPart::BodyPart->value => static function (DataPart $value): array { return ['files' => $value]; }, - PdfPart::FooterPart->value => static function (DataPart $value): array { + '.'.PdfPart::FooterPart->value => static function (DataPart $value): array { return ['files' => $value]; }, ]; @@ -105,7 +105,13 @@ public function getMultipartFormData(): array $multipartFormData = []; foreach ($this->formFields as $key => $value) { - foreach ($this->addToMultipart($key, $value) as $multiPart) { + $preCallback = null; + + if (\array_key_exists($key, $this->normalizers)) { + $preCallback = $this->normalizers[$key](...); + } + + foreach ($this->addToMultipart($key, $value, $preCallback) as $multiPart) { $multipartFormData[] = $multiPart; } } @@ -115,6 +121,10 @@ public function getMultipartFormData(): array protected function addNormalizer(string $key, \Closure $normalizer): void { + if (!str_starts_with($key, '.')) { + throw new \LogicException('To avoid recursive issues, $key must start with ".".'); + } + $this->normalizers[$key] = $normalizer; } @@ -123,41 +133,27 @@ protected function addNormalizer(string $key, \Closure $normalizer): void * * @return list> */ - private function addToMultipart(string $key, array|string|int|float|bool|DataPart $value): array + private function addToMultipart(string $key, array|string|int|float|bool|DataPart $value, \Closure|null $preCallback = null): array { - $multipartFormData = []; - - if (\array_key_exists($key, $this->normalizers)) { + if (null !== $preCallback) { $result = []; - foreach (($this->normalizers[$key])($value) as $key => $value) { - if ('extraHttpHeaders' !== $key) { - $result[] = $this->addToMultipart($key, $value); + foreach ($preCallback($value) as $key => $value) { + $result[] = $this->addToMultipart($key, $value); - return array_merge(...$result); - } - - $multipartFormData[] = [ - $key => $value, - ]; + return array_merge(...$result); } - - return $multipartFormData; } if (\is_bool($value)) { - $multipartFormData[] = [ + return [[ $key => $value ? 'true' : 'false', - ]; - - return $multipartFormData; + ]]; } if (\is_int($value) || \is_float($value)) { - $multipartFormData[] = [ + return [[ $key => (string) $value, - ]; - - return $multipartFormData; + ]]; } if (\is_array($value)) { @@ -169,11 +165,9 @@ private function addToMultipart(string $key, array|string|int|float|bool|DataPar return array_merge(...$result); } - $multipartFormData[] = [ + return [[ $key => $value, - ]; - - return $multipartFormData; + ]]; } /** diff --git a/src/Builder/HtmlPdfBuilder.php b/src/Builder/HtmlPdfBuilder.php index 455da13a..470fef71 100644 --- a/src/Builder/HtmlPdfBuilder.php +++ b/src/Builder/HtmlPdfBuilder.php @@ -31,7 +31,7 @@ public function contentFile(string $path): self public function getMultipartFormData(): array { - if (!\array_key_exists(PdfPart::BodyPart->value, $this->formFields)) { + if (!\array_key_exists('.'.PdfPart::BodyPart->value, $this->formFields)) { throw new MissingRequiredFieldException('Content is required'); } diff --git a/src/Builder/MarkdownPdfBuilder.php b/src/Builder/MarkdownPdfBuilder.php index 719549ef..4f696240 100644 --- a/src/Builder/MarkdownPdfBuilder.php +++ b/src/Builder/MarkdownPdfBuilder.php @@ -50,7 +50,7 @@ public function files(string ...$paths): self public function getMultipartFormData(): array { - if (!\array_key_exists(PdfPart::BodyPart->value, $this->formFields)) { + if (!\array_key_exists('.'.PdfPart::BodyPart->value, $this->formFields)) { throw new MissingRequiredFieldException('HTML template is required'); } From 7d6cbf03edecc82c5c3d4d73bf7559e733731fb9 Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Mon, 8 Apr 2024 17:41:28 +0200 Subject: [PATCH 6/6] Remove dot prefix for normalized properties --- src/Builder/AbstractChromiumPdfBuilder.php | 12 ++++++------ src/Builder/AbstractPdfBuilder.php | 14 +++++--------- src/Builder/HtmlPdfBuilder.php | 2 +- src/Builder/MarkdownPdfBuilder.php | 2 +- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Builder/AbstractChromiumPdfBuilder.php b/src/Builder/AbstractChromiumPdfBuilder.php index 65bec83f..d15014e0 100644 --- a/src/Builder/AbstractChromiumPdfBuilder.php +++ b/src/Builder/AbstractChromiumPdfBuilder.php @@ -235,7 +235,7 @@ public function footerFile(string $path): static */ public function assets(string ...$paths): static { - $this->formFields['.assets'] = []; + $this->formFields['assets'] = []; foreach ($paths as $path) { $this->addAsset($path); @@ -253,7 +253,7 @@ public function addAsset(string $path): static $dataPart = new DataPart(new DataPartFile($resolvedPath)); - $this->formFields['.assets'][$resolvedPath] = $dataPart; + $this->formFields['assets'][$resolvedPath] = $dataPart; return $this; } @@ -320,7 +320,7 @@ public function userAgent(string $userAgent): static */ public function extraHttpHeaders(array $headers): static { - $this->formFields['.extraHttpHeaders'] = $headers; + $this->formFields['extraHttpHeaders'] = $headers; return $this; } @@ -335,7 +335,7 @@ public function extraHttpHeaders(array $headers): static */ public function addExtraHttpHeaders(array $headers): static { - $this->formFields['.extraHttpHeaders'] = array_merge($this->formFields['.extraHttpHeaders'] ?? [], $headers); + $this->formFields['extraHttpHeaders'] = array_merge($this->formFields['extraHttpHeaders'] ?? [], $headers); return $this; } @@ -384,7 +384,7 @@ protected function withPdfPartFile(PdfPart $pdfPart, string $path): static $pdfPart->value, ); - $this->formFields['.'.$pdfPart->value] = $dataPart; + $this->formFields[$pdfPart->value] = $dataPart; return $this; } @@ -407,7 +407,7 @@ protected function withRenderedPart(PdfPart $pdfPart, string $template, array $c throw new PdfPartRenderingException(sprintf('Could not render template "%s" into PDF part "%s". %s', $template, $pdfPart->value, $error->getMessage()), previous: $error); } - $this->formFields['.'.$pdfPart->value] = new DataPart($html, $pdfPart->value, 'text/html'); + $this->formFields[$pdfPart->value] = new DataPart($html, $pdfPart->value, 'text/html'); return $this; } diff --git a/src/Builder/AbstractPdfBuilder.php b/src/Builder/AbstractPdfBuilder.php index 52c11e79..cdb7ba38 100644 --- a/src/Builder/AbstractPdfBuilder.php +++ b/src/Builder/AbstractPdfBuilder.php @@ -32,7 +32,7 @@ public function __construct( protected readonly AssetBaseDirFormatter $asset, ) { $this->normalizers = [ - '.extraHttpHeaders' => static function (mixed $value): array { + 'extraHttpHeaders' => static function (mixed $value): array { try { $extraHttpHeaders = json_encode($value, \JSON_THROW_ON_ERROR); } catch (\JsonException $exception) { @@ -41,16 +41,16 @@ public function __construct( return ['extraHttpHeaders' => $extraHttpHeaders]; }, - '.assets' => static function (array $value): array { + 'assets' => static function (array $value): array { return ['files' => $value]; }, - '.'.PdfPart::HeaderPart->value => static function (DataPart $value): array { + PdfPart::HeaderPart->value => static function (DataPart $value): array { return ['files' => $value]; }, - '.'.PdfPart::BodyPart->value => static function (DataPart $value): array { + PdfPart::BodyPart->value => static function (DataPart $value): array { return ['files' => $value]; }, - '.'.PdfPart::FooterPart->value => static function (DataPart $value): array { + PdfPart::FooterPart->value => static function (DataPart $value): array { return ['files' => $value]; }, ]; @@ -121,10 +121,6 @@ public function getMultipartFormData(): array protected function addNormalizer(string $key, \Closure $normalizer): void { - if (!str_starts_with($key, '.')) { - throw new \LogicException('To avoid recursive issues, $key must start with ".".'); - } - $this->normalizers[$key] = $normalizer; } diff --git a/src/Builder/HtmlPdfBuilder.php b/src/Builder/HtmlPdfBuilder.php index 470fef71..455da13a 100644 --- a/src/Builder/HtmlPdfBuilder.php +++ b/src/Builder/HtmlPdfBuilder.php @@ -31,7 +31,7 @@ public function contentFile(string $path): self public function getMultipartFormData(): array { - if (!\array_key_exists('.'.PdfPart::BodyPart->value, $this->formFields)) { + if (!\array_key_exists(PdfPart::BodyPart->value, $this->formFields)) { throw new MissingRequiredFieldException('Content is required'); } diff --git a/src/Builder/MarkdownPdfBuilder.php b/src/Builder/MarkdownPdfBuilder.php index 4f696240..719549ef 100644 --- a/src/Builder/MarkdownPdfBuilder.php +++ b/src/Builder/MarkdownPdfBuilder.php @@ -50,7 +50,7 @@ public function files(string ...$paths): self public function getMultipartFormData(): array { - if (!\array_key_exists('.'.PdfPart::BodyPart->value, $this->formFields)) { + if (!\array_key_exists(PdfPart::BodyPart->value, $this->formFields)) { throw new MissingRequiredFieldException('HTML template is required'); }