diff --git a/Makefile b/Makefile index 43e1b76..5bc5388 100644 --- a/Makefile +++ b/Makefile @@ -7,3 +7,11 @@ build: .PHONY: test test: build $(MAKE) -C src/Bundle test IMAGE_TAG="${IMAGE_TAG}" ARGS="${ARGS}" + +.PHONY: phpstan +phpstan: + php vendor/bin/phpstan analyse --level max src/ + +.PHONY: php-cs-fixer +php-cs-fixer: + tools/php-cs-fixer/vendor/bin/php-cs-fixer fix ./src diff --git a/src/Backend/HeadlessChromium/ExtraOption/DisableFeatures.php b/src/Backend/HeadlessChromium/ExtraOption/DisableFeatures.php index 5ea5de0..7802a61 100644 --- a/src/Backend/HeadlessChromium/ExtraOption/DisableFeatures.php +++ b/src/Backend/HeadlessChromium/ExtraOption/DisableFeatures.php @@ -8,6 +8,9 @@ class DisableFeatures implements ExtraOption { + /** + * @param array $features + */ public function __construct(private readonly array $features) { } diff --git a/src/Backend/HeadlessChromium/ExtraOption/PrintToPdf.php b/src/Backend/HeadlessChromium/ExtraOption/PrintToPdf.php index 797fca4..a3a844c 100644 --- a/src/Backend/HeadlessChromium/ExtraOption/PrintToPdf.php +++ b/src/Backend/HeadlessChromium/ExtraOption/PrintToPdf.php @@ -5,11 +5,10 @@ namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; -use SplFileInfo; class PrintToPdf implements ExtraOption { - public function __construct(private readonly SplFileInfo $file) + public function __construct(private readonly string $filePath) { } @@ -20,11 +19,11 @@ public function isRepeatable(): bool public function compile(): array { - return ['--print-to-pdf=' . $this->file]; + return ['--print-to-pdf=' . $this->filePath]; } - public function getFile(): SplFileInfo + public function getFilePath(): string { - return $this->file; + return $this->filePath; } } diff --git a/src/Backend/HeadlessChromium/HeadlessChromiumAdapter.php b/src/Backend/HeadlessChromium/HeadlessChromiumAdapter.php index 7dd3c5a..4fcd65a 100644 --- a/src/Backend/HeadlessChromium/HeadlessChromiumAdapter.php +++ b/src/Backend/HeadlessChromium/HeadlessChromiumAdapter.php @@ -9,7 +9,6 @@ use KNPLabs\Snappy\Core\Backend\Options; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; -use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Message\UriInterface; use Symfony\Component\Process\Process; use InvalidArgumentException; @@ -22,17 +21,13 @@ final class HeadlessChromiumAdapter implements UriToPdf */ use Reconfigurable; - private string $tempDir; - public function __construct( private string $binary, private int $timeout, HeadlessChromiumFactory $factory, Options $options, private readonly StreamFactoryInterface $streamFactory, - private readonly UriFactoryInterface $uriFactory, ) { - $this->tempDir = __DIR__; self::validateOptions($options); $this->factory = $factory; @@ -65,7 +60,7 @@ public function getPrintToPdfFilePath(): string if (!empty($printToPdfOption)) { $printToPdfOption = \array_values($printToPdfOption)[0]; - return $printToPdfOption->getFile()->getPathname(); + return $printToPdfOption->getFilePath(); } throw new RuntimeException('Missing option print to pdf.'); @@ -89,18 +84,29 @@ private static function validateOptions(Options $options): void } /** - * @return array + * @return array */ private function compileOptions(): array { return \array_reduce( $this->options->extraOptions, - fn (array $carry, ExtraOption $extraOption) => $this->options->pageOrientation !== null - ?: [ - ...$carry, - ...$extraOption->compile(), - ], - [], + /** + * @param array $carry + * @param ExtraOption $extraOption + * + * @return array + */ + function (array $carry, $extraOption) { + if ($extraOption instanceof ExtraOption) { + return [ + ...$carry, + ...$extraOption->compile(), + ]; + } + + return $carry; + }, + [] ); } } diff --git a/src/Backend/HeadlessChromium/HeadlessChromiumFactory.php b/src/Backend/HeadlessChromium/HeadlessChromiumFactory.php index 1d5017f..75b7685 100644 --- a/src/Backend/HeadlessChromium/HeadlessChromiumFactory.php +++ b/src/Backend/HeadlessChromium/HeadlessChromiumFactory.php @@ -7,7 +7,6 @@ use KNPLabs\Snappy\Core\Backend\Factory; use KNPLabs\Snappy\Core\Backend\Options; use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UriFactoryInterface; /** * @implements Factory @@ -18,7 +17,6 @@ public function __construct( private readonly string $binary, private readonly int $timeout, private readonly StreamFactoryInterface $streamFactory, - private readonly UriFactoryInterface $uriFactory, ) { } @@ -30,7 +28,6 @@ public function create(Options $options): HeadlessChromiumAdapter $this, $options, $this->streamFactory, - $this->uriFactory, ); } } diff --git a/src/Backend/HeadlessChromium/Tests/HeadlessChromiumAdapterTest.php b/src/Backend/HeadlessChromium/Tests/HeadlessChromiumAdapterTest.php index 85af167..85bf471 100644 --- a/src/Backend/HeadlessChromium/Tests/HeadlessChromiumAdapterTest.php +++ b/src/Backend/HeadlessChromium/Tests/HeadlessChromiumAdapterTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; -use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Message\UriInterface; use SplFileInfo; use RuntimeException; @@ -30,22 +29,18 @@ final class HeadlessChromiumAdapterTest extends TestCase private string $directory; - private UriFactoryInterface $uriFactory; - private SplFileInfo $outputFile; protected function setUp(): void { - $this->uriFactory = $this->createMock(UriFactoryInterface::class); $this->directory = __DIR__; $this->outputFile = new SplFileInfo($this->directory . '/file.pdf'); - $this->options = new Options(null, [new Headless(), new PrintToPdf($this->outputFile), new DisableGpu()]); + $this->options = new Options(null, [new Headless(), new PrintToPdf($this->outputFile->getPathname()), new DisableGpu()]); $this->streamFactory = $this->createMock(StreamFactoryInterface::class); $this->factory = new HeadlessChromiumFactory( 'chromium', 120, $this->streamFactory, - $this->uriFactory ); $this->adapter = $this->factory->create($this->options); } @@ -55,12 +50,6 @@ public function testGenerateFromUri(): void $url = $this->createMock(UriInterface::class); $url->method('__toString')->willReturn('https://google.com'); - $this->streamFactory->expects($this->once()) - ->method('createStream') - ->with($this->stringContains($this->outputFile->getPathname())) - ->willReturn($this->createMock(StreamInterface::class)) - ; - $resultStream = $this->adapter->generateFromUri($url); $this->assertNotNull($resultStream); diff --git a/src/Framework/Symfony/DependencyInjection/Configuration/HeadlessChromiumConfigurationFactory.php b/src/Framework/Symfony/DependencyInjection/Configuration/HeadlessChromiumConfigurationFactory.php new file mode 100644 index 0000000..a0df894 --- /dev/null +++ b/src/Framework/Symfony/DependencyInjection/Configuration/HeadlessChromiumConfigurationFactory.php @@ -0,0 +1,111 @@ +setDefinition( + $factoryId, + new Definition( + HeadlessChromiumFactory::class, + [ + '$streamFactory' => $container->getDefinition(Psr17Factory::class), + '$binary' => $configuration['binary'], + '$timeout' => $configuration['timeout'], + ] + ) + ) + ; + + $container + ->setDefinition( + $backendId, + (new Definition(HeadlessChromiumAdapter::class)) + ->setFactory([$container->getDefinition($factoryId), 'create']) + ->setArgument('$options', $options) + ) + ; + + $container->registerAliasForArgument($backendId, HeadlessChromiumAdapter::class, $backendName); + } + + public function getExample(): array + { + return [ + 'extraOptions' => [ + 'construct' => [], + 'output' => [], + ], + ]; + } + + public function addConfiguration(ArrayNodeDefinition $node): void + { + $node + ->children() + ->scalarNode('binary') + ->defaultValue('chromium') + ->info('Path or command to run Chromium') + ; + + $node + ->children() + ->scalarNode('headless') + ->defaultValue('--headless') + ->info('The flag to run Chromium in headless mode') + ; + + $node + ->children() + ->integerNode('timeout') + ->defaultValue(60) + ->info('Timeout for Chromium process') + ; + + $optionsNode = $node + ->children() + ->arrayNode('options') + ->info('Options to configure the Chromium process.') + ->addDefaultsIfNotSet() + ->children() + ; + + $optionsNode + ->arrayNode('extraOptions') + ->info('Extra options passed to the HeadlessChromiumAdapter.') + ->children() + ->scalarNode('printToPdf') + ->info(\sprintf('Configuration passed to %s::__construct().', PrintToPdf::class)) + ; + } +} diff --git a/src/Framework/Symfony/DependencyInjection/SnappyExtension.php b/src/Framework/Symfony/DependencyInjection/SnappyExtension.php index cdd9f59..36037b2 100644 --- a/src/Framework/Symfony/DependencyInjection/SnappyExtension.php +++ b/src/Framework/Symfony/DependencyInjection/SnappyExtension.php @@ -4,10 +4,12 @@ namespace KNPLabs\Snappy\Framework\Symfony\DependencyInjection; +use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption\PrintToPdf; use KNPLabs\Snappy\Core\Backend\Options; use KNPLabs\Snappy\Core\Backend\Options\PageOrientation; use KNPLabs\Snappy\Framework\Symfony\DependencyInjection\Configuration\BackendConfigurationFactory; use KNPLabs\Snappy\Framework\Symfony\DependencyInjection\Configuration\DompdfConfigurationFactory; +use KNPLabs\Snappy\Framework\Symfony\DependencyInjection\Configuration\HeadlessChromiumConfigurationFactory; use KNPLabs\Snappy\Framework\Symfony\DependencyInjection\Configuration\WkHtmlToPdfConfigurationFactory; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -67,6 +69,7 @@ private function getFactories(): array [ new DompdfConfigurationFactory(), new WkHtmlToPdfConfigurationFactory(), + new HeadlessChromiumConfigurationFactory(), ], static fn (BackendConfigurationFactory $factory): bool => $factory->isAvailable(), ); @@ -99,19 +102,34 @@ private function buildOptions(string $backendName, string $backendType, array $c ]; if (isset($configuration['pageOrientation'])) { - if (false === \is_string($configuration['pageOrientation'])) { - throw new InvalidConfigurationException(\sprintf('Invalid “%s” type for “snappy.backends.%s.%s.options.pageOrientation”. The expected type is “string”.', $backendName, $backendType, \gettype($configuration['pageOrientation'])), ); + if (!\is_string($configuration['pageOrientation'])) { + throw new InvalidConfigurationException(\sprintf('Invalid type for “snappy.backends.%s.%s.options.pageOrientation”. Expected "string", got "%s".', $backendName, $backendType, \gettype($configuration['pageOrientation']))); } - $arguments['$pageOrientation'] = PageOrientation::from($configuration['pageOrientation']); } if (isset($configuration['extraOptions'])) { - if (false === \is_array($configuration['extraOptions'])) { - throw new InvalidConfigurationException(\sprintf('Invalid “%s” type for “snappy.backends.%s.%s.options.extraOptions”. The expected type is “array”.', $backendName, $backendType, \gettype($configuration['extraOptions'])), ); + if (!\is_array($configuration['extraOptions'])) { + throw new InvalidConfigurationException(\sprintf('Invalid type for “snappy.backends.%s.%s.options.extraOptions”. Expected "array", got "%s".', $backendName, $backendType, \gettype($configuration['extraOptions']))); } - $arguments['$extraOptions'] = $configuration['extraOptions']; + foreach ($configuration['extraOptions'] as $key => $value) { + switch ($key) { + case 'printToPdf': + if (\is_string($value)) { + $arguments['$extraOptions'][] = new PrintToPdf($value); + } else { + throw new InvalidConfigurationException(\sprintf('Invalid type for “snappy.backends.%s.%s.options.extraOptions.printToPdf”. Expected "string", got "%s".', $backendName, $backendType, \gettype($value))); + } + + break; + + default: + $arguments['$extraOptions'][$key] = $value; + + break; + } + } } return new Definition(Options::class, $arguments); diff --git a/src/Framework/Symfony/Tests/DependencyInjection/SnappyExtensionTest.php b/src/Framework/Symfony/Tests/DependencyInjection/SnappyExtensionTest.php index 9630740..be7ff6c 100644 --- a/src/Framework/Symfony/Tests/DependencyInjection/SnappyExtensionTest.php +++ b/src/Framework/Symfony/Tests/DependencyInjection/SnappyExtensionTest.php @@ -6,6 +6,9 @@ use KNPLabs\Snappy\Backend\Dompdf\DompdfAdapter; use KNPLabs\Snappy\Backend\Dompdf\DompdfFactory; +use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; +use KNPLabs\Snappy\Backend\HeadlessChromium\HeadlessChromiumAdapter; +use KNPLabs\Snappy\Backend\HeadlessChromium\HeadlessChromiumFactory; use KNPLabs\Snappy\Core\Backend\Options; use KNPLabs\Snappy\Core\Backend\Options\PageOrientation; use KNPLabs\Snappy\Framework\Symfony\DependencyInjection\SnappyExtension; @@ -14,6 +17,7 @@ use Psr\Http\Message\StreamFactoryInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use SplFileInfo; final class SnappyExtensionTest extends TestCase { @@ -121,4 +125,68 @@ public function testDompdfBackendConfiguration(): void ), ); } + + public function testHeadlessChromiumBackendConfiguration(): void + { + $directory = __DIR__; + $outputFile = new SplFileInfo($directory . '/file.pdf'); + + $configuration = [ + 'snappy' => [ + 'backends' => [ + 'myBackend' => [ + 'chromium' => [ + 'binary' => 'chromium', + 'timeout' => 60, + 'options' => [ + 'extraOptions' => [ + 'printToPdf' => $outputFile->getPathname(), + ], + ], + ], + ], + ], + ], + ]; + + $this->extension->load($configuration, $this->container); + + $this->assertEquals( + \array_keys($this->container->getDefinitions()), + [ + 'service_container', + StreamFactoryInterface::class, + 'snappy.backend.myBackend.factory', + 'snappy.backend.myBackend', + ] + ); + + $streamFactory = $this->container->get(StreamFactoryInterface::class); + + $this->assertInstanceOf(StreamFactoryInterface::class, $streamFactory); + + $factory = $this->container->get('snappy.backend.myBackend.factory'); + $this->assertInstanceOf(HeadlessChromiumFactory::class, $factory); + + $backend = $this->container->get('snappy.backend.myBackend'); + + $this->assertInstanceOf(HeadlessChromiumAdapter::class, $backend); + + $expectedOptions = new Options( + null, + [ + new ExtraOption\PrintToPdf($outputFile->getPathname()), + ] + ); + + $expectedBackend = new HeadlessChromiumAdapter( + 'chromium', + 60, + $factory, + $expectedOptions, + $streamFactory + ); + + $this->assertEquals($backend, $expectedBackend); + } }