From a11c6367b498adbe4d31a2dbc35dbbcbdee4cb09 Mon Sep 17 00:00:00 2001 From: Ne-Lexa Date: Wed, 24 Feb 2021 15:37:11 +0300 Subject: [PATCH] added method `outputAsSymfonyResponse`, rename method `outputAsResponse` to `outputAsPsr7Response` --- .phpstorm.meta.php | 6 ++ README.RU.md | 56 ++++++++--- README.md | 48 +++++++++- composer.json | 3 +- src/ZipFile.php | 215 +++++++++++++++++++++++++++++++----------- tests/ZipFileTest.php | 23 ++++- 6 files changed, 273 insertions(+), 78 deletions(-) diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 239b859..8580b63 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -65,6 +65,12 @@ namespace PHPSTORM_META { expectedArguments(\PhpZip\ZipFile::outputAsResponse(), 2, argumentsSet("zip_mime_types")); expectedArguments(\PhpZip\ZipFile::outputAsResponse(), 3, argumentsSet("bool")); + expectedArguments(\PhpZip\ZipFile::outputAsPsr7Response(), 2, argumentsSet("zip_mime_types")); + expectedArguments(\PhpZip\ZipFile::outputAsPsr7Response(), 3, argumentsSet("bool")); + + expectedArguments(\PhpZip\ZipFile::outputAsSymfonyResponse(), 1, argumentsSet("zip_mime_types")); + expectedArguments(\PhpZip\ZipFile::outputAsSymfonyResponse(), 2, argumentsSet("bool")); + registerArgumentsSet( 'dos_charset', \PhpZip\Constants\DosCodePage::CP_LATIN_US, diff --git a/README.RU.md b/README.RU.md index 0de0a3d..54fe76e 100644 --- a/README.RU.md +++ b/README.RU.md @@ -144,7 +144,8 @@ finally{ - [ZipFile::openFromString](#zipfileopenfromstring) - открывает ZIP-архив из строки. - [ZipFile::openFromStream](#zipfileopenfromstream) - открывает ZIP-архив из потока. - [ZipFile::outputAsAttachment](#zipfileoutputasattachment) - выводит ZIP-архив в браузер. -- [ZipFile::outputAsResponse](#zipfileoutputasresponse) - выводит ZIP-архив, как Response PSR-7. +- [ZipFile::outputAsPsr7Response](#zipfileoutputaspsr7response) - выводит ZIP-архив, как PSR-7 Response. +- [ZipFile::outputAsSymfonyResponse](#zipfileoutputassymfonyresponse) - выводит ZIP-архив, как Symfony Response. - [ZipFile::outputAsString](#zipfileoutputasstring) - выводит ZIP-архив в виде строки. - [ZipFile::rename](#zipfilerename) - переименовывает запись по имени. - [ZipFile::rewrite](#zipfilerewrite) - сохраняет изменения и заново открывает изменившийся архив. @@ -753,28 +754,57 @@ $zipFile->outputAsAttachment($outputFilename); $mimeType = 'application/zip'; $zipFile->outputAsAttachment($outputFilename, $mimeType); ``` -##### ZipFile::outputAsResponse -Выводит ZIP-архив, как Response [PSR-7](http://www.php-fig.org/psr/psr-7/). +##### ZipFile::outputAsPsr7Response +Выводит ZIP-архив, как [PSR-7 Response](http://www.php-fig.org/psr/psr-7/). Метод вывода может использоваться в любом PSR-7 совместимом фреймворке. ```php // $response = ....; // instance Psr\Http\Message\ResponseInterface -$zipFile->outputAsResponse($response, $outputFilename); +$zipFile->outputAsPsr7Response($response, $outputFilename); ``` Можно установить MIME-тип: ```php $mimeType = 'application/zip'; -$zipFile->outputAsResponse($response, $outputFilename, $mimeType); +$zipFile->outputAsPsr7Response($response, $outputFilename, $mimeType); ``` -Пример для Slim Framework: +##### ZipFile::outputAsSymfonyResponse +Выводит ZIP-архив, как [Symfony Response](https://symfony.com/doc/current/components/http_foundation.html#response). + +Метод вывода можно использовать в фреймворке Symfony. ```php -$app = new \Slim\App; -$app->get('/download', function ($req, $res, $args) { - $zipFile = new \PhpZip\ZipFile(); - $zipFile['file.txt'] = 'content'; - return $zipFile->outputAsResponse($res, 'file.zip'); -}); -$app->run(); +$response = $zipFile->outputAsSymfonyResponse($outputFilename); +``` +Вы можете установить Mime-Type: +```php +$mimeType = 'application/zip'; +$response = $zipFile->outputAsSymfonyResponse($outputFilename, $mimeType); +``` +Пример использования в Symfony Controller: +```php +outputAsSymfonyResponse($outputFilename); + } +} ``` ##### ZipFile::rewrite Сохраняет изменения и заново открывает изменившийся архив. diff --git a/README.md b/README.md index 64a89c0..7bee1b6 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,8 @@ Other examples can be found in the `tests/` folder - [ZipFile::openFromString](#zipfileopenfromstring) - opens a zip-archive from a string. - [ZipFile::openFromStream](#zipfileopenfromstream) - opens a zip-archive from the stream. - [ZipFile::outputAsAttachment](#zipfileoutputasattachment) - outputs a ZIP-archive to the browser. -- [ZipFile::outputAsResponse](#zipfileoutputasresponse) - outputs a ZIP-archive as PSR-7 Response. +- [ZipFile::outputAsPsr7Response](#zipfileoutputaspsr7response) - outputs a ZIP-archive as PSR-7 Response. +- [ZipFile::outputAsSymfonyResponse](#zipfileoutputaspsr7response) - outputs a ZIP-archive as Symfony Response. - [ZipFile::outputAsString](#zipfileoutputasstring) - outputs a ZIP-archive as string. - [ZipFile::rename](#zipfilerename) - renames an entry defined by its name. - [ZipFile::rewrite](#zipfilerewrite) - save changes and re-open the changed archive. @@ -782,18 +783,57 @@ You can set the Mime-Type: $mimeType = 'application/zip'; $zipFile->outputAsAttachment($outputFilename, $mimeType); ``` -##### ZipFile::outputAsResponse +##### ZipFile::outputAsPsr7Response Outputs a ZIP-archive as [PSR-7 Response](http://www.php-fig.org/psr/psr-7/). The output method can be used in any PSR-7 compatible framework. ```php // $response = ....; // instance Psr\Http\Message\ResponseInterface -$zipFile->outputAsResponse($response, $outputFilename); +$zipFile->outputAsPsr7Response($response, $outputFilename); ``` You can set the Mime-Type: ```php $mimeType = 'application/zip'; -$zipFile->outputAsResponse($response, $outputFilename, $mimeType); +$zipFile->outputAsPsr7Response($response, $outputFilename, $mimeType); +``` +##### ZipFile::outputAsSymfonyResponse +Outputs a ZIP-archive as [Symfony Response](https://symfony.com/doc/current/components/http_foundation.html#response). + +The output method can be used in Symfony framework. +```php +$response = $zipFile->outputAsSymfonyResponse($outputFilename); +``` +You can set the Mime-Type: +```php +$mimeType = 'application/zip'; +$response = $zipFile->outputAsSymfonyResponse($outputFilename, $mimeType); +``` +Example use in Symfony Controller: +```php +outputAsSymfonyResponse($outputFilename); + } +} ``` ##### ZipFile::rewrite Save changes and re-open the changed archive. diff --git a/composer.json b/composer.json index 758f45a..aacb944 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "phpunit/phpunit": "^9", "symfony/var-dumper": "^5.0", "friendsofphp/php-cs-fixer": "^2.18", - "vimeo/psalm": "^4.6" + "vimeo/psalm": "^4.6", + "symfony/http-foundation": "^5.2" }, "autoload": { "psr-4": { diff --git a/src/ZipFile.php b/src/ZipFile.php index fba3779..f976f57 100644 --- a/src/ZipFile.php +++ b/src/ZipFile.php @@ -35,6 +35,8 @@ use PhpZip\Util\StringUtil; use Psr\Http\Message\ResponseInterface; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * Create, open .ZIP files, modify, get info and extract files. @@ -239,8 +241,8 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * * @param ?string $comment * - * @throws ZipException * @throws ZipEntryNotFoundException + * @throws ZipException * * @return ZipFile */ @@ -269,8 +271,8 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator } /** - * @throws ZipException * @throws ZipEntryNotFoundException + * @throws ZipException * * @return resource */ @@ -313,8 +315,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * * @return ZipFile */ - public function extractTo(string $destDir, $entries = null, array $options = [], ?array &$extractedEntries = []): self - { + public function extractTo( + string $destDir, + $entries = null, + array $options = [], + ?array &$extractedEntries = [] + ): self { if (!file_exists($destDir)) { throw new ZipException(sprintf('Destination %s not found', $destDir)); } @@ -942,8 +948,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @return ZipFile * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ - public function addFilesFromGlob(string $inputDir, string $globPattern, string $localPath = '/', ?int $compressionMethod = null): self - { + public function addFilesFromGlob( + string $inputDir, + string $globPattern, + string $localPath = '/', + ?int $compressionMethod = null + ): self { return $this->addGlob($inputDir, $globPattern, $localPath, false, $compressionMethod); } @@ -1016,8 +1026,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @return ZipFile * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ - public function addFilesFromGlobRecursive(string $inputDir, string $globPattern, string $localPath = '/', ?int $compressionMethod = null): self - { + public function addFilesFromGlobRecursive( + string $inputDir, + string $globPattern, + string $localPath = '/', + ?int $compressionMethod = null + ): self { return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); } @@ -1039,8 +1053,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * * @internal param bool $recursive Recursive search */ - public function addFilesFromRegex(string $inputDir, string $regexPattern, string $localPath = '/', ?int $compressionMethod = null): self - { + public function addFilesFromRegex( + string $inputDir, + string $regexPattern, + string $localPath = '/', + ?int $compressionMethod = null + ): self { return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod); } @@ -1097,8 +1115,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * * @throws ZipException */ - private function doAddFiles(string $fileSystemDir, array $files, string $zipPath, ?int $compressionMethod = null): void - { + private function doAddFiles( + string $fileSystemDir, + array $files, + string $zipPath, + ?int $compressionMethod = null + ): void { $fileSystemDir = rtrim($fileSystemDir, '/\\') . \DIRECTORY_SEPARATOR; if (!empty($zipPath)) { @@ -1140,8 +1162,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * * @internal param bool $recursive Recursive search */ - public function addFilesFromRegexRecursive(string $inputDir, string $regexPattern, string $localPath = '/', ?int $compressionMethod = null): self - { + public function addFilesFromRegexRecursive( + string $inputDir, + string $regexPattern, + string $localPath = '/', + ?int $compressionMethod = null + ): self { return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); } @@ -1530,10 +1556,35 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function outputAsAttachment(string $outputFilename, ?string $mimeType = null, bool $attachment = true): void { - if ($mimeType === null) { - $mimeType = $this->getMimeTypeByFilename($outputFilename); + [ + 'resource' => $resource, + 'headers' => $headers, + ] = $this->getOutputData($outputFilename, $mimeType, $attachment); + + if (!headers_sent()) { + foreach ($headers as $key => $value) { + header($key . ': ' . $value); + } } + rewind($resource); + + try { + echo stream_get_contents($resource, -1, 0); + } finally { + fclose($resource); + } + } + + /** + * @param ?string $mimeType + * + * @throws ZipException + */ + private function getOutputData(string $outputFilename, ?string $mimeType = null, bool $attachment = true): array + { + $mimeType ??= $this->getMimeTypeByFilename($outputFilename); + if (!($handle = fopen('php://temp', 'w+b'))) { throw new InvalidArgumentException('php://temp cannot open for write.'); } @@ -1542,23 +1593,21 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $size = fstat($handle)['size']; - $headerContentDisposition = 'Content-Disposition: ' . ($attachment ? 'attachment' : 'inline'); + $contentDisposition = $attachment ? 'attachment' : 'inline'; + $name = basename($outputFilename); - if (!empty($outputFilename)) { - $headerContentDisposition .= '; filename="' . basename($outputFilename) . '"'; + if (!empty($name)) { + $contentDisposition .= '; filename="' . $name . '"'; } - header($headerContentDisposition); - header('Content-Type: ' . $mimeType); - header('Content-Length: ' . $size); - - rewind($handle); - - try { - echo stream_get_contents($handle, -1, 0); - } finally { - fclose($handle); - } + return [ + 'resource' => $handle, + 'headers' => [ + 'Content-Disposition' => $contentDisposition, + 'Content-Type' => $mimeType, + 'Content-Length' => $size, + ], + ]; } protected function getMimeTypeByFilename(string $outputFilename): string @@ -1581,39 +1630,93 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline * * @throws ZipException + * + * @deprecated deprecated since version 2.0, replace to {@see ZipFile::outputAsPsr7Response} */ - public function outputAsResponse(ResponseInterface $response, string $outputFilename, ?string $mimeType = null, bool $attachment = true): ResponseInterface - { - if ($mimeType === null) { - $mimeType = $this->getMimeTypeByFilename($outputFilename); - } + public function outputAsResponse( + ResponseInterface $response, + string $outputFilename, + ?string $mimeType = null, + bool $attachment = true + ): ResponseInterface { + @trigger_error( + sprintf( + 'Method %s is deprecated. Replace to %s::%s', + __METHOD__, + __CLASS__, + 'outputAsPsr7Response' + ), + \E_USER_DEPRECATED + ); - if (!($handle = fopen('php://temp', 'w+b'))) { - throw new InvalidArgumentException('php://temp cannot open for write.'); - } - $this->writeZipToStream($handle); - $this->close(); - rewind($handle); + return $this->outputAsPsr7Response($response, $outputFilename, $mimeType, $attachment); + } - $contentDispositionValue = ($attachment ? 'attachment' : 'inline'); + /** + * Output .ZIP archive as PSR-7 Response. + * + * @param ResponseInterface $response Instance PSR-7 Response + * @param string $outputFilename Output filename + * @param string|null $mimeType Mime-Type + * @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline + * + * @throws ZipException + * + * @since 4.0.0 + */ + public function outputAsPsr7Response( + ResponseInterface $response, + string $outputFilename, + ?string $mimeType = null, + bool $attachment = true + ): ResponseInterface { + [ + 'resource' => $resource, + 'headers' => $headers, + ] = $this->getOutputData($outputFilename, $mimeType, $attachment); - if (!empty($outputFilename)) { - $contentDispositionValue .= '; filename="' . basename($outputFilename) . '"'; - } - - $stream = new ResponseStream($handle); - $size = $stream->getSize(); - - if ($size !== null) { + foreach ($headers as $key => $value) { /** @noinspection CallableParameterUseCaseInTypeContextInspection */ - $response = $response->withHeader('Content-Length', (string) $size); + $response = $response->withHeader($key, (string) $value); } - return $response - ->withHeader('Content-Type', $mimeType) - ->withHeader('Content-Disposition', $contentDispositionValue) - ->withBody($stream) - ; + return $response->withBody(new ResponseStream($resource)); + } + + /** + * Output .ZIP archive as Symfony Response. + * + * @param string $outputFilename Output filename + * @param string|null $mimeType Mime-Type + * @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline + * + * @throws ZipException + * + * @since 4.0.0 + */ + public function outputAsSymfonyResponse( + string $outputFilename, + ?string $mimeType = null, + bool $attachment = true + ): Response { + [ + 'resource' => $resource, + 'headers' => $headers, + ] = $this->getOutputData($outputFilename, $mimeType, $attachment); + + return new StreamedResponse( + static function () use ($resource): void { + if (!($output = fopen('php://output', 'w+b'))) { + throw new InvalidArgumentException('php://output cannot open for write.'); + } + rewind($resource); + stream_copy_to_stream($resource, $output); + fclose($output); + fclose($resource); + }, + 200, + $headers + ); } /** diff --git a/tests/ZipFileTest.php b/tests/ZipFileTest.php index 9f71529..c987a03 100644 --- a/tests/ZipFileTest.php +++ b/tests/ZipFileTest.php @@ -1856,7 +1856,7 @@ class ZipFileTest extends ZipTestCase public function testFilename0(): void { $zipFile = new ZipFile(); - $zipFile[0] = 0; + $zipFile[0] = '0'; static::assertTrue(isset($zipFile['0'])); static::assertCount(1, $zipFile); $zipFile @@ -1891,18 +1891,33 @@ class ZipFileTest extends ZipTestCase /** * @throws ZipException */ - public function testPsrResponse(): void + public function testOutputAsPsr7Response(): void { $zipFile = new ZipFile(); for ($i = 0; $i < 10; $i++) { - $zipFile[$i] = $i; + $zipFile[$i] = (string) $i; } $filename = 'file.jar'; - $response = $zipFile->outputAsResponse(new Response(), $filename); + $response = $zipFile->outputAsPsr7Response(new Response(), $filename); static::assertSame('application/java-archive', $response->getHeaderLine('content-type')); static::assertSame('attachment; filename="file.jar"', $response->getHeaderLine('content-disposition')); } + /** + * @throws ZipException + */ + public function testOutputAsSymfonyResponse(): void + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile[$i] = (string) $i; + } + $filename = 'file.jar'; + $response = $zipFile->outputAsSymfonyResponse($filename); + static::assertSame('application/java-archive', $response->headers->get('content-type')); + static::assertSame('attachment; filename="file.jar"', $response->headers->get('content-disposition')); + } + /** * @dataProvider provideCompressionLevels *