diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
new file mode 100644
index 00000000..f01d7b13
--- /dev/null
+++ b/.github/workflows/tests.yaml
@@ -0,0 +1,77 @@
+name: Tests
+
+on:
+ pull_request: ~
+ push: ~
+
+jobs:
+ packages:
+ runs-on: ubuntu-20.04
+ outputs:
+ packages: ${{ steps.script.outputs.result }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/github-script@v7
+ id: script
+ with:
+ script: |
+ const fs = require('fs');
+
+ const composer = JSON.parse(
+ fs.readFileSync('./composer.json')
+ );
+
+ const packages = Object.keys(composer.autoload['psr-4']).map(
+ (namespace) => {
+ const path = composer.autoload['psr-4'][namespace];
+ const name = JSON.parse(fs.readFileSync(path + 'composer.json')).name;
+
+ return {
+ name: name,
+ path: './' + path,
+ };
+ }
+ );
+
+ packages.push({name: "knplabs/snappy", path: './'})
+
+ console.log(packages);
+
+ return packages;
+
+ package:
+ runs-on: ubuntu-20.04
+ needs: packages
+ strategy:
+ matrix:
+ includes: ${{ needs.packages.outputs.packages }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/github-script@v7
+ id: packages
+ with:
+ script: |
+ const fs = require('fs');
+
+ const composer = JSON.parse(
+ fs.readFileSync('./composer.json')
+ );
+
+ const packages = Object.keys(composer.autoload['psr-4']).map(
+ (namespace) => {
+ const path = composer.autoload['psr-4'][namespace];
+ const name = JSON.parse(fs.readFileSync(path + 'composer.json')).name;
+
+ return {
+ name: name,
+ path: './' + path,
+ };
+ }
+ );
+
+ packages.push({name: "knplabs/snappy", path: './'})
+
+ console.log(packages);
+
+ return packages;
+
diff --git a/.phpunit.result.cache b/.phpunit.result.cache
new file mode 100644
index 00000000..a50923ef
--- /dev/null
+++ b/.phpunit.result.cache
@@ -0,0 +1 @@
+{"version":1,"defects":{"KNPLabs\\Snappy\\Framework\\Symfony\\Tests\\DependencyInjection\\SnappyExtensionTest::testLoadEmptyConfiguration":8,"KNPLabs\\Snappy\\Framework\\Symfony\\Tests\\DependencyInjection\\SnappyExtensionTest::testConfigure":5,"KNPLabs\\Snappy\\Framework\\Symfony\\Tests\\DependencyInjection\\SnappyExtensionTest::testConfigureTmpDirectory":7,"KNPLabs\\Snappy\\Framework\\Symfony\\Tests\\DependencyInjection\\SnappyExtensionTest::testDompdfBackendConfiguration":8,"KNPLabs\\Snappy\\Core\\Tests\\Stream\\FileStreamTest::testTmpFileIsAutomaticalyRemoved":8},"times":{"KNPLabs\\Snappy\\Framework\\Symfony\\Tests\\DependencyInjection\\SnappyExtensionTest::testLoadEmptyConfiguration":0.003,"KNPLabs\\Snappy\\Framework\\Symfony\\Tests\\DependencyInjection\\SnappyExtensionTest::testConfigure":0,"KNPLabs\\Snappy\\Framework\\Symfony\\Tests\\DependencyInjection\\SnappyExtensionTest::testConfigureTmpDirectory":0,"KNPLabs\\Snappy\\Framework\\Symfony\\Tests\\DependencyInjection\\SnappyExtensionTest::testDompdfBackendConfiguration":0.001,"KNPLabs\\Snappy\\Core\\Tests\\Stream\\FileStreamTest::testTmpFileStream":0.005,"KNPLabs\\Snappy\\Core\\Tests\\Stream\\FileStreamTest::testTmpFileStreamCreateTemporaryFile":0,"KNPLabs\\Snappy\\Core\\Tests\\Stream\\FileStreamTest::testTmpFileStreamReadTheFile":0,"KNPLabs\\Snappy\\Core\\Tests\\Stream\\FileStreamTest::testTmpFileIsAutomaticalyRemoved":0}}
\ No newline at end of file
diff --git a/bin/sync-composer.php b/bin/sync-composer.php
new file mode 100644
index 00000000..9e0cae15
--- /dev/null
+++ b/bin/sync-composer.php
@@ -0,0 +1,56 @@
+ $constraint) {
+ if (isset($replace[$name])) {
+ continue;
+ }
+
+ if (false === isset($dependencies[$name])) {
+ throw new \Exception(
+ sprintf(
+ 'Dependency "%s" not found in %s/composer.json.',
+ $name,
+ __DIR__,
+ )
+ );
+ }
+
+ $json[$part][$name] = $dependencies[$name];
+ }
+ }
+
+ $content = file_put_contents(
+ $path . '/composer.json',
+ json_encode(
+ $json,
+ flags: JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR|JSON_UNESCAPED_SLASHES,
+ ),
+ );
+}
diff --git a/composer.json b/composer.json
index e6babd5f..fd30a2c9 100644
--- a/composer.json
+++ b/composer.json
@@ -1,10 +1,16 @@
{
"name": "knplabs/knp-snappy",
- "type": "library",
"description": "PHP library allowing thumbnail, snapshot or PDF generation from a url or a html page. Wrapper for wkhtmltopdf/wkhtmltoimage.",
- "keywords": ["pdf", "thumbnail", "snapshot", "knplabs", "knp", "wkhtmltopdf"],
- "homepage": "http://github.com/KnpLabs/snappy",
"license": "MIT",
+ "type": "library",
+ "keywords": [
+ "pdf",
+ "thumbnail",
+ "snapshot",
+ "knplabs",
+ "knp",
+ "wkhtmltopdf"
+ ],
"authors": [
{
"name": "KNP Labs Team",
@@ -15,18 +21,46 @@
"homepage": "http://github.com/KnpLabs/snappy/contributors"
}
],
+ "homepage": "http://github.com/KnpLabs/snappy",
"require": {
"php": ">=8.1",
- "symfony/process": "~5.0||~6.0",
- "psr/log": "^2.0||^3.0",
- "symfony/http-client": "^6.2",
- "psr/http-message": "^2.0"
+ "dompdf/dompdf": "^3.0",
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^2.0",
+ "psr/log": "^2.0|^3.0",
+ "symfony/config": "^5.4|^6.4|^7.1",
+ "symfony/dependency-injection": "^5.4|^6.4|^7.1",
+ "symfony/http-client": "^5.4|^6.4|^7.1",
+ "symfony/http-kernel": "^5.4|^6.4|^7.1",
+ "symfony/process": "^5.4|^6.4|^7.1"
+ },
+ "require-dev": {
+ "nyholm/psr7": "^1.8",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-phpunit": "^1.4",
+ "phpunit/phpunit": "^11.4"
+ },
+ "replace": {
+ "knplabs/snappy-bundle": "self.version",
+ "knplabs/snappy-core": "self.version",
+ "knplabs/snappy-dompdf": "self.version",
+ "knplabs/snappy-wkhtmltopdf": "self.version"
},
"autoload": {
"psr-4": {
- "KnpLabs\\Snappy\\": "src/"
+ "KNPLabs\\Snappy\\Backend\\Dompdf\\": "src/Backend/Dompdf/",
+ "KNPLabs\\Snappy\\Backend\\WkHtmlToPdf\\": "src/Backend/WkHtmlToPdf/",
+ "KNPLabs\\Snappy\\Core\\": "src/Core/",
+ "KNPLabs\\Snappy\\Framework\\Symfony\\": "src/Framework/Symfony/"
}
},
+ "config": {
+ "allow-plugins": {
+ "phpstan/extension-installer": true
+ },
+ "sort-packages": true
+ },
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 00000000..776ccd86
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,4 @@
+parameters:
+ level: max
+ paths:
+ - src
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 00000000..f42624f3
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+ ./src/Backend/Dompdf
+
+
+ ./src/Backend/WkHtmlToPdf
+
+
+ ./src/Core/
+
+
+ ./src/Framework/Symfony/
+
+
+
diff --git a/src/Backend/Dompdf/DompdfAdapter.php b/src/Backend/Dompdf/DompdfAdapter.php
new file mode 100644
index 00000000..266ffbb3
--- /dev/null
+++ b/src/Backend/Dompdf/DompdfAdapter.php
@@ -0,0 +1,104 @@
+
+ */
+ use Reconfigurable;
+
+ public function __construct(
+ DompdfFactory $factory,
+ Options $options,
+ private readonly StreamFactoryInterface $streamFactory
+ ) {
+ $this->factory = $factory;
+ $this->options = $options;
+ }
+
+ public function generateFromDOMDocument(DOMDocument $DOMDocument): StreamInterface
+ {
+ $dompdf = $this->buildDompdf();
+ $dompdf->loadDOM($DOMDocument);
+
+ return $this->createStream($dompdf);
+ }
+
+ public function generateFromHtmlFile(SplFileInfo $file): StreamInterface
+ {
+ $dompdf = $this->buildDompdf();
+ $dompdf->loadHtmlFile($file->getPath());
+
+ return $this->createStream($dompdf);
+ }
+
+ public function generateFromHtml(string $html): StreamInterface
+ {
+ $dompdf = $this->buildDompdf();
+ $dompdf->loadHtml($html);
+
+ return $this->createStream($dompdf);
+ }
+
+ private function buildDompdf(): Dompdf\Dompdf
+ {
+ return new Dompdf\Dompdf( $this->compileInitOptions());
+ }
+
+ private function compileInitOptions(): Dompdf\Options
+ {
+ $options = new Dompdf\Options(
+ is_array($this->options->extraOptions['initOptions'])
+ ? $this->options->extraOptions['initOptions']
+ : null
+ );
+
+ if (null !== $this->options->pageOrientation) {
+ $options->setDefaultPaperOrientation(
+ $this->options->pageOrientation->value
+ );
+ }
+
+ return $options;
+ }
+
+ /**
+ * @return array
+ */
+ private function compileOutputOptions(): array
+ {
+ $options = $this->options->extraOptions['outputOptions'];
+
+ if (false === is_array($options)) {
+ $options = [];
+ }
+
+ return $options;
+ }
+
+ private function createStream(Dompdf\Dompdf $dompdf): StreamInterface
+ {
+ $output = $dompdf->output($this->compileOutputOptions());
+
+ return $this
+ ->streamFactory
+ ->createStream($output ?: '')
+ ;
+ }
+}
diff --git a/src/Backend/Dompdf/DompdfFactory.php b/src/Backend/Dompdf/DompdfFactory.php
new file mode 100644
index 00000000..17b08f6b
--- /dev/null
+++ b/src/Backend/Dompdf/DompdfFactory.php
@@ -0,0 +1,30 @@
+
+ */
+final readonly class DompdfFactory implements Factory
+{
+ public function __construct(private readonly StreamFactoryInterface $streamFactory)
+ {
+ }
+
+ public function create(Options $options): DompdfAdapter
+ {
+ return new DompdfAdapter(
+ factory: $this,
+ options: $options,
+ streamFactory: $this->streamFactory,
+ );
+ }
+}
diff --git a/src/Backend/Dompdf/composer.json b/src/Backend/Dompdf/composer.json
new file mode 100644
index 00000000..9089d2db
--- /dev/null
+++ b/src/Backend/Dompdf/composer.json
@@ -0,0 +1,35 @@
+{
+ "name": "knplabs/snappy-dompdf",
+ "license": "MIT",
+ "type": "library",
+ "authors": [
+ {
+ "name": "KNP Labs Team",
+ "homepage": "http://knplabs.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://github.com/KnpLabs/snappy/contributors"
+ }
+ ],
+ "homepage": "http://github.com/KnpLabs/snappy",
+ "require": {
+ "php": ">=8.1",
+ "dompdf/dompdf": "^3.0",
+ "knplabs/snappy-core": "^2.0",
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^2.0"
+ },
+ "require-dev": {
+ "nyholm/psr7": "^1.8",
+ "phpunit/phpunit": "^11.4"
+ },
+ "autoload": {
+ "psr-4": {
+ "KNPLabs\\Snappy\\Backend\\Dompdf\\": "src/"
+ }
+ },
+ "config": {
+ "sort-packages": true
+ }
+}
\ No newline at end of file
diff --git a/src/Backend/WkHtmlToPdf/WkHtmlToPdf.php b/src/Backend/WkHtmlToPdf/WkHtmlToPdf.php
deleted file mode 100644
index a17fda7e..00000000
--- a/src/Backend/WkHtmlToPdf/WkHtmlToPdf.php
+++ /dev/null
@@ -1,17 +0,0 @@
-
+ */
+ use Reconfigurable;
+
+ /**
+ * @param non-empty-string $binary
+ * @param positive-int $timeout
+ */
+ public function __construct(
+ private string $binary,
+ private int $timeout,
+ WkHtmlToPdfFactory $factory,
+ Options $options
+ ) {
+ $this->factory = $factory;
+ $this->options = $options;
+ }
+
+ public function generateFromHtmlFile(SplFileInfo $file): StreamInterface
+ {
+ throw new \Exception("Not implemented for {$this->binary} with timeout {$this->timeout}.");
+ }
+}
diff --git a/src/Backend/WkHtmlToPdf/WkHtmlToPdfFactory.php b/src/Backend/WkHtmlToPdf/WkHtmlToPdfFactory.php
new file mode 100644
index 00000000..77829e36
--- /dev/null
+++ b/src/Backend/WkHtmlToPdf/WkHtmlToPdfFactory.php
@@ -0,0 +1,35 @@
+
+ */
+final class WkHtmlToPdfFactory implements Factory
+{
+ /**
+ * @param non-empty-string $binary
+ * @param positive-int $timeout
+ */
+ public function __construct(private readonly string $binary, private readonly int $timeout)
+ {
+
+ }
+
+ public function create(Options $options): Adapter
+ {
+ return new WkHtmlToPdfAdapter(
+ $this->binary,
+ $this->timeout,
+ $this,
+ $options,
+ );
+ }
+}
diff --git a/src/Backend/WkHtmlToPdf/composer.json b/src/Backend/WkHtmlToPdf/composer.json
new file mode 100644
index 00000000..150de1f5
--- /dev/null
+++ b/src/Backend/WkHtmlToPdf/composer.json
@@ -0,0 +1,35 @@
+{
+ "name": "knplabs/snappy-wkhtmltopdf",
+ "license": "MIT",
+ "type": "library",
+ "authors": [
+ {
+ "name": "KNP Labs Team",
+ "homepage": "http://knplabs.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://github.com/KnpLabs/snappy/contributors"
+ }
+ ],
+ "homepage": "http://github.com/KnpLabs/snappy",
+ "require": {
+ "php": ">=8.1",
+ "knplabs/snappy-core": "^2.0",
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^2.0",
+ "symfony/process": "^5.4|^6.4|^7.1"
+ },
+ "require-dev": {
+ "nyholm/psr7": "^1.8",
+ "phpunit/phpunit": "^11.4"
+ },
+ "autoload": {
+ "psr-4": {
+ "KNPLabs\\Snappy\\Backend\\WkHtmlToPdf\\": "src/"
+ }
+ },
+ "config": {
+ "sort-packages": true
+ }
+}
\ No newline at end of file
diff --git a/src/Bundle/DependencyInjection/Configuration.php b/src/Bundle/DependencyInjection/Configuration.php
deleted file mode 100644
index ae9d19ef..00000000
--- a/src/Bundle/DependencyInjection/Configuration.php
+++ /dev/null
@@ -1,49 +0,0 @@
-getRootNode()
- ->children()
- ->arrayNode('backends')
- ->useAttributeAsKey('name')
- ->arrayPrototype()
- ->children()
- ->scalarNode('driver')
- ->isRequired()
- ->validate()
- ->ifNotInArray(['wkhtmltopdf', 'chromium'])
- ->thenInvalid('Invalid backend driver %s')
- ->end()
- ->end()
- ->integerNode('timeout')
- ->min(1)
- ->defaultValue(30)
- ->end()
- ->scalarNode('binary_path')
- ->isRequired()
- ->cannotBeEmpty()
- ->end()
- ->arrayNode('options')
- ->useAttributeAsKey('name')
- ->scalarPrototype()->end()
- ->end()
- ->end()
- ->end()
- ->end()
- ->end()
- ;
-
- return $treeBuilder;
- }
-}
diff --git a/src/Bundle/DependencyInjection/SnappyExtension.php b/src/Bundle/DependencyInjection/SnappyExtension.php
deleted file mode 100644
index 52d5f46c..00000000
--- a/src/Bundle/DependencyInjection/SnappyExtension.php
+++ /dev/null
@@ -1,18 +0,0 @@
-processConfiguration(
- new Configuration(),
- [[
- 'backends' => [
- 'my_minimally_configured_wkhtmltopdf_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- ],
- ],
- ]]
- );
-
- $expected = [
- 'backends' => [
- 'my_minimally_configured_wkhtmltopdf_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'timeout' => 30,
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- 'options' => [],
- ],
- ],
- ];
-
- $this->assertEquals($config, $expected);
- }
-
- public function testItProcessesAFullWkhtmltopdfConfiguration(): void
- {
- $config = (new Processor())->processConfiguration(
- new Configuration(),
- [[
- 'backends' => [
- 'my_fully_configured_wkhtmltopdf_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'timeout' => 60,
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- 'options' => [
- 'key1' => 'val',
- 'key2' => null,
- 'key3',
- ],
- ],
- ],
- ]]
- );
-
- $expected = [
- 'backends' => [
- 'my_fully_configured_wkhtmltopdf_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'timeout' => 60,
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- 'options' => [
- 'key1' => 'val',
- 'key2' => null,
- 'key3',
- ],
- ],
- ],
- ];
-
- $this->assertEquals($config, $expected);
- }
-
- public function testItProcessesAMinimalChromiumConfiguration(): void
- {
- $config = (new Processor())->processConfiguration(
- new Configuration(),
- [[
- 'backends' => [
- 'my_minimally_configured_chromium_backend' => [
- 'driver' => 'chromium',
- 'binary_path' => '/usr/bin/chromium',
- ],
- ],
- ]]
- );
-
- $expected = [
- 'backends' => [
- 'my_minimally_configured_chromium_backend' => [
- 'driver' => 'chromium',
- 'timeout' => 30,
- 'binary_path' => '/usr/bin/chromium',
- 'options' => [],
- ],
- ],
- ];
-
- $this->assertEquals($config, $expected);
- }
-
- public function testItProcessesAFullChromiumConfiguration(): void
- {
- $config = (new Processor())->processConfiguration(
- new Configuration(),
- [[
- 'backends' => [
- 'my_fully_configured_chromium_backend' => [
- 'driver' => 'chromium',
- 'timeout' => 60,
- 'binary_path' => '/usr/bin/chromium',
- 'options' => [
- 'key1' => 'val',
- 'key2' => null,
- 'key3',
- ],
- ],
- ],
- ]]
- );
-
- $expected = [
- 'backends' => [
- 'my_fully_configured_chromium_backend' => [
- 'driver' => 'chromium',
- 'timeout' => 60,
- 'binary_path' => '/usr/bin/chromium',
- 'options' => [
- 'key1' => 'val',
- 'key2' => null,
- 'key3',
- ],
- ],
- ],
- ];
-
- $this->assertEquals($config, $expected);
- }
-
- public function testItProcessesAMultiBackendConfiguration(): void
- {
- $config = (new Processor())->processConfiguration(
- new Configuration(),
- [[
- 'backends' => [
- 'my_minimally_configured_wkhtmltopdf_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- ],
- 'my_fully_configured_wkhtmltopdf_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'timeout' => 60,
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- 'options' => [
- 'key1' => 'val',
- 'key2' => null,
- 'key3',
- ],
- ],
- 'my_fully_configured_chromium_backend' => [
- 'driver' => 'chromium',
- 'timeout' => 60,
- 'binary_path' => '/usr/bin/chromium',
- 'options' => [
- 'key1' => 'val',
- 'key2' => null,
- 'key3',
- ],
- ],
- ],
- ]]
- );
-
- $expected = [
- 'backends' => [
- 'my_minimally_configured_wkhtmltopdf_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'timeout' => 30,
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- 'options' => [],
- ],
- 'my_fully_configured_wkhtmltopdf_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'timeout' => 60,
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- 'options' => [
- 'key1' => 'val',
- 'key2' => null,
- 'key3',
- ],
- ],
- 'my_fully_configured_chromium_backend' => [
- 'driver' => 'chromium',
- 'timeout' => 60,
- 'binary_path' => '/usr/bin/chromium',
- 'options' => [
- 'key1' => 'val',
- 'key2' => null,
- 'key3',
- ],
- ],
- ],
- ];
-
- $this->assertEquals($config, $expected);
- }
-
- public function testItThrowsWhenProcessingAnInvalidDriverConfiguration(): void
- {
- $this->expectException(InvalidConfigurationException::class);
-
- (new Processor())->processConfiguration(
- new Configuration(),
- [[
- 'backends' => [
- 'invalid_backend' => [
- 'driver' => 'non-existing-driver',
- 'binary_path' => '/usr/bin/non-existing-binary',
- ],
- ],
- ]]
- );
- }
-
- public function testItThrowsWhenProcessingAnInvalidBinaryPathConfiguration(): void
- {
- $this->expectException(InvalidConfigurationException::class);
-
- (new Processor())->processConfiguration(
- new Configuration(),
- [[
- 'backends' => [
- 'invalid_backend' => [
- 'driver' => 'wkhtmltopdf',
- ],
- ],
- ]]
- );
- }
-
- public function testItThrowsWhenProcessingAnInvalidTimeoutConfiguration(): void
- {
- $this->expectException(InvalidConfigurationException::class);
-
- (new Processor())->processConfiguration(
- new Configuration(),
- [[
- 'backends' => [
- 'invalid_backend' => [
- 'driver' => 'wkhtmltopdf',
- 'timeout' => 0,
- 'binary_path' => '/usr/bin/wkhtmltopdf',
- ],
- ],
- ]]
- );
- }
-}
diff --git a/src/Core/Backend/Adapter.php b/src/Core/Backend/Adapter.php
new file mode 100644
index 00000000..b06af24c
--- /dev/null
+++ b/src/Core/Backend/Adapter.php
@@ -0,0 +1,13 @@
+
+ */
+ private readonly Factory $factory;
+
+ private readonly Options $options;
+
+ /**
+ * @return TAdapter
+ */
+ public function withOptions(Options|callable $options): static
+ {
+ if (is_callable($options)) {
+ $options = $options($this->options);
+ }
+
+ return $this
+ ->factory
+ ->create($options)
+ ;
+ }
+}
diff --git a/src/Core/Backend/Adapter/UriToPdf.php b/src/Core/Backend/Adapter/UriToPdf.php
new file mode 100644
index 00000000..df2c21e1
--- /dev/null
+++ b/src/Core/Backend/Adapter/UriToPdf.php
@@ -0,0 +1,14 @@
+ $extraOptions
+ */
+ public function __construct(
+ public readonly ?PageOrientation $pageOrientation,
+ public readonly array $extraOptions
+ ) {
+ }
+
+ public static function create(): self
+ {
+ return new self(
+ pageOrientation: null,
+ extraOptions: []
+ );
+ }
+
+ public function withPageOrientation(?PageOrientation $pageOrientation): self
+ {
+ return new self(
+ pageOrientation: $pageOrientation,
+ extraOptions: $this->extraOptions,
+ );
+ }
+
+ /**
+ * @param array $extraOptions
+ */
+ public function withExtraOptions(array $extraOptions): self
+ {
+ return new self(
+ pageOrientation: $this->pageOrientation,
+ extraOptions: $extraOptions,
+ );
+ }
+}
diff --git a/src/Core/Backend/Options/PageOrientation.php b/src/Core/Backend/Options/PageOrientation.php
new file mode 100644
index 00000000..46d466ee
--- /dev/null
+++ b/src/Core/Backend/Options/PageOrientation.php
@@ -0,0 +1,12 @@
+stringToPdf->generateFromString(
- file_get_contents($file->getPathname()),
- $options,
- );
- }
-}
diff --git a/src/Core/Bridge/FromHtmlFileToHtmlToPdf.php b/src/Core/Bridge/FromHtmlFileToHtmlToPdf.php
new file mode 100644
index 00000000..25d99414
--- /dev/null
+++ b/src/Core/Bridge/FromHtmlFileToHtmlToPdf.php
@@ -0,0 +1,36 @@
+getPathname());
+
+ if (false === $html) {
+ throw new \RuntimeException('Unable to read file.');
+ }
+
+ return $this->adapter->generateFromHtml($html);
+ }
+
+ public function withOptions(Options|callable $options): self
+ {
+ return new self( $this->adapter->withOptions($options));
+ }
+}
diff --git a/src/Core/Bridge/FromHtmlToHtmlFileToPdf.php b/src/Core/Bridge/FromHtmlToHtmlFileToPdf.php
new file mode 100644
index 00000000..03f2e397
--- /dev/null
+++ b/src/Core/Bridge/FromHtmlToHtmlFileToPdf.php
@@ -0,0 +1,39 @@
+streamFactory);
+
+ file_put_contents($temporary->file->getPathname(), $html);
+
+ return $this->adapter->generateFromHtmlFile($temporary->file);
+ }
+
+ public function withOptions(Options|callable $options): self
+ {
+ return new self(
+ $this->adapter->withOptions($options),
+ $this->streamFactory,
+ );
+ }
+}
diff --git a/src/Core/Bridge/FromStringToFileToPdf.php b/src/Core/Bridge/FromStringToFileToPdf.php
deleted file mode 100644
index fd22e359..00000000
--- a/src/Core/Bridge/FromStringToFileToPdf.php
+++ /dev/null
@@ -1,33 +0,0 @@
-fileToPdf->generateFromFile($file, $options);
- } finally {
- unlink($path);
- }
-
- return $stream;
- }
-}
diff --git a/src/Core/FileToPdf.php b/src/Core/FileToPdf.php
deleted file mode 100644
index 9cd2db76..00000000
--- a/src/Core/FileToPdf.php
+++ /dev/null
@@ -1,10 +0,0 @@
-createStreamFromResource(tmpFile());
+ $filename = $stream->getMetadata('uri');
+
+ if (false === is_string($filename)) {
+ throw new \UnexpectedValueException('Unable to retrieve the uri of the temporary file created.');
+ }
+
+ return new self(
+ new SplFileInfo($filename),
+ $stream
+ );
+ }
+
+ public function __construct(public readonly SplFileInfo $file, StreamInterface $stream)
+ {
+ $this->stream = $stream;
+ }
+}
diff --git a/src/Core/Stream/StreamWrapper.php b/src/Core/Stream/StreamWrapper.php
new file mode 100644
index 00000000..efc85e70
--- /dev/null
+++ b/src/Core/Stream/StreamWrapper.php
@@ -0,0 +1,85 @@
+stream;
+ }
+
+ public function close(): void
+ {
+ $this->stream->close();
+ }
+
+ public function detach()
+ {
+ return $this->stream->detach();
+ }
+
+ public function getSize(): ?int
+ {
+ return $this->stream->getSize();
+ }
+
+ public function tell(): int
+ {
+ return $this->stream->tell();
+ }
+
+ public function eof(): bool
+ {
+ return $this->stream->eof();
+ }
+
+ public function isSeekable(): bool
+ {
+ return $this->stream->isSeekable();
+ }
+
+ public function seek(int $offset, int $whence = 0): void
+ {
+ $this->stream->seek($offset, $whence);
+ }
+
+ public function rewind(): void
+ {
+ $this->stream->rewind();
+ }
+
+ public function isWritable(): bool
+ {
+ return $this->stream->isWritable();
+ }
+
+ public function write(string $string): int
+ {
+ return $this->stream->write($string);
+ }
+
+ public function isReadable(): bool
+ {
+ return $this->stream->isWritable();
+ }
+
+ public function read(int $length): string
+ {
+ return $this->stream->read($length);
+ }
+
+ public function getContents(): string
+ {
+ return $this->stream->getContents();
+ }
+
+ public function getMetadata(?string $key = null)
+ {
+ return $this->stream->getMetadata($key);
+ }
+}
diff --git a/src/Core/StringToPdf.php b/src/Core/StringToPdf.php
deleted file mode 100644
index aebb85fe..00000000
--- a/src/Core/StringToPdf.php
+++ /dev/null
@@ -1,10 +0,0 @@
-stream = FileStream::createTmpFile(
+ new Psr17Factory,
+ );
+ }
+
+ public function testTmpFileStreamCreateTemporaryFile(): void
+ {
+ $file = $this->stream->file;
+
+ $this->assertFileExists($file->getPathname());
+ $this->assertFileIsReadable( $file->getPathname());
+ $this->assertFileIsWritable( $file->getPathname());
+ }
+
+ public function testTmpFileStreamReadTheFile(): void
+ {
+ $file = $this->stream->file;
+
+ file_put_contents($file->getPathname(), 'the content');
+
+ $this->assertEquals(
+ (string) $this->stream,
+ 'the content',
+ );
+ }
+
+ public function testTmpFileIsAutomaticalyRemoved(): void
+ {
+ $file = $this->stream->file;
+
+ $this->assertFileExists($file->getPathname());
+
+ unset($this->stream);
+
+ $this->assertFileDoesNotExist($file->getPathname());
+ }
+}
diff --git a/src/Core/UriToPdf.php b/src/Core/UriToPdf.php
deleted file mode 100644
index f30a6ecf..00000000
--- a/src/Core/UriToPdf.php
+++ /dev/null
@@ -1,11 +0,0 @@
-=7.2.5",
+ "php": ">=8.1",
"psr/http-message": "^2.0"
},
"autoload": {
"psr-4": {
- "KnpLabs\\Snappy\\Core\\": "./"
+ "KNPLabs\\Snappy\\Core\\": "./"
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Bundle/.gitattributes b/src/Framework/Symfony/.gitattributes
similarity index 100%
rename from src/Bundle/.gitattributes
rename to src/Framework/Symfony/.gitattributes
diff --git a/src/Bundle/.gitignore b/src/Framework/Symfony/.gitignore
similarity index 100%
rename from src/Bundle/.gitignore
rename to src/Framework/Symfony/.gitignore
diff --git a/src/Framework/Symfony/DependencyInjection/Configuration.php b/src/Framework/Symfony/DependencyInjection/Configuration.php
new file mode 100644
index 00000000..d5e5548c
--- /dev/null
+++ b/src/Framework/Symfony/DependencyInjection/Configuration.php
@@ -0,0 +1,91 @@
+
+ */
+ private array $factories;
+
+ public function __construct(BackendConfigurationFactory ...$factories)
+ {
+ $this->factories = $factories;
+ }
+
+ public function getConfigTreeBuilder(): TreeBuilder
+ {
+ $treeBuilder = new TreeBuilder('snappy');
+ $rootNode = $treeBuilder->getRootNode();
+
+ $backendNodeBuilder = $rootNode
+ ->children()
+ ->arrayNode('backends')
+ ->useAttributeAsKey('name')
+ ->example(
+ array_merge(
+ ...array_map(
+ fn(BackendConfigurationFactory $factory): array => [
+ $factory->getKey() => [
+ 'pageOrientation' => PageOrientation::PORTRAIT->value,
+ 'options' => [],
+ ...$factory->getExample()
+ ],
+ ],
+ $this->factories
+ )
+ )
+ )
+ ->arrayPrototype()
+ ;
+
+ foreach ($this->factories as $factory) {
+ $name = str_replace('-', '_', $factory->getKey());
+
+ $factoryNode = $backendNodeBuilder
+ ->children()
+ ->arrayNode($name)
+ ->canBeUnset()
+ ;
+
+ $this->buildOptionsConfiguration($factoryNode);
+
+ $factory->addConfiguration($factoryNode);
+ }
+
+ return $treeBuilder;
+ }
+
+ private function buildOptionsConfiguration(ArrayNodeDefinition $node): void
+ {
+ $optionsNode = $node
+ ->children()
+ ->arrayNode('options')
+ ;
+
+ $optionsNode
+ ->children()
+ ->enumNode('pageOrientation')
+ ->values(
+ array_map(
+ fn(PageOrientation $pageOrientation): string => $pageOrientation->value,
+ PageOrientation::cases(),
+ )
+ )
+ ;
+
+ $optionsNode
+ ->children()
+ ->arrayNode('extraOptions')
+ ;
+ }
+}
diff --git a/src/Framework/Symfony/DependencyInjection/Configuration/BackendConfigurationFactory.php b/src/Framework/Symfony/DependencyInjection/Configuration/BackendConfigurationFactory.php
new file mode 100644
index 00000000..39970846
--- /dev/null
+++ b/src/Framework/Symfony/DependencyInjection/Configuration/BackendConfigurationFactory.php
@@ -0,0 +1,35 @@
+
+ */
+ public function getExample(): array;
+
+ /**
+ * @param array $configuration
+ * @param non-empty-string $backendId
+ * @param non-empty-string $factoryId
+ * @param non-empty-string $backendName
+ */
+ public function create(ContainerBuilder $container, array $configuration, string $backendId, string $backendName, string $factoryId, Definition $options): void;
+
+ public function addConfiguration(ArrayNodeDefinition $node): void;
+}
diff --git a/src/Framework/Symfony/DependencyInjection/Configuration/DompdfConfigurationFactory.php b/src/Framework/Symfony/DependencyInjection/Configuration/DompdfConfigurationFactory.php
new file mode 100644
index 00000000..da03c235
--- /dev/null
+++ b/src/Framework/Symfony/DependencyInjection/Configuration/DompdfConfigurationFactory.php
@@ -0,0 +1,87 @@
+setDefinition(
+ $factoryId,
+ new Definition(
+ DompdfFactory::class,
+ [
+ '$streamFactory' => $container->getDefinition(StreamFactoryInterface::class)
+ ]
+ ),
+ );
+
+ $container
+ ->setDefinition(
+ $backendId,
+ (new Definition(DompdfAdapter::class))
+ ->setFactory([$container->getDefinition($factoryId), 'create'])
+ ->setArgument('$options', $options)
+ )
+ ;
+
+ $container->registerAliasForArgument($backendId, DompdfAdapter::class, $backendName);
+ }
+
+ public function getExample(): array
+ {
+ return [
+ 'extraOptions' => [
+ 'initOptions' => [],
+ 'outputOptions' => [],
+ ]
+ ];
+ }
+
+ public function addConfiguration(ArrayNodeDefinition $node): void
+ {
+ $optionsNode = $node
+ ->getChildNodeDefinitions()['options']
+ ;
+
+ $extraOptionsNode = $optionsNode
+ ->getChildNodeDefinitions()['extraOptions']
+ ;
+
+ $extraOptionsNode
+ ->children()
+ ->variableNode('initOptions')
+ ->info(sprintf('Configuration passed to %s::__construct().', Dompdf::class))
+ ;
+
+ $extraOptionsNode
+ ->children()
+ ->variableNode('outputOptions')
+ ->info(sprintf('Configuration passed to %s::output().', Dompdf::class))
+ ;
+ }
+}
diff --git a/src/Framework/Symfony/DependencyInjection/Configuration/WkHtmlToPdfConfigurationFactory.php b/src/Framework/Symfony/DependencyInjection/Configuration/WkHtmlToPdfConfigurationFactory.php
new file mode 100644
index 00000000..96b61888
--- /dev/null
+++ b/src/Framework/Symfony/DependencyInjection/Configuration/WkHtmlToPdfConfigurationFactory.php
@@ -0,0 +1,81 @@
+setDefinition(
+ $factoryId,
+ new Definition(
+ WkHtmlToPdfFactory::class,
+ [
+ '$streamFactory' => $container->getDefinition(StreamFactoryInterface::class),
+ '$binary' => $configuration['binary'],
+ '$timeout' => $configuration['timeout'],
+ ]
+ ),
+ );
+
+ $container
+ ->setDefinition(
+ $backendId,
+ (new Definition(WkHtmlToPdfAdapter::class))
+ ->setFactory([$container->getDefinition($factoryId), 'create'])
+ ->setArgument('$options', $options)
+ )
+ ;
+
+ $container->registerAliasForArgument($backendId, WkHtmlToPdfAdapter::class, $backendName);
+ }
+
+ public function getExample(): array
+ {
+ return [
+ 'binary' => '/usr/local/bin/wkhtmltopdf',
+ ];
+ }
+
+ public function addConfiguration(ArrayNodeDefinition $node): void
+ {
+ $node
+ ->children()
+ ->scalarNode('binary')
+ ->defaultValue('wkhtmltopdf')
+ ->info('Path or command to run wkdtmltopdf')
+ ;
+
+ $node
+ ->children()
+ ->integerNode('timeout')
+ ->defaultValue(60)
+ ->min(1)
+ ->info('Timeout (seconds) for wkhtmltopdf command')
+ ;
+ }
+}
diff --git a/src/Framework/Symfony/DependencyInjection/SnappyExtension.php b/src/Framework/Symfony/DependencyInjection/SnappyExtension.php
new file mode 100644
index 00000000..7e138013
--- /dev/null
+++ b/src/Framework/Symfony/DependencyInjection/SnappyExtension.php
@@ -0,0 +1,135 @@
+processConfiguration(
+ $this->getConfiguration($configuration, $container),
+ $configuration
+ );
+
+ $factories = array_merge(
+ ...array_map(
+ static fn(BackendConfigurationFactory $factory): array => [$factory->getKey() => $factory],
+ $this->getFactories(),
+ ),
+ );
+
+ foreach ($configuration['backends'] as $backendName => $subConfiguration) {
+ foreach ($subConfiguration as $backendType => $backendConfiguration) {
+ $backendId = $this->buildBackendServiceId($backendName);
+ $factoryId = $this->buildFactoryServiceId($backendName);
+ $options = $this->buildOptions($backendName, $backendType, $backendConfiguration['options']);
+
+ $factories[$backendType]
+ ->create(
+ $container,
+ $backendConfiguration,
+ $backendId,
+ $backendName,
+ $factoryId,
+ $options,
+ )
+ ;
+ }
+ }
+ }
+
+ /**
+ * @param array $configuration
+ */
+ public function getConfiguration(array $configuration, ContainerBuilder $container): Configuration
+ {
+ return new Configuration(...$this->getFactories());
+ }
+
+ /**
+ * @return array
+ */
+ private function getFactories(): array
+ {
+ return array_filter(
+ [
+ new DompdfConfigurationFactory,
+ new WkHtmlToPdfConfigurationFactory,
+ ],
+ static fn (BackendConfigurationFactory $factory): bool => $factory->isAvailable(),
+ );
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ private function buildBackendServiceId(string $name): string
+ {
+ return "snappy.backend.$name";
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ private function buildFactoryServiceId(string $name): string
+ {
+ return "snappy.backend.$name.factory";
+ }
+
+ /**
+ * @param array $configuration
+ */
+ private function buildOptions(string $backendName, string $backendType, array $configuration): Definition
+ {
+ $arguments = [
+ '$pageOrientation' => null,
+ '$extraOptions' => [],
+ ];
+
+ 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'])
+ ),
+ );
+ }
+
+ $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'])
+ ),
+ );
+ }
+
+ $arguments[ '$extraOptions'] = $configuration['extraOptions'];
+ }
+
+ return new Definition( Options::class, $arguments);
+ }
+}
diff --git a/src/Bundle/Makefile b/src/Framework/Symfony/Makefile
similarity index 100%
rename from src/Bundle/Makefile
rename to src/Framework/Symfony/Makefile
diff --git a/src/Framework/Symfony/SnappyBundle.php b/src/Framework/Symfony/SnappyBundle.php
new file mode 100644
index 00000000..e5f0de9f
--- /dev/null
+++ b/src/Framework/Symfony/SnappyBundle.php
@@ -0,0 +1,17 @@
+extension = new SnappyExtension;
+ $this->container = new ContainerBuilder;
+
+ $this->container->setDefinition(
+ StreamFactoryInterface::class,
+ new Definition(Psr17Factory::class),
+ );
+ }
+
+ public function testLoadEmptyConfiguration(): void
+ {
+ $configuration = [];
+
+ $this->extension->load(
+ $configuration,
+ $this->container,
+ );
+
+ $this->assertEquals(
+ array_keys($this->container->getDefinitions()),
+ [
+ 'service_container',
+ StreamFactoryInterface::class,
+ ],
+ );
+ }
+
+ public function testDompdfBackendConfiguration(): void
+ {
+ $configuration = [
+ 'snappy' => [
+ 'backends' => [
+ 'myBackend' => [
+ 'dompdf' => [
+ 'options' => [
+ 'pageOrientation' => PageOrientation::LANDSCAPE->value,
+ 'extraOptions' => [
+ 'initOptions' => [
+ 'foo' => 'bar',
+ ],
+ 'outputOptions' => [
+ 'baz' => 'yol',
+ ],
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ ];
+
+ $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(DompdfFactory::class, $factory);
+ $this->assertEquals(
+ $factory,
+ new DompdfFactory($streamFactory)
+ );
+
+ $backend = $this->container->get('snappy.backend.myBackend');
+
+ $this->assertInstanceOf(DompdfAdapter::class, $backend);
+ $this->assertEquals(
+ $factory,
+ new DompdfFactory($streamFactory),
+ ) ;
+
+ $this->assertEquals(
+ $backend,
+ new DompdfAdapter(
+ $factory,
+ new Options(
+ PageOrientation::LANDSCAPE,
+ [
+ 'initOptions' => ['foo' => 'bar'],
+ 'outputOptions' => ['baz' => 'yol'],
+ ],
+ ),
+ $streamFactory,
+ ),
+ );
+ }
+}
diff --git a/src/Bundle/composer.json b/src/Framework/Symfony/composer.json
similarity index 58%
rename from src/Bundle/composer.json
rename to src/Framework/Symfony/composer.json
index c6afaa35..feeb791d 100644
--- a/src/Bundle/composer.json
+++ b/src/Framework/Symfony/composer.json
@@ -2,7 +2,13 @@
"name": "knplabs/snappy-bundle",
"type": "symfony-bundle",
"description": "Easily create PDF and images in Symfony from HTML inputs",
- "keywords": ["knplabs", "knp", "snappy", "pdf", "bundle"],
+ "keywords": [
+ "knplabs",
+ "knp",
+ "snappy",
+ "pdf",
+ "bundle"
+ ],
"homepage": "http://github.com/KnpLabs/snappy",
"license": "MIT",
"authors": [
@@ -16,23 +22,26 @@
}
],
"require": {
- "php": ">=7.2.5",
- "symfony/config": "^5.4|^6.0",
- "symfony/dependency-injection": "^5.4|^6.0",
- "symfony/http-kernel": "^5.4|^6.0"
+ "php": ">=8.1",
+ "knplabs/snappy-core": "^2.0",
+ "symfony/config": "^5.4|^6.4|^7.1",
+ "symfony/dependency-injection": "^5.4|^6.4|^7.1",
+ "symfony/http-kernel": "^5.4|^6.4|^7.1"
},
"autoload": {
- "psr-4": { "KnpLabs\\Snappy\\Bundle\\": "" },
+ "psr-4": {
+ "KNPLabs\\Snappy\\Bundle\\": ""
+ },
"exclude-from-classmap": [
"/Tests/"
]
},
"autoload-dev": {
"psr-4": {
- "Tests\\KnpLabs\\Snappy\\Bundle\\": "Tests/"
+ "Tests\\KNPLabs\\Snappy\\Bundle\\": "Tests/"
}
},
"require-dev": {
- "phpunit/phpunit": "<7.5|^9.6"
+ "phpunit/phpunit": "^11.4"
}
}
diff --git a/src/Bundle/phpunit.xml.dist b/src/Framework/Symfony/phpunit.xml.dist
similarity index 100%
rename from src/Bundle/phpunit.xml.dist
rename to src/Framework/Symfony/phpunit.xml.dist