Răsfoiți Sursa

added method `outputAsSymfonyResponse`, rename method `outputAsResponse` to `outputAsPsr7Response`

Ne-Lexa 5 ani în urmă
părinte
comite
a11c6367b4
6 a modificat fișierele cu 274 adăugiri și 79 ștergeri
  1. 6 0
      .phpstorm.meta.php
  2. 43 13
      README.RU.md
  3. 44 4
      README.md
  4. 2 1
      composer.json
  5. 160 57
      src/ZipFile.php
  6. 19 4
      tests/ZipFileTest.php

+ 6 - 0
.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,

+ 43 - 13
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
+<?php
+
+namespace App\Controller;
+
+use PhpZip\ZipFile;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Annotation\Route;
+
+class DownloadZipController
+{
+    /**
+     * @Route("/downloads/{id}")
+     *
+     * @throws \PhpZip\Exception\ZipException
+     */
+    public function __invoke(string $id): Response
+    {
+        $zipFile = new ZipFile();
+        $zipFile['file'] = 'contents';
+
+        $outputFilename = $id . '.zip';
+        return $zipFile->outputAsSymfonyResponse($outputFilename);
+    }
+}
 ```
 ##### ZipFile::rewrite
 Сохраняет изменения и заново открывает изменившийся архив.

+ 44 - 4
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
+<?php
+
+namespace App\Controller;
+
+use PhpZip\ZipFile;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Annotation\Route;
+
+class DownloadZipController
+{
+    /**
+     * @Route("/downloads/{id}")
+     *
+     * @throws \PhpZip\Exception\ZipException
+     */
+    public function __invoke(string $id): Response
+    {
+        $zipFile = new ZipFile();
+        $zipFile['file'] = 'contents';
+
+        $outputFilename = $id . '.zip';
+        return $zipFile->outputAsSymfonyResponse($outputFilename);
+    }
+}
 ```
 ##### ZipFile::rewrite
 Save changes and re-open the changed archive.

+ 2 - 1
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": {

+ 160 - 57
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
-     */
-    public function outputAsResponse(ResponseInterface $response, string $outputFilename, ?string $mimeType = null, bool $attachment = true): ResponseInterface
-    {
-        if ($mimeType === null) {
-            $mimeType = $this->getMimeTypeByFilename($outputFilename);
-        }
-
-        if (!($handle = fopen('php://temp', 'w+b'))) {
-            throw new InvalidArgumentException('php://temp cannot open for write.');
-        }
-        $this->writeZipToStream($handle);
-        $this->close();
-        rewind($handle);
-
-        $contentDispositionValue = ($attachment ? 'attachment' : 'inline');
+     *
+     * @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 {
+        @trigger_error(
+            sprintf(
+                'Method %s is deprecated. Replace to %s::%s',
+                __METHOD__,
+                __CLASS__,
+                'outputAsPsr7Response'
+            ),
+            \E_USER_DEPRECATED
+        );
 
-        if (!empty($outputFilename)) {
-            $contentDispositionValue .= '; filename="' . basename($outputFilename) . '"';
-        }
+        return $this->outputAsPsr7Response($response, $outputFilename, $mimeType, $attachment);
+    }
 
-        $stream = new ResponseStream($handle);
-        $size = $stream->getSize();
+    /**
+     * 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 ($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
+        );
     }
 
     /**

+ 19 - 4
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
      *