diff --git a/src/Backend/Dompdf/Tests/DomAdapterTest.php b/src/Backend/Dompdf/Tests/DomAdapterTest.php index 12ee21d..76ca3a2 100644 --- a/src/Backend/Dompdf/Tests/DomAdapterTest.php +++ b/src/Backend/Dompdf/Tests/DomAdapterTest.php @@ -8,6 +8,7 @@ use KNPLabs\Snappy\Core\Backend\Options; use Nyholm\Psr7\Factory\Psr17Factory; use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamInterface; @@ -15,6 +16,7 @@ * @internal */ #[CoversNothing] +#[Group('integration')] final class DomAdapterTest extends TestCase { private DompdfFactory $factory; @@ -23,6 +25,10 @@ final class DomAdapterTest extends TestCase protected function setUp(): void { + if (!\extension_loaded('imagick')) { + self::markTestSkipped('Imagick extension is required for PDF comparison tests'); + } + $this->factory = new DompdfFactory(new Psr17Factory()); $this->options = Options::create() ->withExtraOptions( @@ -77,7 +83,8 @@ private function assertPdfStreamEqualsFile(StreamInterface $stream, string $file stream_copy_to_stream($from, $to); - $path = stream_get_meta_data($to)['uri']; + $metadata = stream_get_meta_data($to); + $path = $metadata['uri'] ?? throw new \RuntimeException('Stream metadata does not contain uri.'); $controlDocument = new \Imagick(); $compareDocument = new \Imagick(); diff --git a/src/Backend/WkHtmlToPdf/Tests/ExtraOptionTest.php b/src/Backend/WkHtmlToPdf/Tests/ExtraOptionTest.php index adb4f57..1dd5b17 100644 --- a/src/Backend/WkHtmlToPdf/Tests/ExtraOptionTest.php +++ b/src/Backend/WkHtmlToPdf/Tests/ExtraOptionTest.php @@ -15,10 +15,20 @@ #[CoversNothing] final class ExtraOptionTest extends TestCase { + /** + * @param non-empty-array $command + */ + #[DataProvider('provideRepeatableOptionCases')] + public function testRepeatableOption(ExtraOption $option, array $command): void + { + self::assertTrue($option->repeatable); + self::assertSame($option->command, $command); + } + /** * @return iterable}> */ - public static function repeatableProvider(): iterable + public static function provideRepeatableOptionCases(): iterable { yield [ new ExtraOption\Allow('/the/path'), @@ -76,10 +86,20 @@ public static function repeatableProvider(): iterable ]; } + /** + * @param non-empty-array $command + */ + #[DataProvider('provideNonRepeatableOptionCases')] + public function testNonRepeatableOption(ExtraOption $option, array $command): void + { + self::assertFalse($option->repeatable); + self::assertSame($option->command, $command); + } + /** * @return iterable}> */ - public static function nonRepeatableProvider(): iterable + public static function provideNonRepeatableOptionCases(): iterable { yield [ new ExtraOption\NoCollate(), @@ -476,24 +496,4 @@ public static function nonRepeatableProvider(): iterable ['--xsl-style-sheet', '/the/path'], ]; } - - /** - * @param non-empty-array $command - */ - #[DataProvider('repeatableProvider')] - public function testRepeatableOption(ExtraOption $option, array $command): void - { - self::assertTrue($option->repeatable); - self::assertSame($option->command, $command); - } - - /** - * @param non-empty-array $command - */ - #[DataProvider('nonRepeatableProvider')] - public function testNonRepeatableOption(ExtraOption $option, array $command): void - { - self::assertFalse($option->repeatable); - self::assertSame($option->command, $command); - } } diff --git a/src/Backend/WkHtmlToPdf/Tests/WkHtmlToPdfAdapterTest.php b/src/Backend/WkHtmlToPdf/Tests/WkHtmlToPdfAdapterTest.php index 498126a..67a12db 100644 --- a/src/Backend/WkHtmlToPdf/Tests/WkHtmlToPdfAdapterTest.php +++ b/src/Backend/WkHtmlToPdf/Tests/WkHtmlToPdfAdapterTest.php @@ -10,6 +10,7 @@ use KNPLabs\Snappy\Core\Backend\Options; use Nyholm\Psr7\Uri; use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamFactoryInterface; @@ -54,8 +55,13 @@ protected function setUp(): void $this->wkHtmlToPdfAdapter = $this->factory->create(new Options(null, [])); } + #[Group('integration')] public function testGenerateFromHtmlFile(): void { + if (!$this->isWkhtmltopdfAvailable()) { + self::markTestSkipped('wkhtmltopdf binary is not available'); + } + $htmlContent = '

