Skip to content

Commit 18a5721

Browse files
committed
add lz4 and refactor
1 parent 9b139ea commit 18a5721

File tree

9 files changed

+158
-45
lines changed

9 files changed

+158
-45
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
uses: shivammathur/setup-php@v2
3434
with:
3535
php-version: ${{ matrix.php }}
36-
extensions: dom, curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, exif, zstd, brotli
36+
extensions: dom, curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, exif, zstd, brotli, lz4
3737
coverage: pcov
3838

3939
- name: 🏗 Get composer cache directory

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/vendor/
2+
/ext/
23
/node_modules
34
composer.lock
45
coverage

composer.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "open-southeners/laravel-vapor-response-compression",
2+
"name": "open-southeners/laravel-response-compression",
33
"description": "Add server-side response compression with a range of different algorithms (Gzip, brotli, deflate...)",
44
"license": "MIT",
55
"keywords": [
@@ -10,7 +10,9 @@
1010
"response-compression",
1111
"gzip-compression",
1212
"deflate-compression",
13-
"brotli-compression"
13+
"brotli-compression",
14+
"ztsd-compression",
15+
"lz4-compression"
1416
],
1517
"authors": [
1618
{
@@ -20,13 +22,13 @@
2022
}
2123
],
2224
"require": {
23-
"php": "^7.2 || ^8.0",
24-
"ext-zlib": "^7.2 || ^8.0"
25+
"php": "^8.2",
26+
"ext-zlib": "^8.2"
2527
},
2628
"require-dev": {
27-
"larastan/larastan": "^2.0",
28-
"orchestra/testbench": "^8.0 || ^9.0",
29-
"phpstan/phpstan": "^1.0",
29+
"larastan/larastan": "^3.0",
30+
"orchestra/testbench": "^9.0 || ^10.0",
31+
"phpstan/phpstan": "^2.0",
3032
"phpunit/phpunit": "^10.0"
3133
},
3234
"minimum-stability": "stable",

config/response-compression.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
'enable' => env('RESPONSE_COMPRESSION_ENABLE', true),
1717

18+
// Threshold size in bytes from where the compression will be applied to responses
1819
// @see https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#http-api-quotas
1920
'threshold' => 10000,
2021

@@ -31,6 +32,9 @@
3132

3233
// @see https://www.php.net/manual/en/function.gzdeflate.php
3334
'deflate' => 9,
35+
36+
// @see Maximum level is 12 at the moment of this written
37+
'lz4' => 4,
3438

3539
],
3640

src/CompressionEncoding.php

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,63 @@
22

33
namespace OpenSoutheners\LaravelVaporResponseCompression;
44

5-
class CompressionEncoding
5+
use ReflectionEnumBackedCase;
6+
7+
enum CompressionEncoding: string
68
{
7-
public const BROTLI = 'br';
9+
#[EncoderAsFunction('brotli_compress')]
10+
case Brotli = 'br';
811

9-
public const DEFLATE = 'deflate';
12+
#[EncoderAsFunction('gzdeflate')]
13+
case Deflate = 'deflate';
1014

11-
public const GZIP = 'gzip';
15+
#[EncoderAsFunction('gzencode')]
16+
case Gzip = 'gzip';
1217

13-
public const ZSTANDARD = 'zstd';
18+
#[EncoderAsFunction('zstd_compress')]
19+
case Zstandard = 'zstd';
20+
21+
#[EncoderAsFunction('lz4_compress')]
22+
case Lz4 = 'lz4';
23+
24+
/**
25+
* Get a list of compression encoding formats supported by the system.
26+
*
27+
* @return array<string, string>
28+
*/
29+
public static function listSupported(): array
30+
{
31+
$supportedList = [];
32+
33+
foreach (self::cases() as $case) {
34+
if ($function = $case->isSupported()) {
35+
$supportedList[$case->value] = $function;
36+
}
37+
}
38+
39+
return $supportedList;
40+
}
41+
42+
/**
43+
* Check if compression encoding is supported by this system in case not it returns null.
44+
*/
45+
public function isSupported(): ?string
46+
{
47+
$reflector = new ReflectionEnumBackedCase(self::class, $this->name);
48+
49+
$attributes = $reflector->getAttributes(EncoderAsFunction::class);
50+
51+
if (count($attributes) === 0) {
52+
return null;
53+
}
54+
55+
/** @var \OpenSoutheners\LaravelVaporResponseCompression\EncoderAsFunction $attribute */
56+
$attribute = $attributes[0]->newInstance();
57+
58+
if (function_exists($attribute->name)) {
59+
return $attribute->name;
60+
}
61+
62+
return null;
63+
}
1464
}

