From 6fce02c727fc63581d131eb59b2f5b11a91b218a Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Tue, 2 Jul 2024 17:25:49 +0200 Subject: [PATCH] [UPDATE] Introduce DefaultBuilderTrait (#86) * [UPDATE] Introduce DefaultBuilderTrait * [UPDATE] default value for normalizers * [UPDATE] Skip null values --- phpstan.neon | 5 + src/Builder/DefaultBuilderTrait.php | 218 ++++++++++++++++++ src/Builder/Pdf/AbstractPdfBuilder.php | 197 +--------------- src/Builder/Pdf/ConvertPdfBuilder.php | 2 +- src/Builder/Pdf/LibreOfficePdfBuilder.php | 2 +- src/Builder/Pdf/MergePdfBuilder.php | 2 +- .../Screenshot/AbstractScreenshotBuilder.php | 194 +--------------- .../Screenshot/UrlScreenshotBuilder.php | 2 +- tests/Builder/Pdf/AbstractPdfBuilderTest.php | 2 +- .../AbstractScreenshotBuilderTest.php | 2 +- 10 files changed, 245 insertions(+), 381 deletions(-) create mode 100644 src/Builder/DefaultBuilderTrait.php diff --git a/phpstan.neon b/phpstan.neon index 45f26542..af92d594 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,5 @@ parameters: + phpVersion: 80100 level: 8 paths: - 'config' @@ -37,3 +38,7 @@ parameters: message: "#^Method Sensiolabs\\\\GotenbergBundle\\\\Tests\\\\Kernel\\:\\:configureContainer\\(\\) is unused\\.$#" count: 1 path: tests/Kernel.php + - # Fixed when requiring 8.2 + message: "#^Parameter \\#1 \\$iterator of function iterator_to_array expects Traversable, iterable given\\.$#" + count: 1 + path: tests/Builder/AbstractBuilderTestCase.php diff --git a/src/Builder/DefaultBuilderTrait.php b/src/Builder/DefaultBuilderTrait.php new file mode 100644 index 00000000..a3f81d65 --- /dev/null +++ b/src/Builder/DefaultBuilderTrait.php @@ -0,0 +1,218 @@ + + */ + protected array $formFields = []; + + /** + * @var array|string|\Stringable|int|float|bool|\BackedEnum|DataPart>)> + */ + private array $normalizers = []; + + private string|null $fileName = null; + + private string $headerDisposition = HeaderUtils::DISPOSITION_INLINE; + + protected LoggerInterface|null $logger = null; + + public function setLogger(LoggerInterface|null $logger): void + { + $this->logger = $logger; + } + + /** + * @param (\Closure(mixed): array|string|\Stringable|int|float|bool|\BackedEnum|DataPart>) $normalizer + */ + protected function addNormalizer(string $key, \Closure $normalizer): void + { + $this->normalizers[$key] = $normalizer; + } + + /** + * @return array + */ + private function encodeData(string $key, mixed $value): array + { + try { + $encodedValue = json_encode($value, \JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new JsonEncodingException(sprintf('Could not encode property "%s" into JSON', $key), previous: $exception); + } + + return [$key => $encodedValue]; + } + + /** + * @param HeaderUtils::DISPOSITION_* $headerDisposition + */ + public function fileName(string $fileName, string $headerDisposition = HeaderUtils::DISPOSITION_INLINE): static + { + $this->fileName = $fileName; + $this->headerDisposition = $headerDisposition; + + return $this; + } + + /** + * The Gotenberg API endpoint path. + */ + abstract protected function getEndpoint(): string; + + /** + * @param array $configurations + */ + abstract public function setConfigurations(array $configurations): static; + + /** + * @param non-empty-list $validExtensions + */ + protected function assertFileExtension(string $path, array $validExtensions): void + { + $file = new File($this->asset->resolve($path)); + $extension = $file->getExtension(); + + if (!\in_array($extension, $validExtensions, true)) { + throw new \InvalidArgumentException(sprintf('The file extension "%s" is not valid in this context.', $extension)); + } + } + + /** + * 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) { + if (null === $value) { + $this->logger?->debug('Key {sensiolabs_gotenberg.key} is null, skipping.', [ + 'sensiolabs_gotenberg.key' => $key, + ]); + + continue; + } + + $preCallback = null; + + if (\array_key_exists($key, $this->normalizers)) { + $this->logger?->debug('Normalizer found for key {sensiolabs_gotenberg.key}.', [ + 'sensiolabs_gotenberg.key' => $key, + ]); + $preCallback = $this->normalizers[$key](...); + } + + foreach ($this->convertToMultipartItems($key, $value, $preCallback) as $multiPart) { + $multipartFormData[] = $multiPart; + } + } + + return $multipartFormData; + } + + /** + * @param array|string|\Stringable|int|float|bool|\BackedEnum|DataPart $value + * + * @return list> + */ + private function convertToMultipartItems(string $key, array|string|\Stringable|int|float|bool|\BackedEnum|DataPart $value, \Closure|null $preCallback = null): array + { + if (null !== $preCallback) { + $result = []; + + foreach ($preCallback($value) as $innerKey => $innerValue) { + $result[] = $this->convertToMultipartItems($innerKey, $innerValue); + } + + return array_merge(...$result); + } + + if (\is_bool($value)) { + return [[ + $key => $value ? 'true' : 'false', + ]]; + } + + if (\is_int($value)) { + return [[ + $key => (string) $value, + ]]; + } + + if (\is_float($value)) { + [$left, $right] = sscanf((string) $value, '%d.%s') ?? [$value, '']; + + $right ??= '0'; + + return [[ + $key => "{$left}.{$right}", + ]]; + } + + if ($value instanceof \BackedEnum) { + return [[ + $key => (string) $value->value, + ]]; + } + + if ($value instanceof \Stringable) { + return [[ + $key => (string) $value, + ]]; + } + + if (\is_array($value)) { + $result = []; + foreach ($value as $nestedValue) { + $result[] = $this->convertToMultipartItems($key, $nestedValue); + } + + return array_merge(...$result); + } + + return [[ + $key => $value, + ]]; + } + + private function doCall(): GotenbergResponse + { + $this->logger?->debug('Generating file using {sensiolabs_gotenberg.builder} builder.', [ + 'sensiolabs_gotenberg.builder' => $this::class, + ]); + + $response = $this->client->call($this->getEndpoint(), $this->getMultipartFormData()); + + if (null !== $this->fileName) { + $disposition = HeaderUtils::makeDisposition( + $this->headerDisposition, + $this->fileName, + ); + + $response + ->headers->set('Content-Disposition', $disposition) + ; + } + + return $response; + } +} diff --git a/src/Builder/Pdf/AbstractPdfBuilder.php b/src/Builder/Pdf/AbstractPdfBuilder.php index 899385b5..3f9f7072 100644 --- a/src/Builder/Pdf/AbstractPdfBuilder.php +++ b/src/Builder/Pdf/AbstractPdfBuilder.php @@ -2,38 +2,24 @@ namespace Sensiolabs\GotenbergBundle\Builder\Pdf; -use Psr\Log\LoggerInterface; +use Sensiolabs\GotenbergBundle\Builder\DefaultBuilderTrait; use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Client\GotenbergResponse; use Sensiolabs\GotenbergBundle\Enumeration\Part; -use Sensiolabs\GotenbergBundle\Exception\JsonEncodingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; -use Symfony\Component\HttpFoundation\File\File; -use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\Mime\Part\DataPart; abstract class AbstractPdfBuilder implements PdfBuilderInterface { - protected LoggerInterface|null $logger = null; - - /** - * @var array - */ - protected array $formFields = []; - - private string|null $fileName = null; - - private string $headerDisposition = HeaderUtils::DISPOSITION_INLINE; - - /** - * @var array|non-empty-string|\Stringable|int|float|bool|\BackedEnum|DataPart>)> - */ - private array $normalizers; + use DefaultBuilderTrait; public function __construct( - protected readonly GotenbergClientInterface $gotenbergClient, - protected readonly AssetBaseDirFormatter $asset, + GotenbergClientInterface $gotenbergClient, + AssetBaseDirFormatter $asset, ) { + $this->client = $gotenbergClient; + $this->asset = $asset; + $this->normalizers = [ 'extraHttpHeaders' => function (mixed $value): array { return $this->encodeData('extraHttpHeaders', $value); @@ -62,175 +48,8 @@ public function __construct( ]; } - public function setLogger(LoggerInterface|null $logger): void - { - $this->logger = $logger; - } - - /** - * @return array - */ - private function encodeData(string $key, mixed $value): array - { - try { - $encodedValue = json_encode($value, \JSON_THROW_ON_ERROR); - } catch (\JsonException $exception) { - throw new JsonEncodingException(sprintf('Could not encode property "%s" into JSON', $key), previous: $exception); - } - - return [$key => $encodedValue]; - } - - /** - * The GotenbergPdf API endpoint path. - */ - abstract protected function getEndpoint(): string; - - /** - * @param array $configurations - */ - abstract public function setConfigurations(array $configurations): self; - - /** - * @param HeaderUtils::DISPOSITION_* $headerDisposition - */ - public function fileName(string $fileName, string $headerDisposition = HeaderUtils::DISPOSITION_INLINE): static - { - $this->fileName = $fileName; - $this->headerDisposition = $headerDisposition; - - return $this; - } - public function generate(): GotenbergResponse { - $this->logger?->debug('Generating PDF file using {sensiolabs_gotenberg.builder} builder.', [ - 'sensiolabs_gotenberg.builder' => $this::class, - ]); - - $pdfResponse = $this->gotenbergClient->call($this->getEndpoint(), $this->getMultipartFormData()); - - if (null !== $this->fileName) { - $disposition = HeaderUtils::makeDisposition( - $this->headerDisposition, - $this->fileName, - ); - - $pdfResponse - ->headers->set('Content-Disposition', $disposition) - ; - } - - return $pdfResponse; - } - - /** - * 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) { - $preCallback = null; - - if (\array_key_exists($key, $this->normalizers)) { - $this->logger?->debug('Normalizer found for key {sensiolabs_gotenberg.key}.', [ - 'sensiolabs_gotenberg.key' => $key, - ]); - $preCallback = $this->normalizers[$key](...); - } - - foreach ($this->addToMultipart($key, $value, $preCallback) as $multiPart) { - $multipartFormData[] = $multiPart; - } - } - - return $multipartFormData; - } - - protected function addNormalizer(string $key, \Closure $normalizer): void - { - $this->normalizers[$key] = $normalizer; - } - - /** - * @param array|string|\Stringable|int|float|bool|\BackedEnum|DataPart $value - * - * @return list> - */ - private function addToMultipart(string $key, array|string|\Stringable|int|float|bool|\BackedEnum|DataPart $value, \Closure|null $preCallback = null): array - { - if (null !== $preCallback) { - $result = []; - - foreach ($preCallback($value) as $innerKey => $innerValue) { - $result[] = $this->addToMultipart($innerKey, $innerValue); - } - - return array_merge(...$result); - } - - if (\is_bool($value)) { - return [[ - $key => $value ? 'true' : 'false', - ]]; - } - - if (\is_int($value)) { - return [[ - $key => (string) $value, - ]]; - } - - if (\is_float($value)) { - [$left, $right] = sscanf((string) $value, '%d.%s') ?? [$value, '']; - - $right ??= '0'; - - return [[ - $key => "{$left}.{$right}", - ]]; - } - - if ($value instanceof \BackedEnum) { - return [[ - $key => (string) $value->value, - ]]; - } - - if ($value instanceof \Stringable) { - return [[ - $key => (string) $value, - ]]; - } - - if (\is_array($value)) { - $result = []; - foreach ($value as $nestedValue) { - $result[] = $this->addToMultipart($key, $nestedValue); - } - - return array_merge(...$result); - } - - return [[ - $key => $value, - ]]; - } - - /** - * @param non-empty-list $validExtensions - */ - protected function assertFileExtension(string $path, array $validExtensions): void - { - $file = new File($this->asset->resolve($path)); - $extension = $file->getExtension(); - - if (!\in_array($extension, $validExtensions, true)) { - throw new \InvalidArgumentException(sprintf('The file extension "%s" is not available in GotenbergPdf.', $extension)); - } + return $this->doCall(); } } diff --git a/src/Builder/Pdf/ConvertPdfBuilder.php b/src/Builder/Pdf/ConvertPdfBuilder.php index 883bae21..37f2c778 100644 --- a/src/Builder/Pdf/ConvertPdfBuilder.php +++ b/src/Builder/Pdf/ConvertPdfBuilder.php @@ -17,7 +17,7 @@ final class ConvertPdfBuilder extends AbstractPdfBuilder * * @param array $configurations */ - public function setConfigurations(array $configurations): self + public function setConfigurations(array $configurations): static { foreach ($configurations as $property => $value) { $this->addConfiguration($property, $value); diff --git a/src/Builder/Pdf/LibreOfficePdfBuilder.php b/src/Builder/Pdf/LibreOfficePdfBuilder.php index ec15a807..0d95fdc2 100644 --- a/src/Builder/Pdf/LibreOfficePdfBuilder.php +++ b/src/Builder/Pdf/LibreOfficePdfBuilder.php @@ -30,7 +30,7 @@ final class LibreOfficePdfBuilder extends AbstractPdfBuilder * * @param array $configurations */ - public function setConfigurations(array $configurations): self + public function setConfigurations(array $configurations): static { foreach ($configurations as $property => $value) { $this->addConfiguration($property, $value); diff --git a/src/Builder/Pdf/MergePdfBuilder.php b/src/Builder/Pdf/MergePdfBuilder.php index 070c93d4..df327930 100644 --- a/src/Builder/Pdf/MergePdfBuilder.php +++ b/src/Builder/Pdf/MergePdfBuilder.php @@ -17,7 +17,7 @@ final class MergePdfBuilder extends AbstractPdfBuilder * * @param array $configurations */ - public function setConfigurations(array $configurations): self + public function setConfigurations(array $configurations): static { foreach ($configurations as $property => $value) { $this->addConfiguration($property, $value); diff --git a/src/Builder/Screenshot/AbstractScreenshotBuilder.php b/src/Builder/Screenshot/AbstractScreenshotBuilder.php index 7b444970..82e9a4fb 100644 --- a/src/Builder/Screenshot/AbstractScreenshotBuilder.php +++ b/src/Builder/Screenshot/AbstractScreenshotBuilder.php @@ -2,38 +2,24 @@ namespace Sensiolabs\GotenbergBundle\Builder\Screenshot; -use Psr\Log\LoggerInterface; +use Sensiolabs\GotenbergBundle\Builder\DefaultBuilderTrait; use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; use Sensiolabs\GotenbergBundle\Client\GotenbergResponse; use Sensiolabs\GotenbergBundle\Enumeration\Part; -use Sensiolabs\GotenbergBundle\Exception\JsonEncodingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; -use Symfony\Component\HttpFoundation\File\File; -use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\Mime\Part\DataPart; abstract class AbstractScreenshotBuilder implements ScreenshotBuilderInterface { - protected LoggerInterface|null $logger = null; - - /** - * @var array - */ - protected array $formFields = []; - - private string|null $fileName = null; - - private string $headerDisposition = HeaderUtils::DISPOSITION_INLINE; - - /** - * @var array|non-empty-string|\Stringable|int|float|bool|\BackedEnum|DataPart>)> - */ - private array $normalizers; + use DefaultBuilderTrait; public function __construct( - protected readonly GotenbergClientInterface $gotenbergClient, - protected readonly AssetBaseDirFormatter $asset, + GotenbergClientInterface $gotenbergClient, + AssetBaseDirFormatter $asset, ) { + $this->client = $gotenbergClient; + $this->asset = $asset; + $this->normalizers = [ 'extraHttpHeaders' => function (mixed $value): array { return $this->encodeData('extraHttpHeaders', $value); @@ -53,172 +39,8 @@ public function __construct( ]; } - public function setLogger(LoggerInterface|null $logger): void - { - $this->logger = $logger; - } - - /** - * @return array - */ - private function encodeData(string $key, mixed $value): array - { - try { - $encodedValue = json_encode($value, \JSON_THROW_ON_ERROR); - } catch (\JsonException $exception) { - throw new JsonEncodingException(sprintf('Could not encode property "%s" into JSON', $key), previous: $exception); - } - - return [$key => $encodedValue]; - } - - /** - * The GotenbergPdf API endpoint path. - */ - abstract protected function getEndpoint(): string; - - /** - * @param array $configurations - */ - abstract public function setConfigurations(array $configurations): self; - - /** - * @param HeaderUtils::DISPOSITION_* $headerDisposition - */ - public function fileName(string $fileName, string $headerDisposition = HeaderUtils::DISPOSITION_INLINE): static - { - $this->fileName = $fileName; - $this->headerDisposition = $headerDisposition; - - return $this; - } - public function generate(): GotenbergResponse { - $this->logger?->debug('Generating Screenshot file using {sensiolabs_gotenberg.builder} builder.', [ - 'sensiolabs_gotenberg.builder' => $this::class, - ]); - - $pdfResponse = $this->gotenbergClient->call($this->getEndpoint(), $this->getMultipartFormData()); - - if (null !== $this->fileName) { - $disposition = HeaderUtils::makeDisposition( - $this->headerDisposition, - $this->fileName, - ); - - $pdfResponse - ->headers->set('Content-Disposition', $disposition) - ; - } - - return $pdfResponse; - } - - /** - * 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) { - $preCallback = null; - - if (\array_key_exists($key, $this->normalizers)) { - $preCallback = $this->normalizers[$key](...); - } - - foreach ($this->addToMultipart($key, $value, $preCallback) as $multiPart) { - $multipartFormData[] = $multiPart; - } - } - - return $multipartFormData; - } - - protected function addNormalizer(string $key, \Closure $normalizer): void - { - $this->normalizers[$key] = $normalizer; - } - - /** - * @param array|string|\Stringable|int|float|bool|\BackedEnum|DataPart $value - * - * @return list> - */ - private function addToMultipart(string $key, array|string|\Stringable|int|float|bool|\BackedEnum|DataPart $value, \Closure|null $preCallback = null): array - { - if (null !== $preCallback) { - $result = []; - - foreach ($preCallback($value) as $innerKey => $innerValue) { - $result[] = $this->addToMultipart($innerKey, $innerValue); - } - - return array_merge(...$result); - } - - if (\is_bool($value)) { - return [[ - $key => $value ? 'true' : 'false', - ]]; - } - - if (\is_int($value)) { - return [[ - $key => (string) $value, - ]]; - } - - if (\is_float($value)) { - [$left, $right] = sscanf((string) $value, '%d.%s') ?? [$value, '']; - - $right ??= '0'; - - return [[ - $key => "{$left}.{$right}", - ]]; - } - - if ($value instanceof \BackedEnum) { - return [[ - $key => (string) $value->value, - ]]; - } - - if ($value instanceof \Stringable) { - return [[ - $key => (string) $value, - ]]; - } - - if (\is_array($value)) { - $result = []; - foreach ($value as $nestedValue) { - $result[] = $this->addToMultipart($key, $nestedValue); - } - - return array_merge(...$result); - } - - return [[ - $key => $value, - ]]; - } - - /** - * @param non-empty-list $validExtensions - */ - protected function assertFileExtension(string $path, array $validExtensions): void - { - $file = new File($this->asset->resolve($path)); - $extension = $file->getExtension(); - - if (!\in_array($extension, $validExtensions, true)) { - throw new \InvalidArgumentException(sprintf('The file extension "%s" is not available in GotenbergPdf.', $extension)); - } + return $this->doCall(); } } diff --git a/src/Builder/Screenshot/UrlScreenshotBuilder.php b/src/Builder/Screenshot/UrlScreenshotBuilder.php index 197b3c44..38369559 100644 --- a/src/Builder/Screenshot/UrlScreenshotBuilder.php +++ b/src/Builder/Screenshot/UrlScreenshotBuilder.php @@ -34,7 +34,7 @@ public function setRequestContext(RequestContext|null $requestContext = null): s } /** - * URL of the page you want to convert into PDF. + * URL of the page you want to screenshot. */ public function url(string $url): self { diff --git a/tests/Builder/Pdf/AbstractPdfBuilderTest.php b/tests/Builder/Pdf/AbstractPdfBuilderTest.php index 518c038d..378686e1 100644 --- a/tests/Builder/Pdf/AbstractPdfBuilderTest.php +++ b/tests/Builder/Pdf/AbstractPdfBuilderTest.php @@ -145,7 +145,7 @@ public function __construct(GotenbergClientInterface $gotenbergClient, AssetBase $this->formFields = $formFields; } - public function setConfigurations(array $configurations): AbstractPdfBuilder + public function setConfigurations(array $configurations): static { return $this; } diff --git a/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php b/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php index f3825983..ea23f00c 100644 --- a/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php +++ b/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php @@ -128,7 +128,7 @@ public function __construct(GotenbergClientInterface $gotenbergClient, AssetBase $this->formFields = $formFields; } - public function setConfigurations(array $configurations): AbstractScreenshotBuilder + public function setConfigurations(array $configurations): static { return $this; }