diff --git a/composer.json b/composer.json index 61a4a639..ba8bce16 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "league/flysystem-bundle": "^3.4", "league/flysystem-sftp-v3": "^3.29", "league/oauth2-server-bundle": "^1.0", - "nelexa/zip": "^4.0", + "maennchen/zipstream-php": "^3.2", "nesbot/carbon": "^3.8", "phpdocumentor/reflection-docblock": "^5.6", "phpoffice/phpspreadsheet": "^5.4", diff --git a/composer.lock b/composer.lock index d4c5aefe..36421769 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b4db6b0bb348c8ec765a75a409121ce1", + "content-hash": "2c2a8c19ca2e117f521584a4e3e6212c", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -3297,79 +3297,6 @@ ], "time": "2026-09-29T19:21:12+00:00" }, - { - "name": "nelexa/zip", - "version": "4.0.2", - "source": { - "type": "git", - "url": "https://github.com/Ne-Lexa/php-zip.git", - "reference": "88a1b6549be813278ff2dd3b6b2ac188827634a7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Ne-Lexa/php-zip/zipball/88a1b6549be813278ff2dd3b6b2ac188827634a7", - "reference": "88a1b6549be813278ff2dd3b6b2ac188827634a7", - "shasum": "" - }, - "require": { - "ext-zlib": "*", - "php": "^7.4 || ^8.0", - "psr/http-message": "*", - "symfony/finder": "*" - }, - "require-dev": { - "ext-bz2": "*", - "ext-dom": "*", - "ext-fileinfo": "*", - "ext-iconv": "*", - "ext-openssl": "*", - "ext-xml": "*", - "friendsofphp/php-cs-fixer": "^3.4.0", - "guzzlehttp/psr7": "^1.6", - "phpunit/phpunit": "^9", - "symfony/http-foundation": "*", - "symfony/var-dumper": "*", - "vimeo/psalm": "^4.6" - }, - "suggest": { - "ext-bz2": "Needed to support BZIP2 compression", - "ext-fileinfo": "Needed to get mime-type file", - "ext-iconv": "Needed to support convert zip entry name to requested character encoding", - "ext-openssl": "Needed to support encrypt zip entries or use ext-mcrypt" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpZip\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ne-Lexa", - "email": "alexey@nelexa.ru", - "role": "Developer" - } - ], - "description": "PhpZip is a php-library for extended work with ZIP-archives. Open, create, update, delete, extract and get info tool. Supports appending to existing ZIP files, WinZip AES encryption, Traditional PKWARE Encryption, BZIP2 compression, external file attributes and ZIP64 extensions. Alternative ZipArchive. It does not require php-zip extension.", - "homepage": "https://github.com/Ne-Lexa/php-zip", - "keywords": [ - "archive", - "extract", - "unzip", - "winzip", - "zip", - "ziparchive" - ], - "support": { - "issues": "https://github.com/Ne-Lexa/php-zip/issues", - "source": "https://github.com/Ne-Lexa/php-zip/tree/4.0.2" - }, - "time": "2022-06-17T11:17:46+00:00" - }, { "name": "nesbot/carbon", "version": "3.14.1", diff --git a/src/BusProNet/XmlParser/DocumentsParser.php b/src/BusProNet/XmlParser/DocumentsParser.php index 3ee02716..dc996176 100644 --- a/src/BusProNet/XmlParser/DocumentsParser.php +++ b/src/BusProNet/XmlParser/DocumentsParser.php @@ -3,9 +3,9 @@ namespace App\BusProNet\XmlParser; use App\BusProNet\Model\File; -use PhpZip\Exception\ZipException; -use PhpZip\ZipFile; use Symfony\Component\DomCrawler\Crawler; +use ZipStream\Exception as ZipException; +use ZipStream\ZipStream; class DocumentsParser { @@ -28,17 +28,28 @@ class DocumentsParser } // In case of multiple documents create ZIP file + $stream = fopen('php://temp', 'w+b'); try { - $zip = new ZipFile(); + // Zero headers trip up some unzip clients, and the data is in memory anyway + $zip = new ZipStream( + outputStream: $stream, + defaultEnableZeroHeader: false, + sendHttpHeaders: false, + ); - $documents->each(function (Crawler $node) use (&$zip) { + $documents->each(function (Crawler $node) use ($zip) { [$filename, $pdfData] = $this->getFileInfo($node); - $zip->addFromString($filename.'.pdf', $pdfData); + $zip->addFile($filename.'.pdf', $pdfData); }); - return new File('reisedokumente.zip', $zip->outputAsString(), 'application/zip'); + $zip->finish(); + rewind($stream); + + return new File('reisedokumente.zip', stream_get_contents($stream), 'application/zip'); } catch (ZipException $e) { return null; + } finally { + fclose($stream); } } diff --git a/tests/BusProNet/XmlParser/DocumentsParserTest.php b/tests/BusProNet/XmlParser/DocumentsParserTest.php new file mode 100644 index 00000000..03bd3454 --- /dev/null +++ b/tests/BusProNet/XmlParser/DocumentsParserTest.php @@ -0,0 +1,68 @@ +parseDocuments($this->response(['Rechnung' => 'pdf-one'])); + + self::assertNotNull($file); + self::assertSame('Rechnung.pdf', $file->filename); + self::assertSame('application/pdf', $file->mimeType); + self::assertSame('pdf-one', $file->content); + } + + public function testMultipleDocumentsAreBundledIntoZip(): void + { + $file = new DocumentsParser()->parseDocuments($this->response([ + 'Rechnung' => 'pdf-one', + 'Reiseplan' => 'pdf-two', + ])); + + self::assertNotNull($file); + self::assertSame('reisedokumente.zip', $file->filename); + self::assertSame('application/zip', $file->mimeType); + + $path = tempnam(sys_get_temp_dir(), 'docs'); + try { + file_put_contents($path, $file->content); + $zip = new \ZipArchive(); + self::assertTrue($zip->open($path, \ZipArchive::CHECKCONS)); + self::assertSame(2, $zip->numFiles); + self::assertSame('pdf-one', $zip->getFromName('Rechnung.pdf')); + self::assertSame('pdf-two', $zip->getFromName('Reiseplan.pdf')); + $zip->close(); + } finally { + unlink($path); + } + } + + /** + * @param array $documents filename => PDF bytes + */ + private function response(array $documents): Crawler + { + $xml = ''; + foreach ($documents as $name => $pdf) { + $xml .= sprintf( + '%sdata:application/pdf;base64,%s', + $name, + base64_encode($pdf), + ); + } + $xml .= ''; + + $crawler = new Crawler(); + $crawler->addXmlContent($xml); + + return $crawler; + } +}