src/EncoderAsFunction.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace OpenSoutheners\LaravelVaporResponseCompression;
4+
5+
use Attribute;
6+
7+
#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
8+
class EncoderAsFunction
9+
{
10+
public function __construct(public string $name)
11+
{
12+
//
13+
}
14+
}

src/ResponseCompression.php

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@ public function handle($request, Closure $next)
3030
)
3131
);
3232

33-
$response->headers->add([
33+
$responseHeaders = [
3434
'Content-Encoding' => $algo,
35-
'X-Vapor-Base64-Encode' => 'True',
36-
]);
35+
];
36+
37+
if (getenv('VAPOR_SSM_PATH')) {
38+
$responseHeaders['X-Vapor-Base64-Encode'] = 'True';
39+
}
40+
41+
$response->headers->add($responseHeaders);
3742
}
3843
});
3944
}
@@ -60,24 +65,14 @@ protected function shouldCompressResponse($response): bool
6065
*/
6166
protected function shouldCompressUsing($request): ?array
6267
{
63-
$requestEncodings = $request->getEncodings();
64-
65-
if (in_array(CompressionEncoding::ZSTANDARD, $requestEncodings) && function_exists('zstd_compress')) {
66-
return [CompressionEncoding::ZSTANDARD, 'zstd_compress'];
67-
}
68+
$supportedList = CompressionEncoding::listSupported();
6869

69-
if (in_array(CompressionEncoding::BROTLI, $requestEncodings) && function_exists('brotli_compress')) {
70-
return [CompressionEncoding::BROTLI, 'brotli_compress'];
71-
}
70+
$fromSupportedList = array_intersect($request->getEncodings(), array_keys($supportedList));
7271

73-
if (in_array(CompressionEncoding::GZIP, $requestEncodings) && function_exists('gzencode')) {
74-
return [CompressionEncoding::GZIP, 'gzencode'];
72+
if ($fromSupportedList[0] ?? false) {
73+
return [$fromSupportedList[0], $supportedList[$fromSupportedList[0]]];
7574
}
76-
77-
if (in_array(CompressionEncoding::DEFLATE, $requestEncodings) && function_exists('gzdeflate')) {
78-
return [CompressionEncoding::DEFLATE, 'gzdeflate'];
79-
}
80-
75+
8176
return null;
8277
}
8378
}

tests/CompressionEncodingTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace OpenSoutheners\LaravelVaporResponseCompression\Tests;
4+
5+
use OpenSoutheners\LaravelVaporResponseCompression\CompressionEncoding;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class CompressionEncodingTest extends TestCase
9+
{
10+
public function testCompressionEncodingListSupportedReturnsArray()
11+
{
12+
$supportedList = CompressionEncoding::listSupported();
13+
14+
$this->assertIsArray($supportedList);
15+
$this->assertArrayHasKey(CompressionEncoding::Deflate->value, $supportedList);
16+
$this->assertEmpty(array_diff(
17+
array_map(fn ($case) => $case->value, CompressionEncoding::cases()),
18+
array_keys($supportedList),
19+
));
20+
}
21+
}

tests/ResponseCompressionTest.php

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,25 @@ protected function setUp(): void
2929
})->middleware(ResponseCompression::class);
3030
}
3131

32+
public function testList()
33+
{
34+
$this->withoutExceptionHandling();
35+
36+
$response = $this->get('/light', ['Accept-Encoding' => CompressionEncoding::Gzip->value]);
37+
38+
$response->assertHeaderMissing('Content-Encoding');
39+
40+
$this->assertEquals(
41+
$response->json(),
42+
['content' => $this->lightResponseContent]
43+
);
44+
}
45+
3246
public function testClientGetRawResponseWhenThresholdNotReached()
3347
{
3448
$this->withoutExceptionHandling();
3549

36-
$response = $this->get('/light', ['Accept-Encoding' => CompressionEncoding::GZIP]);
50+
$response = $this->get('/light', ['Accept-Encoding' => CompressionEncoding::Gzip->value]);
3751

3852
$response->assertHeaderMissing('Content-Encoding');
3953

@@ -49,7 +63,7 @@ public function testClientGetRawResponseWhenNotEnabled()
4963

5064
$this->withoutExceptionHandling();
5165

52-
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::GZIP]);
66+
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::Gzip->value]);
5367

