mirror of
https://github.com/notrab/dumbo.git
synced 2025-01-17 14:18:14 +01:00
feat: dumbo compress helper (#38)
* refactor: add missing response interface * feat(compress): adding compress helper * Apply suggestions from code review Co-authored-by: Jamie Barton <jamie@notrab.dev> * refactor: add brotli compress suggestion * revert brotli for now --------- Co-authored-by: Jamie Barton <jamie@notrab.dev>
This commit is contained in:
parent
038db48f75
commit
b558a2b1ef
2
composer.lock
generated
2
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "0c0bda01f8c04d2e20e328eeb6eff878",
|
"content-hash": "3015b9d542341a20dd43db046eb7c4e7",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "guzzlehttp/psr7",
|
"name": "guzzlehttp/psr7",
|
||||||
|
23
examples/compress/README.md
Normal file
23
examples/compress/README.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Compress Example
|
||||||
|
|
||||||
|
This example demonstrates how to compress responses using gzip or deflate in Dumbo.
|
||||||
|
|
||||||
|
## Running the Example
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer start
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Make a request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Accept-Encoding: gzip" --compressed -i http://localhost:8000
|
||||||
|
```
|
18
examples/compress/composer.json
Normal file
18
examples/compress/composer.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"notrab/dumbo": "@dev"
|
||||||
|
},
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "path",
|
||||||
|
"url": "../../"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"start": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"php -S localhost:8000 -t ."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prefer-stable": false
|
||||||
|
}
|
21
examples/compress/index.php
Normal file
21
examples/compress/index.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require "vendor/autoload.php";
|
||||||
|
|
||||||
|
use Dumbo\Dumbo;
|
||||||
|
use Dumbo\Helpers\Compress;
|
||||||
|
|
||||||
|
$app = new Dumbo();
|
||||||
|
|
||||||
|
$app->use(
|
||||||
|
Compress::compress([
|
||||||
|
"threshold" => 1024, // Minimum size to compress (bytes)
|
||||||
|
"encoding" => "gzip", // Preferred encoding (gzip or deflate)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$app->get("/", function ($c) {
|
||||||
|
return $c->json(["message" => "Hello, Dumbo!"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->run();
|
144
src/Helpers/Compress.php
Normal file
144
src/Helpers/Compress.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Dumbo\Helpers;
|
||||||
|
|
||||||
|
use Dumbo\Context;
|
||||||
|
use GuzzleHttp\Psr7\Utils;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
class Compress
|
||||||
|
{
|
||||||
|
private const STR_REGEX = '/^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a middleware that compresses the response body using the specified encoding
|
||||||
|
* if the response size is greater than the given threshold.
|
||||||
|
*
|
||||||
|
* @param array{
|
||||||
|
* threshold?: int,
|
||||||
|
* allowedEncodings?: string[],
|
||||||
|
* encoding?: string
|
||||||
|
* } $options Configuration options
|
||||||
|
* @return callable The middleware
|
||||||
|
*/
|
||||||
|
public static function compress(array $options = []): callable
|
||||||
|
{
|
||||||
|
$threshold = $options["threshold"] ?? 1024;
|
||||||
|
$allowedEncodings = ["gzip", "deflate"];
|
||||||
|
$encoding = $options["encoding"] ?? null;
|
||||||
|
|
||||||
|
return self::compressor($threshold, $allowedEncodings, $encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a middleware that compresses the response body using the specified encoding
|
||||||
|
* if the response size is greater than the given threshold.
|
||||||
|
*
|
||||||
|
* @param int $threshold The minimum response size in bytes to compress
|
||||||
|
* @param array $allowedEncodings The list of allowed encodings
|
||||||
|
* @param ?string $encoding The selected encoding, or null to auto-detect
|
||||||
|
* @return callable The middleware
|
||||||
|
*/
|
||||||
|
private static function compressor(
|
||||||
|
int $threshold,
|
||||||
|
array $allowedEncodings,
|
||||||
|
?string $encoding
|
||||||
|
): callable {
|
||||||
|
return function (Context $ctx, callable $next) use (
|
||||||
|
$threshold,
|
||||||
|
$allowedEncodings,
|
||||||
|
$encoding
|
||||||
|
) {
|
||||||
|
$next($ctx);
|
||||||
|
|
||||||
|
$response = $ctx->getResponse();
|
||||||
|
$contentLength = $response->getHeaderLine("Content-Length");
|
||||||
|
|
||||||
|
if (
|
||||||
|
$response->hasHeader("Content-Encoding") ||
|
||||||
|
$ctx->req->method() === "HEAD" ||
|
||||||
|
($contentLength && (int) $contentLength < $threshold) ||
|
||||||
|
!self::shouldCompress($response) ||
|
||||||
|
!self::shouldTransform($response)
|
||||||
|
) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$acceptedEncodings = array_map(
|
||||||
|
"trim",
|
||||||
|
explode(",", $ctx->req->header("Accept-Encoding"))
|
||||||
|
);
|
||||||
|
if (!$encoding) {
|
||||||
|
foreach ($allowedEncodings as $enc) {
|
||||||
|
if (in_array($enc, $acceptedEncodings)) {
|
||||||
|
$encoding = $enc;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$encoding || !$response->getBody()) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$compressedBody = self::performCompression(
|
||||||
|
$response->getBody(),
|
||||||
|
$encoding
|
||||||
|
);
|
||||||
|
$response = $response
|
||||||
|
->withBody($compressedBody)
|
||||||
|
->withoutHeader("Content-Length")
|
||||||
|
->withHeader("Content-Encoding", $encoding);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the response should be compressed based on the Content-Type header
|
||||||
|
*
|
||||||
|
* @param ResponseInterface $response The response to check
|
||||||
|
* @return bool Whether the response should be compressed
|
||||||
|
*/
|
||||||
|
private static function shouldCompress(ResponseInterface $response): bool
|
||||||
|
{
|
||||||
|
$type = $response->getHeaderLine("Content-Type");
|
||||||
|
return preg_match(self::STR_REGEX, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the response should be transformed (e.g. compressed) based on the
|
||||||
|
* Cache-Control header
|
||||||
|
*
|
||||||
|
* @param ResponseInterface $response The response to check
|
||||||
|
* @return bool Whether the response should be transformed
|
||||||
|
*/
|
||||||
|
private static function shouldTransform(ResponseInterface $response): bool
|
||||||
|
{
|
||||||
|
$cacheControl = $response->getHeaderLine("Cache-Control");
|
||||||
|
return !preg_match(
|
||||||
|
'/(?:^|,)\s*?no-transform\s*?(?:,|$)/i',
|
||||||
|
$cacheControl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the actual compression of the response body using the specified encoding
|
||||||
|
*
|
||||||
|
* @param string $body The response body to compress
|
||||||
|
* @param string $encoding The encoding to use for compression
|
||||||
|
*
|
||||||
|
* @return string|StreamInterface The compressed response body
|
||||||
|
*/
|
||||||
|
private static function performCompression(
|
||||||
|
string $body,
|
||||||
|
string $encoding
|
||||||
|
): StreamInterface {
|
||||||
|
return match ($encoding) {
|
||||||
|
"deflate" => Utils::streamFor(gzdeflate($body)),
|
||||||
|
"gzip" => Utils::streamFor(gzencode($body)),
|
||||||
|
default => $body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ namespace Dumbo;
|
|||||||
|
|
||||||
use FastRoute\Dispatcher;
|
use FastRoute\Dispatcher;
|
||||||
use FastRoute\RouteCollector;
|
use FastRoute\RouteCollector;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user