mirror of
https://github.com/notrab/dumbo.git
synced 2025-01-17 06:08:31 +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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "0c0bda01f8c04d2e20e328eeb6eff878",
|
||||
"content-hash": "3015b9d542341a20dd43db046eb7c4e7",
|
||||
"packages": [
|
||||
{
|
||||
"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\RouteCollector;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user