5468
$response->assertHeaderMissing('Content-Encoding');
5569

@@ -61,9 +75,9 @@ public function testClientGetRawResponseWhenNotEnabled()
6175

6276
public function testClientGetResponseCompressedWhenThresholdReached()
6377
{
64-
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::GZIP]);
78+
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::Gzip->value]);
6579

66-
$response->assertHeader('Content-Encoding', CompressionEncoding::GZIP);
80+
$response->assertHeader('Content-Encoding', CompressionEncoding::Gzip->value);
6781

6882
$this->assertEquals(
6983
gzdecode($response->getContent()),
@@ -73,9 +87,9 @@ public function testClientGetResponseCompressedWhenThresholdReached()
7387

7488
public function testClientGetResponseInThePreferredEncoding()
7589
{
76-
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::DEFLATE]);
90+
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::Deflate->value]);
7791

78-
$response->assertHeader('Content-Encoding', CompressionEncoding::DEFLATE);
92+
$response->assertHeader('Content-Encoding', CompressionEncoding::Deflate->value);
7993

8094
$this->assertEquals(
8195
gzinflate($response->getContent()),
@@ -100,12 +114,12 @@ public function testClientGetResponseWithContentEncodingHeaderAlreadyAttachedRec
100114
Route::get('/heavy-ignored', function () {
101115
return response()->json([
102116
'content' => $this->heavyResponseContent,
103-
], 200, ['Content-Encoding' => CompressionEncoding::DEFLATE]);
117+
], 200, ['Content-Encoding' => CompressionEncoding::Deflate->value]);
104118
})->middleware(ResponseCompression::class);
105119

106-
$response = $this->get('/heavy-ignored', ['Accept-Encoding' => CompressionEncoding::GZIP]);
120+
$response = $this->get('/heavy-ignored', ['Accept-Encoding' => CompressionEncoding::Gzip->value]);
107121

108-
$response->assertHeader('Content-Encoding', CompressionEncoding::DEFLATE);
122+
$response->assertHeader('Content-Encoding', CompressionEncoding::Deflate->value);
109123

110124
$this->assertEquals(
111125
$response->json(),
@@ -121,7 +135,7 @@ public function testClientGetStreamDownloadResponseReceivesRawResponse()
121135
});
122136
})->middleware(ResponseCompression::class);
123137

124-
$response = $this->get('/download', ['Accept-Encoding' => CompressionEncoding::DEFLATE]);
138+
$response = $this->get('/download', ['Accept-Encoding' => CompressionEncoding::Deflate->value]);
125139

126140
$response->assertHeaderMissing('Content-Encoding');
127141

@@ -133,9 +147,9 @@ public function testClientGetStreamDownloadResponseReceivesRawResponse()
133147

134148
public function testClientGetResponseUsingZstandardEncoding()
135149
{
136-
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::ZSTANDARD]);
150+
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::Zstandard->value]);
137151

138-
$response->assertHeader('Content-Encoding', CompressionEncoding::ZSTANDARD);
152+
$response->assertHeader('Content-Encoding', CompressionEncoding::Zstandard->value);
139153

140154
$this->assertEquals(
141155
zstd_uncompress($response->getContent()),
@@ -145,13 +159,25 @@ public function testClientGetResponseUsingZstandardEncoding()
145159

146160
public function testClientGetResponseUsingBrotliEncoding()
147161
{
148-
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::BROTLI]);
162+
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::Brotli->value]);
149163

150-
$response->assertHeader('Content-Encoding', CompressionEncoding::BROTLI);
164+
$response->assertHeader('Content-Encoding', CompressionEncoding::Brotli->value);
151165

152166
$this->assertEquals(
153167
brotli_uncompress($response->getContent()),
154168
json_encode(['content' => $this->heavyResponseContent])
155169
);
156170
}
171+
172+
public function testClientGetResponseUsingLz4Encoding()
173+
{
174+
$response = $this->get('/heavy', ['Accept-Encoding' => CompressionEncoding::Lz4->value]);
175+
176+
$response->assertHeader('Content-Encoding', CompressionEncoding::Lz4->value);
177+
178+
$this->assertEquals(
179+
lz4_uncompress($response->getContent()),
180+
json_encode(['content' => $this->heavyResponseContent])
181+
);
182+
}
157183
}

0 commit comments

Comments
 (0)