wyrihaximus/compress
wyrihaximus/compress is a small PHP library offering a simple, consistent API to compress and decompress strings and streams using multiple algorithms such as gzip and bzip2, making it easy to add compression to your Laravel or PHP application.
wyrihaximus/compress is a small, framework-agnostic package: a marker/contract interface plus a handful of concrete compressor classes that wrap PHP's built-in compression extensions (zlib, bz2, lzf, etc.). There is no service provider, no config file, and no facade — you wire it into Laravel yourself. That is the whole point of it: a typed, injectable seam over gzencode()/gzdecode() so you are not scattering raw function calls through your codebase.
composer require wyrihaximus/compress
Look at these first:
src/ — the interface (a CompressorInterface-style contract with a compress()/decompress() pair) and the concrete classes, one per algorithm. The interface is deliberately tiny; confirm the exact method names in your pinned version before writing call sites.composer.json — check the ext-* requirements. Each compressor class likely requires its matching PHP extension, which means ext-bz2 and friends are not necessarily installed in your runtime.README.md — usually a two-line example per class; there is no deeper documentation.First useful use case: compressing a JSON payload before it goes into Redis or a queue.
use WyriHaximus\Compress\CompressorInterface; // verify exact FQCN in src/
$compressor = app(CompressorInterface::class); // bind this yourself, see below
$payload = json_encode($order->toArray(), JSON_THROW_ON_ERROR);
$stored = $compressor->compress($payload); // binary string
$restored = json_decode($compressor->decompress($stored), true, flags: JSON_THROW_ON_ERROR);
Bind the interface once in AppServiceProvider::register():
$this->app->singleton(CompressorInterface::class, fn () => new GzipCompressor());
Do this on day one. Everything else in this assessment assumes you inject the interface, not concrete classes.
Config-driven algorithm selection. Keep the binding swappable so you can move from gzip to bz2 (or to a no-op in tests) without touching call sites.
// config/compress.php
return ['driver' => env('COMPRESS_DRIVER', 'gzip')];
// AppServiceProvider
$this->app->singleton(CompressorInterface::class, function ($app) {
return match (config('compress.driver')) {
'gzip' => new GzipCompressor(),
'bz2' => new Bzip2Compressor(),
'none' => new NoopCompressor(), // write this yourself in ~5 lines
default => throw new InvalidArgumentException('Unknown compressor'),
};
});
Queue jobs: compress the payload, not the serialized job. Laravel already serializes job properties, so compressing the whole job body fights the framework. Compress the domain payload inside the job and decompress in handle().
class ImportRows implements ShouldQueue
{
public function __construct(private string $compressedCsv) {}
public static function fromArray(array $rows): self
{
return new self(app(CompressorInterface::class)->compress(json_encode($rows)));
}
public function handle(CompressorInterface $compressor): void
{
$rows = json_decode($compressor->decompress($this->compressedCsv), true, flags: JSON_THROW_ON_ERROR);
// ...
}
}
This is where the package genuinely earns its keep: queue payloads in Redis/SQS have hard size caps, and CSV/JSON batches compress 5–10x.
Cache decorator. Wrap Illuminate\Contracts\Cache\Repository so compression is a policy decision, not something every Cache::put() call has to remember.
final class CompressingCacheRepository implements Repository
{
public function __construct(
private Repository $inner,
private CompressorInterface $compressor,
) {}
public function put($key, $value, $ttl = null): bool
{
$encoded = serialize($value);
return $this->inner->put($key, $this->compressor->compress($encoded), $ttl);
}
public function get($key, $default = null): mixed
{
$raw = $this->inner->get($key);
return $raw === null
? $default
: unserialize($this->compressor->decompress($raw));
}
// delegate the rest...
}
Bind this only for the specific stores holding large values — not globally. Note this conflicts with the cache.serializable_classes hardening in Laravel 11+; prefer json_encode if you care about that.
Eloquent attribute cast for blobs. Store compressed binary in a LONGBLOB/BYTEA column and hide the mechanics in a cast. Avoid text columns — they will mangle binary on some MySQL charsets (see Gotchas).
final class CompressedJson implements CastsAttributes
{
public function get($model, $key, $value, $attributes): ?array
{
return $value === null ? null : json_decode(
app(CompressorInterface::class)->decompress($value), true, flags: JSON_THROW_ON_ERROR
);
}
public function set($model, $key, $value, $attributes): string
{
return app(CompressorInterface::class)->compress(json_encode($value, JSON_THROW_ON_ERROR));
}
}
Round-trip test helper. Since the interface has exactly two methods, a shared test trait is cheap insurance whenever you swap drivers:
public function test_round_trip(): void
{
$c = app(CompressorInterface::class);
$input = str_repeat('laravel ', 1000);
$this->assertSame($input, $c->decompress($c->compress($input)));
}
gzcompress vs gzencode are not interchangeable. One emits raw zlib, the other gzip framing. If you switch PHP versions or fork a class, compressed blobs written by the old code will fail to decompress with gzdecode: data error. Whatever you persist must carry an algorithm/format tag.
Always store which compressor produced the bytes. The marker interface erases that information. Put a one-byte header (or a compression column) next to the payload before you ship this to production — otherwise changing COMPRESS_DRIVER silently corrupts every cached value.
Binary safety. If the bytes land in a text/varchar column, a JSON column, an HTTP header, or a URL, base64 them (base64_encode) and accept the 33% overhead. Only skip this for true blob columns and file storage.
Check the extension exists at boot, not at request time. extension_loaded('bz2') in the service provider with a clear exception beats a fatal Call to undefined function bzcompress() at 3am. Docker images built from php:8.x-fpm often ship zlib but not bz2 or lzf.
Do not double-compress. Redis and several Laravel cache drivers, plus your web server/CDN and Laravel's CompressResponse middleware, already compress. Nested gzip is wasted CPU and can increase size below ~200 bytes.
Small payloads cost more than they save. zlib has ~18 bytes of header/footer plus a dictionary setup. Compress only above a threshold (1–4 KB) and skip below it — the branching logic belongs in a small decorator, not at every call site.
Corruption is not exceptional in a cache layer. gzdecode() returns false and emits a warning on bad data. Wrap decompression, catch false/\Throwable, and treat it as a cache miss rather than a 500.
Memory doubles. Both PHP's zlib calls and these wrappers build the full output string in memory. Compressing a 200 MB CSV in a web request will hit memory_limit. Stream and chunk it (or use gzopen/gzwrite on temp files) for anything large.
Compression is not encryption. Highly compressible data + compression length is a real CRIME/BREACH-style side channel, and none of this hides a secret from anyone with the blob. Encrypt first, then compress.
Low adoption (7 stars, 0 dependents) means low bus factor. It is a few hundred lines of thin wrappers — read them, and if a dependency ever becomes a liability, vendor the two classes you use into app/Support/. That is cheaper than the alternative of a stuck transitive constraint.
Extension point: implementing CompressorInterface yourself for zstd/lz4/zlib-level tuning is trivial and is the intended way to extend the package. Keep your implementations final, stateless, and constructor-injected so they behave as singletons.
How can I help you explore Laravel packages today?