Skip to content

Refactor - Multiple Compression/Encoding Methods #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [3.0.0] - 2021-07-14

### Added

- Brotli & ZStd Compression are now support when the correspoding php extensions are avaiable

### Changed

- Generic CompressEncoder middleware with with pluggable Compressors now replaces the individual `GzipEncoder` and `DefalteEncoder`.

### Deprecated

- The `GzipEncoder` and the `DeflateEncoder` are now deprecated, as they are
just shims for backwards compatibility. Please replace them with `CompressEncoder`

### Removed

- The `Encoder` class has been removed, as its no longer functional.

## [2.1.1] - 2020-12-03
### Added
- Support for PHP 8.0
Expand Down
156 changes: 129 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@
![Testing][ico-ga]
[![Total Downloads][ico-downloads]][link-downloads]

Middleware to encode the response body to `gzip` or `deflate` if the `Accept-Encoding` header is present and adds the `Content-Encoding` header. This package is splitted into the following components:
Middleware to encode the response body using any of `gzip`, `deflate`, `brotli` or `zstd` compression (where available) if the `Accept-Encoding` header is present and can be matched. The `Content-Encoding` header is added when the body has been compressed. This package is split into the following components:

* [GzipEncoder](#gzipencoder)
* [DeflateEncoder](#deflateencoder)
* [CompressEncoder](#compressencoder)
* [BrotliCompressor](#brotlicompressor)
* [DeflateCompressor](#deflatecompressor)
* [GzipCompressor](#gzipcompressor)
* [DeflateCompressor](#deflatecompressor)
* (deprecated) [GzipEncoder](#gzipencoder-deprecated)
* (deprecated) [DeflateEncoder](#deflateencoder-deprecated)

You can use the component `ContentEncoding` in the [middlewares/negotiation](https://github.com/middlewares/negotiation#contentencoding) to negotiate the encoding to use.

## Requirements

* PHP >= 7.2
* A [PSR-7 http library](https://github.com/middlewares/awesome-psr15-middlewares#psr-7-implementations)
* A [PSR-15 middleware dispatcher](https://github.com/middlewares/awesome-psr15-middlewares#dispatcher)
* A [PSR-7 HTTP Library](https://github.com/middlewares/awesome-psr15-middlewares#psr-7-implementations)
* A [PSR-15 Middleware Dispatcher](https://github.com/middlewares/awesome-psr15-middlewares#dispatcher)

### Optional Requirements

* [PHP Brotli Extension](https://github.com/kjdev/php-ext-brotli)
* [PHP ZStd Extension](https://github.com/kjdev/php-ext-zstd)

## Installation

Expand All @@ -26,63 +36,155 @@ This package is installable and autoloadable via Composer as [middlewares/encode
composer require middlewares/encoder
```

## GzipEncoder
## Upgrading from v2.x or earlier

Compress the response body to GZIP format using [gzencode](http://php.net/manual/en/function.gzencode.php) and add the header `Content-Encoding: gzip`.
When upgrading its advised to switch from using the deprecated GzipEncoder and or DeflateEncoder directly, to using the
CompressEncoder middleware.

**Note:** The response body is encoded only if the header contains the value `gzip` in the header `Accept-Encoding`.
```diff
Dispatcher::run([
...
- new Middlewares\GzipEncoder(),
- new Middlewares\DeflateEncoder(),
+ new Middlewares\CompressEncoder(),
...
]);
```

_This configuration will try ZStd, Brotli, GZip, and then Deflate, in that order, if available (php extensions are
loaded for zstd and or brotli)._

## CompressEncoder

Compress the response body to matching `Accept-Encoding` format format using the first matching Compressor (by default
`zstd`, `brotli`, `gzip` and `defalate` are tried, in that order, where php extentions are available) after which the
`Content-Encoding` header will be added to the output.

**Note:** The response body is encoded only if the `Accept-Encoding` header contains an available compression type, the
Content-Type is considered compressible, and there is no `Content-Encoding` header.

```php
Dispatcher::run([
new Middlewares\GzipEncoder(),
new Middlewares\CompressEncoder(),
]);
```

Optionally, you can provide a `Psr\Http\Message\StreamFactoryInterface` that will be used to create the response body. If it's not defined, [Middleware\Utils\Factory](https://github.com/middlewares/utils#factory) will be used to detect it automatically.
#### Optional Parameters:

- You can provide a `Psr\Http\Message\StreamFactoryInterface` that will be used to create the response body.
If it's not defined, [Middleware\Utils\Factory](https://github.com/middlewares/utils#factory) will be used a default.

```php
$streamFactory = new MyOwnStreamFactory();

$encoder = new Middlewares\GzipEncoder($streamFactory);
$encoder = new Middlewares\CompressEncoder($streamFactory);
```

## DeflateEncoder
- You can also provide your own list of Compressors (that implement the `Middlewares\CompressorInterface`). For example:

Compress the response body to Deflate format using [gzdeflate](http://php.net/manual/en/function.gzdeflate.php) and add the header `Content-Encoding: deflate`.
```php
$encoder = new Middlewares\CompressEncoder(null, [
new MyProject\LzmaCompressor(),
new Middlewares\GZipCompressor($level = 9),
]);

**Note:** The response body is encoded only if the header contains the value `deflate` in the header `Accept-Encoding`.
```

### Only compress specific Content-Types

This option allows the overriding of the default patterns used to detect what resources are already compressed.

The default pattern detects the following mime types `text/*`, `application/json`, `image/svg+xml` and empty content
types as compressible. If the pattern begins with a forward slash `/` it is tested as a regular expression, otherwise
its is treated as a case-insensitive string comparison.

```php
Dispatcher::run([
new Middlewares\DeflateEncoder(),
(new Middlewares\CompressEncoder())
->contentType(
'/^application\/pdf$/', // Regular Expression
'text/csv' // Text Pattern
)
]);
```

Optionally, you can provide a `Psr\Http\Message\StreamFactoryInterface` that will be used to create the response body. If it's not defined, [Middleware\Utils\Factory](https://github.com/middlewares/utils#factory) will be used to detect it automatically.
---

### BrotliCompressor

The brotli compressor is used where the `Accept-Encoding` includes `br` and can be configured with a custom compression
level, via a constructor parameter.

```php
$streamFactory = new MyOwnStreamFactory();
$encoder = new Middlewares\CompressEncoder(null, [
new Middlewares\BrotliCompressor($level = 1),
]);
```
### DeflateCompressor

The deflate compressor is used where the `Accept-Encoding` includes `deflate` and can be configured with a custom compression
level, via a constructor parameter.

$encoder = new Middlewares\DeflateEncoder($streamFactory);
```php
$encoder = new Middlewares\CompressEncoder(null, [
new Middlewares\DeflateCompressor($level = 1),
]);
```

## Common Options
### GzipCompressor

### contentType
The gzip compressor is used where the `Accept-Encoding` includes `gzip` and can be configured with a custom compression
level, via a constructor parameter.

This option allows the overring of the default patterns used to detect what resources are already compressed.
```php
$encoder = new Middlewares\CompressEncoder(null, [
new Middlewares\GzipCompressor($level = 1),
]);
```

The default pattern detects the following mime types `text/*`, `application/json`, `image/svg+xml` and empty content types as compressible. If the pattern begins with a forward slash `/` it is tested as a regular expression, otherwise its is case-insensitive string comparison.
### ZStdCompressor

The gzip compressor is used where the `Accept-Encoding` includes `zstd` and can be configured with a custom compression
level, via a constructor parameter.

```php
$encoder = new Middlewares\CompressEncoder(null, [
new Middlewares\ZStdCompressor($level = 1),
]);
```

## GzipEncoder (deprecated)

**Please note, this is provided for backward compatibility only**

Compress the response body to GZIP format using [gzencode](http://php.net/manual/en/function.gzencode.php) and add the header `Content-Encoding: gzip`.

**Note:** The response body is encoded only if the header contains the value `gzip` in the header `Accept-Encoding`.

```php
Dispatcher::run([
(new Middlewares\DeflateEncoder())
->contentType(
'/^application\/pdf$/', // Regular Expression
'text/csv' // Text Pattern
)
new Middlewares\GzipEncoder(),
]);
```

Optionally, you can provide a `Psr\Http\Message\StreamFactoryInterface` as above.

## DeflateEncoder (deprecated)

**Please note, this is provided for backward compatibility only**

Compress the response body to Deflate format using [gzdeflate](http://php.net/manual/en/function.gzdeflate.php) and add the header `Content-Encoding: deflate`.

**Note:** The response body is encoded only if the header contains the value `deflate` in the header `Accept-Encoding`.

```php
Dispatcher::run([
new Middlewares\DeflateEncoder(),
]);
```

Optionally, you can provide a `Psr\Http\Message\StreamFactoryInterface` as above.

---

Please see [CHANGELOG](CHANGELOG.md) for more information about recent changes and [CONTRIBUTING](CONTRIBUTING.md) for contributing details.
Expand Down
9 changes: 7 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"require": {
"ext-zlib": "*",
"php": "^7.2 || ^8.0",
"middlewares/utils": "^3.0",
"middlewares/utils": "^4.0",
"psr/http-server-middleware": "^1.0"
},
"require-dev": {
Expand All @@ -29,7 +29,12 @@
"friendsofphp/php-cs-fixer": "^2.0",
"squizlabs/php_codesniffer": "^3.0",
"oscarotero/php-cs-fixer-config": "^1.0",
"phpstan/phpstan": "^0.12"
"phpstan/phpstan": "^0.12",
"vimeo/psalm": "^4.8"
},
"suggest": {
"ext-brotli": "Enable Brotli Compression",
"ext-zstd": "Enable ZStd Compression"
},
"autoload": {
"psr-4": {
Expand Down
15 changes: 15 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>
35 changes: 35 additions & 0 deletions src/BrotliCompressor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);

namespace Middlewares;

final class BrotliCompressor implements CompressorInterface
{
/** @var int Compression Level */
private $level;

/**
* Brotli Compression Support (requires brotli php extension)
*
* @link https://github.com/kjdev/php-ext-brotli
* @param int $level Brotli Compression Level, 5 is default (small than gzip, about as fast)
*/
public function __construct(int $level = 5)
{
$this->level = $level;
}

public function name(): string
{
return 'br';
}

public function compress(string $input): string
{
$out = \brotli_compress($input, $this->level);
if ($out === false) {
throw new \RuntimeException('Error occurred while compressing output');
}
return $out;
}
}
Loading