From 051176bb7b551fee5f8098a8db5216a930094b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Oct 2025 17:40:28 +0200 Subject: [PATCH 1/2] Support all streaming flush modes for `Compressor` --- src/Compressor.php | 6 +++--- tests/CompressorTest.php | 7 +++++++ tests/GzipCompressorTest.php | 18 +++++++++--------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Compressor.php b/src/Compressor.php index faa3150..0d7dcd5 100644 --- a/src/Compressor.php +++ b/src/Compressor.php @@ -40,7 +40,7 @@ final class Compressor extends TransformStream /** * @param int $encoding ZLIB_ENCODING_GZIP, ZLIB_ENCODING_RAW or ZLIB_ENCODING_DEFLATE * @param int $level optional compression level - * @param int $flush optional flush mode (ZLIB_NO_FLUSH, ZLIB_SYNC_FLUSH, ZLIB_FULL_FLUSH, ZLIB_FINISH) + * @param int $flush optional flush mode (ZLIB_NO_FLUSH, ZLIB_PARTIAL_FLUSH, ZLIB_SYNC_FLUSH, ZLIB_FULL_FLUSH) */ public function __construct($encoding, $level = -1, int $flush = ZLIB_NO_FLUSH) { @@ -65,8 +65,8 @@ public function __construct($encoding, $level = -1, int $flush = ZLIB_NO_FLUSH) throw new \InvalidArgumentException('Unable to initialize compressor' . $errstr); // @codeCoverageIgnore } - if (!in_array($flush, [ZLIB_NO_FLUSH, ZLIB_SYNC_FLUSH, ZLIB_FULL_FLUSH, ZLIB_FINISH], true)) { - throw new \InvalidArgumentException('Argument #3 ($flush) must be one of ZLIB_NO_FLUSH, ZLIB_SYNC_FLUSH, ZLIB_FULL_FLUSH or ZLIB_FINISH'); + if (!in_array($flush, [ZLIB_NO_FLUSH, ZLIB_PARTIAL_FLUSH, ZLIB_SYNC_FLUSH, ZLIB_FULL_FLUSH], true)) { + throw new \InvalidArgumentException('Argument #3 ($flush) must be one of ZLIB_NO_FLUSH, ZLIB_PARTIAL_FLUSH, ZLIB_SYNC_FLUSH or ZLIB_FULL_FLUSH'); } $this->context = $context; diff --git a/tests/CompressorTest.php b/tests/CompressorTest.php index 000942c..10a9ed5 100644 --- a/tests/CompressorTest.php +++ b/tests/CompressorTest.php @@ -34,6 +34,13 @@ public function testCtorThrowsForInvalidEncodingAndUnsetsUsedErrorHandler() public function testCtorThrowsForInvalidFlushMode() { $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Argument #3 ($flush) must be one of ZLIB_NO_FLUSH, ZLIB_PARTIAL_FLUSH, ZLIB_SYNC_FLUSH or ZLIB_FULL_FLUSH'); new Compressor(ZLIB_ENCODING_GZIP, -1, -1); } + + public function testCtorThrowsForFinishFlushMode() + { + $this->expectException(\InvalidArgumentException::class); + new Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_FINISH); + } } diff --git a/tests/GzipCompressorTest.php b/tests/GzipCompressorTest.php index 43646e4..0e61cda 100644 --- a/tests/GzipCompressorTest.php +++ b/tests/GzipCompressorTest.php @@ -67,30 +67,30 @@ public function testWriteWillOnlyFlushHeaderByDefaultToBufferDataBeforeFlushing( $compressor->write('hello'); } - public function testWriteWithSyncFlushWillFlushHeaderWithFirstChunkImmediately() + public function testWriteWithPartialFlushWillFlushHeaderWithFirstChunkImmediately() { - $compressor = new Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_SYNC_FLUSH); + $compressor = new Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_PARTIAL_FLUSH); - $compressor->on('data', $this->expectCallableOnceWith("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00" . $this->os . "\xca\x48\xcd\xc9\xc9\x07\x00\x00\x00\xff\xff")); + $compressor->on('data', $this->expectCallableOnceWith("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00" . $this->os . "\xca\x48\xcd\xc9\xc9\x07\x08")); $compressor->write('hello'); } - public function testWriteWithFinishFlushWillFlushEntireGzipHeaderAndFooterWithFirstChunkImmediately() + public function testWriteWithSyncFlushWillFlushHeaderWithFirstChunkImmediately() { - $compressor = new Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_FINISH); + $compressor = new Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_SYNC_FLUSH); - $compressor->on('data', $this->expectCallableOnceWith("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00" . $this->os . "\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36" . "\x05\x00\x00\x00")); + $compressor->on('data', $this->expectCallableOnceWith("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00" . $this->os . "\xca\x48\xcd\xc9\xc9\x07\x00\x00\x00\xff\xff")); $compressor->write('hello'); } - public function testWriteAfterFinishFlushWillFlushEntireGzipWithSyncFlushWillFlushEntireGzipHeaderAndFooterAgainImmediately() + public function testWriteAfterWriteWithFullFlushWillFlushChunkIndependentOfPreviousChunk() { - $compressor = new Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_FINISH); + $compressor = new Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_FULL_FLUSH); $compressor->write('hello'); - $compressor->on('data', $this->expectCallableOnceWith("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00" . $this->os . "\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36" . "\x05\x00\x00\x00")); + $compressor->on('data', $this->expectCallableOnceWith("\xca\x48\xcd\xc9\xc9\x07\x00\x00\x00\xff\xff")); $compressor->write('hello'); } From db83fb5f030b00707bc52ab42f74877868add741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 10 Oct 2026 00:45:55 +0200 Subject: [PATCH 2/2] Document `$level` and `$flush` parameters for `Compressor` --- README.md | 19 +++++++++++++++++++ src/Compressor.php | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/README.md b/README.md index 17d7273..e414cdc 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,25 @@ This is particularly useful in a piping context: $input->pipe($filterBadWords)->pipe($compressor)->pipe($output); ``` +This class takes an optional `int $level` parameter that controls the +compression level from `0` (no compression) to `9` (best compression). +It defaults to `-1` which uses zlib's default compression level. + +This class takes an optional `int $flush` parameter that controls when +compressed data will be emitted. It defaults to `ZLIB_NO_FLUSH` which +buffers data internally to achieve the best compression ratio, so compressed +data is only emitted once enough data is available or the stream ends. +For streaming protocols like EventSource (SSE), you can use `ZLIB_SYNC_FLUSH` +to make sure each chunk can be decompressed by the receiving side immediately +after writing. Each flush adds a few bytes, so this may produce larger output +when writing many small chunks. This parameter also accepts `ZLIB_PARTIAL_FLUSH` +and `ZLIB_FULL_FLUSH`, the latter also resets the compression state for each +chunk, which may considerably increase output size. + +```php +$compressor = new Clue\React\Zlib\Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_SYNC_FLUSH); +``` + For more details, see ReactPHP's [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). diff --git a/src/Compressor.php b/src/Compressor.php index 0d7dcd5..504775c 100644 --- a/src/Compressor.php +++ b/src/Compressor.php @@ -26,6 +26,25 @@ * $input->pipe($filterBadWords)->pipe($compressor)->pipe($output); * ``` * + * This class takes an optional `int $level` parameter that controls the + * compression level from `0` (no compression) to `9` (best compression). + * It defaults to `-1` which uses zlib's default compression level. + * + * This class takes an optional `int $flush` parameter that controls when + * compressed data will be emitted. It defaults to `ZLIB_NO_FLUSH` which + * buffers data internally to achieve the best compression ratio, so compressed + * data is only emitted once enough data is available or the stream ends. + * For streaming protocols like EventSource (SSE), you can use `ZLIB_SYNC_FLUSH` + * to make sure each chunk can be decompressed by the receiving side immediately + * after writing. Each flush adds a few bytes, so this may produce larger output + * when writing many small chunks. This parameter also accepts `ZLIB_PARTIAL_FLUSH` + * and `ZLIB_FULL_FLUSH`, the latter also resets the compression state for each + * chunk, which may considerably increase output size. + * + * ```php + * $compressor = new Clue\React\Zlib\Compressor(ZLIB_ENCODING_GZIP, -1, ZLIB_SYNC_FLUSH); + * ``` + * * For more details, see ReactPHP's * [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). */