Test PDF

'; $testFilePath = $this->tempDir.'/test.html'; file_put_contents($testFilePath, $htmlContent); @@ -94,8 +100,13 @@ public function testGenerateFromInvalidHtmlFile(): void $this->wkHtmlToPdfAdapter->generateFromHtmlFile(new \SplFileInfo($this->tempDir.'/nonexistent.html')); } + #[Group('integration')] public function testGenerateWithAdditionalOptions(): void { + if (!$this->isWkhtmltopdfAvailable()) { + self::markTestSkipped('wkhtmltopdf binary is not available'); + } + $htmlContent = 'Test PDF

Test PDF

'; $testFilePath = $this->tempDir.'/test_with_options.html'; file_put_contents($testFilePath, $htmlContent); @@ -140,4 +151,13 @@ public function testGenerateWithAdditionalOptions(): void unlink($testFilePath); } + + private function isWkhtmltopdfAvailable(): bool + { + $output = null; + $returnCode = null; + exec('which wkhtmltopdf 2>/dev/null', $output, $returnCode); + + return 0 === $returnCode; + } } diff --git a/src/Core/Filesystem/SplResourceInfo.php b/src/Core/Filesystem/SplResourceInfo.php index f7571c6..0f53253 100644 --- a/src/Core/Filesystem/SplResourceInfo.php +++ b/src/Core/Filesystem/SplResourceInfo.php @@ -11,7 +11,9 @@ final class SplResourceInfo extends \SplFileInfo */ public function __construct(public readonly mixed $resource) { - parent::__construct(stream_get_meta_data($this->resource)['uri']); + $metadata = stream_get_meta_data($this->resource); + $uri = $metadata['uri'] ?? throw new \RuntimeException('Stream metadata does not contain uri.'); + parent::__construct($uri); } public static function fromTmpFile(): self diff --git a/src/Core/Frontend/UriToPdf.php b/src/Core/Frontend/UriToPdf.php new file mode 100644 index 0000000..e9562b9 --- /dev/null +++ b/src/Core/Frontend/UriToPdf.php @@ -0,0 +1,52 @@ +adapter->withOptions($options), + $this->streamFactory + ); + } + + public function generateFromUri(UriInterface $uri): StreamInterface + { + if ($this->adapter instanceof Adapter\UriToPdf) { + return $this->adapter->generateFromUri($uri); + } + + if ($this->adapter instanceof Adapter\HtmlFileToPdf) { + // Download URI content to temp file, delegate + $content = file_get_contents((string) $uri); + + if (false === $content) { + throw new \RuntimeException(\sprintf('Failed to download URI: %s', (string) $uri)); + } + + $file = SplResourceInfo::fromTmpFile(); + fwrite($file->resource, $content); + + return $this->adapter->generateFromHtmlFile($file); + } + + throw new FrontendUnsupportedBackendException( + self::class, + $this->adapter::class, + ); + } +} diff --git a/src/Core/Tests/Frontend/UriToPdfTest.php b/src/Core/Tests/Frontend/UriToPdfTest.php new file mode 100644 index 0000000..ec245c2 --- /dev/null +++ b/src/Core/Tests/Frontend/UriToPdfTest.php @@ -0,0 +1,93 @@ +output = self::createStub(StreamInterface::class); + $this->streamFactory = new Psr17Factory(); + } + + public function testWithUriToPdf(): void + { + $backend = $this->createMock(Adapter\UriToPdf::class); + $frontend = new UriToPdf($backend, $this->streamFactory); + + $uri = new Uri('https://example.com'); + + $backend + ->method('generateFromUri') + ->with($uri) + ->willReturn($this->output) + ; + + self::assertSame( + $frontend->generateFromUri($uri), + $this->output, + ); + } + + public function testWithHtmlFileToPdf(): void + { + $backend = $this->createMock(Adapter\HtmlFileToPdf::class); + $frontend = new UriToPdf($backend, $this->streamFactory); + + $uri = new Uri('data:text/html,'); + + $backend + ->method('generateFromHtmlFile') + ->with( + new Constraint\Callback( + static fn (\SplFileInfo $file): bool => '' === file_get_contents($file->getPathname()) + ) + ) + ->willReturn($this->output) + ; + + self::assertSame( + $frontend->generateFromUri($uri), + $this->output, + ); + } + + public function testWithOptions(): void + { + $backend = $this->createMock(Adapter\UriToPdf::class); + $frontend = new UriToPdf($backend, $this->streamFactory); + + $options = Options::create(); + + $backend + ->method('withOptions') + ->with($options) + ->willReturnSelf() + ; + + $reconfigured = $frontend->withOptions($options); + + self::assertNotSame($frontend, $reconfigured); + } +} diff --git a/src/Framework/Symfony/DependencyInjection/Configuration/WkHtmlToPdfConfigurationFactory.php b/src/Framework/Symfony/DependencyInjection/Configuration/WkHtmlToPdfConfigurationFactory.php index d0b14d9..8e1bf89 100644 --- a/src/Framework/Symfony/DependencyInjection/Configuration/WkHtmlToPdfConfigurationFactory.php +++ b/src/Framework/Symfony/DependencyInjection/Configuration/WkHtmlToPdfConfigurationFactory.php @@ -7,6 +7,7 @@ use KNPLabs\Snappy\Backend\WkHtmlToPdf\WkHtmlToPdfAdapter; use KNPLabs\Snappy\Backend\WkHtmlToPdf\WkHtmlToPdfFactory; use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -37,9 +38,10 @@ public function create( new Definition( WkHtmlToPdfFactory::class, [ - '$streamFactory' => $container->getDefinition(StreamFactoryInterface::class), '$binary' => $configuration['binary'], '$timeout' => $configuration['timeout'], + '$streamFactory' => $container->getDefinition(StreamFactoryInterface::class), + '$uriFactory' => $container->getDefinition(UriFactoryInterface::class), ] ), ) diff --git a/src/Framework/Symfony/DependencyInjection/SnappyExtension.php b/src/Framework/Symfony/DependencyInjection/SnappyExtension.php index 1a0aacf..89d92fa 100644 --- a/src/Framework/Symfony/DependencyInjection/SnappyExtension.php +++ b/src/Framework/Symfony/DependencyInjection/SnappyExtension.php @@ -26,6 +26,7 @@ final class SnappyExtension extends Extension Adapter\HtmlFileToPdf::class => Frontend\HtmlFileToPdf::class, Adapter\HtmlToPdf::class => Frontend\HtmlToPdf::class, Adapter\StreamToPdf::class => Frontend\StreamToPdf::class, + Adapter\UriToPdf::class => Frontend\UriToPdf::class, ]; public function load(array $configuration, ContainerBuilder $container): void @@ -136,9 +137,9 @@ public function load(array $configuration, ContainerBuilder $container): void ), ) ; - } - $container->registerAliasForArgument($frontendId, $adapterClass, $backendName); + $container->registerAliasForArgument($frontendId, $frontendClass, $backendName); + } } } } diff --git a/src/Framework/Symfony/Tests/DependencyInjection/SnappyExtensionTest.php b/src/Framework/Symfony/Tests/DependencyInjection/SnappyExtensionTest.php index 9dfe037..e78f56e 100644 --- a/src/Framework/Symfony/Tests/DependencyInjection/SnappyExtensionTest.php +++ b/src/Framework/Symfony/Tests/DependencyInjection/SnappyExtensionTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Constraint\IsEqual; use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -37,6 +38,11 @@ protected function setUp(): void StreamFactoryInterface::class, new Definition(Psr17Factory::class), ); + + $this->container->setDefinition( + UriFactoryInterface::class, + new Definition(Psr17Factory::class), + ); } public function testLoadEmptyConfiguration(): void @@ -53,6 +59,7 @@ public function testLoadEmptyConfiguration(): void [ 'service_container', StreamFactoryInterface::class, + UriFactoryInterface::class, ], ); } @@ -88,12 +95,14 @@ public function testDompdfBackendConfiguration(): void [ 'service_container', StreamFactoryInterface::class, + UriFactoryInterface::class, 'knplabs.snappy.core.backend.factory.myBackend', 'knplabs.snappy.core.backend.adapter.myBackend', 'knplabs.snappy.core.frontend.domdocumenttopdf.myBackend', 'knplabs.snappy.core.frontend.htmlfiletopdf.myBackend', 'knplabs.snappy.core.frontend.htmltopdf.myBackend', 'knplabs.snappy.core.frontend.streamtopdf.myBackend', + 'knplabs.snappy.core.frontend.uritopdf.myBackend', ] );