Skip to content

Commit dd94501

Browse files
committed
init commit
0 parents  commit dd94501

20 files changed

+874
-0
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: "Continuous Integration"
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
tags:
8+
9+
jobs:
10+
ci:
11+
uses: laminas/workflow-continuous-integration/.github/workflows/[email protected]

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
vendor
2+
.idea
3+
composer.lock
4+
composer.phar
5+
.phpunit.cache

.laminas-ci.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"additional_checks": [
3+
{
4+
"name": "PHPStan",
5+
"job": {
6+
"php": "*",
7+
"dependencies": "*",
8+
"command": "vendor/bin/phpstan analyse"
9+
}
10+
}
11+
],
12+
"stablePHP": "8.0"
13+
}

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Laravel Content Encoding Middleware
2+
3+
## About
4+
5+
Middleware that encodes response content.
6+
7+
Reduces data sent out, reduces bandwidth used.
8+
9+
## Installation
10+
11+
Require the `` package in your composer.json and update your dependencies:
12+
13+
## Configuration
14+
15+
The defaults are set in `config/content-encoding.php`.
16+
To publish a copy to your own config, use the following:
17+
18+
```text
19+
php artisan vendor:publish --tag="green-turtle-content-encoding"
20+
```
21+
22+
### Encode Unknown Types
23+
24+
Sometimes the `Content-Type` header may be missing.
25+
You may specify in your config whether you still wish to try encoding data.
26+
27+
By default, it is set to false.
28+
29+
```php
30+
'encode_unknown_type' => false,
31+
```
32+
33+
### Allowed Types
34+
35+
These are the types of content allowed to be encoded.
36+
Each type is a string that will be used as a regex pattern.
37+
38+
Example, any text format is acceptable:
39+
40+
```php
41+
'allowed_types' => [ '#^(text\/.*)(;.*)?$#' ]
42+
```
43+
44+
#### Encoders
45+
46+
These are the encoders determine what encodings are supported.
47+
48+
The built-in Encoders are enabled by default:
49+
50+
```php
51+
'encoders' => [
52+
Gzip::class,
53+
Deflate::class,
54+
]
55+
```
56+
57+
You may create more by implementing the following interface:
58+
59+
```text
60+
GreenTurtle\Middleware\Encoder\ContentEncoder
61+
```
62+
63+
## Global Usage
64+
65+
To enable this middleware globally, add the following to your `middleware` array, found within `app/Http/Kernel.php`:
66+
67+
For example:
68+
69+
```php
70+
protected $middleware = [
71+
// other middleware...
72+
\GreenTurtle\Middleware\ContentEncoding::class
73+
// other middleware...
74+
];
75+
```

composer.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "green-turtle/content-encoding",
3+
"type": "library",
4+
"autoload": {
5+
"psr-4": {
6+
"GreenTurtle\\Middleware\\": "src/"
7+
}
8+
},
9+
"autoload-dev": {
10+
"psr-4": {
11+
"GreenTurtle\\Middleware\\Tests\\": "tests/",
12+
"GreenTurtle\\Middleware\\Tests\\Fixtures\\": "tests/Fixtures"
13+
}
14+
},
15+
"require": {
16+
"php": "^8.1.0",
17+
"ext-zlib": "*",
18+
"illuminate/http": "^9.0 || ^10.0",
19+
"illuminate/support": "^9.0 || ^10.0",
20+
"symfony/psr-http-message-bridge": "^2.1"
21+
},
22+
"require-dev": {
23+
"phpstan/phpstan": "^1.10",
24+
"phpunit/phpunit": "^10.5",
25+
"squizlabs/php_codesniffer": "^3.7"
26+
},
27+
"extra": {
28+
"laravel": {
29+
"providers": [
30+
"GreenTurtle\\Middleware\\ServiceProvider"
31+
]
32+
}
33+
}
34+
}

config/content-encoding.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use GreenTurtle\Middleware\Encoder;
6+
7+
return [
8+
/*
9+
|--------------------------------------------------------------------------
10+
| Encode Unknown Content Type
11+
|--------------------------------------------------------------------------
12+
|
13+
| A boolean value
14+
| false ~ Do not encode if the 'Content-Type' header is missing
15+
| true ~ Try to encode when the 'Content-Type' header is missing
16+
*/
17+
'encode_unknown_type' => false,
18+
19+
/*
20+
|--------------------------------------------------------------------------
21+
| Allowed Content Types
22+
|--------------------------------------------------------------------------
23+
|
24+
| An array of string regex patterns
25+
| Specifies the content types allowed for encoding
26+
| Any content type that matches one of the regex patterns is allowed
27+
|
28+
*/
29+
'allowed_types' => [
30+
'#^(text\/.*)(;.*)?$#',
31+
'#^(image\/svg\\+xml)(;.*)?$#',
32+
'#^(application\/json)(;.*)?$#',
33+
],
34+
35+
/*
36+
|--------------------------------------------------------------------------
37+
| Content Encoders
38+
|--------------------------------------------------------------------------
39+
|
40+
| An array of ContentEncoder implementations
41+
| Specifies which encodings are available to the middleware
42+
| The order determines which available encodings take priority
43+
|
44+
*/
45+
'encoders' => [
46+
Encoder\Gzip::class,
47+
Encoder\Deflate::class,
48+
],
49+
];

phpcs.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<ruleset name="PSR">
4+
<description>PHPCS configuration file.</description>
5+
<file>src</file>
6+
7+
<exclude-pattern>*/vendor/*</exclude-pattern>
8+
9+
<rule ref="PSR1"/>
10+
<rule ref="PSR12"/>
11+
</ruleset>

phpstan.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
level: 9
3+
paths:
4+
- src
5+
ignoreErrors:
6+
- '#^Function config not found.$#'
7+
- '#^Function config_path not found.$#'

phpunit.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" bootstrap="vendor/autoload.php" executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" failOnRisky="true" failOnWarning="true" cacheDirectory=".phpunit.cache" requireCoverageMetadata="true" beStrictAboutCoverageMetadata="true">
3+
<testsuites>
4+
<testsuite name="default">
5+
<directory>tests</directory>
6+
</testsuite>
7+
</testsuites>
8+
<coverage/>
9+
<source>
10+
<include>
11+
<directory suffix=".php">src</directory>
12+
</include>
13+
</source>
14+
</phpunit>

src/ContentEncoding.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace GreenTurtle\Middleware;
4+
5+
use Closure;
6+
use GreenTurtle\Middleware\Encoder\ContentEncoder;
7+
use GreenTurtle\Middleware\Exception\FailedToEncode;
8+
use Illuminate\Http\Request;
9+
use Symfony\Component\HttpFoundation\Response;
10+
11+
class ContentEncoding
12+
{
13+
/** @var string[] */
14+
private readonly array $types;
15+
/** @var ContentEncoder[] */
16+
private readonly array $encoders;
17+
18+
/**
19+
* @param string[] $allowedTypes
20+
* @param ContentEncoder[] $encoders
21+
*/
22+
public function __construct(
23+
private readonly bool $encodeUnknownType,
24+
array $allowedTypes,
25+
array $encoders
26+
) {
27+
(fn(string ...$p) => true)(...$allowedTypes);
28+
(fn(ContentEncoder ...$p) => true)(...$encoders);
29+
30+
$this->types = $allowedTypes;
31+
$this->encoders = $encoders;
32+
}
33+
34+
public function handle(Request $request, Closure $next): Response
35+
{
36+
$response = $next($request);
37+
assert($response instanceof Response);
38+
39+
if (
40+
!$response->getContent()
41+
|| $response->headers->has('Content-Encoding')
42+
|| !is_string($request->header('Accept-Encoding'))
43+
|| !$this->isEncodable($request->header('Content-Type'))
44+
) {
45+
return $response;
46+
}
47+
48+
[$encoding, $encoder] = $this->findEncoder($request->header('Accept-Encoding'));
49+
if (is_null($encoding) || is_null($encoder)) {
50+
return $response;
51+
}
52+
53+
try {
54+
$encodedContent = $encoder->encode($response->getContent());
55+
} catch (FailedToEncode) {
56+
return $response;
57+
}
58+
59+
$response->headers->set('Content-Encoding', $encoding);
60+
$response->setContent($encodedContent);
61+
return $response;
62+
}
63+
64+
private function isEncodable(mixed $contentType): bool
65+
{
66+
if (!is_string($contentType)) {
67+
return $this->encodeUnknownType;
68+
}
69+
70+
foreach ($this->types as $type) {
71+
if (preg_match($type, $contentType) === 1) {
72+
return true;
73+
}
74+
}
75+
76+
return false;
77+
}
78+
79+
/** @return array{?string, ?ContentEncoder} */
80+
private function findEncoder(string $acceptEncoding): array
81+
{
82+
$acceptEncodings = explode(',', str_replace(' ', '', $acceptEncoding));
83+
84+
foreach ($this->encoders as $encoder) {
85+
foreach ($acceptEncodings as $acceptEncoding) {
86+
if ($encoder->supports($acceptEncoding)) {
87+
return [$acceptEncoding, $encoder];
88+
}
89+
}
90+
}
91+
92+
return [null, null];
93+
}
94+
}

src/Encoder/ContentEncoder.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GreenTurtle\Middleware\Encoder;
6+
7+
interface ContentEncoder
8+
{
9+
public function supports(string $encoding): bool;
10+
11+
public function encode(string $content): string;
12+
}

src/Encoder/Deflate.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GreenTurtle\Middleware\Encoder;
6+
7+
use GreenTurtle\Middleware\Exception\FailedToEncode;
8+
9+
final class Deflate implements ContentEncoder
10+
{
11+
private const SUPPORTED_ENCODINGS = [
12+
'deflate',
13+
];
14+
15+
public function supports(string $encoding): bool
16+
{
17+
return in_array(strtolower($encoding), self::SUPPORTED_ENCODINGS);
18+
}
19+
20+
public function encode(string $content): string
21+
{
22+
$encodedContent = gzdeflate($content);
23+
24+
if (!$encodedContent) {
25+
throw FailedToEncode::errorOccured();
26+
}
27+
28+
return $encodedContent;
29+
}
30+
}

src/Encoder/Gzip.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GreenTurtle\Middleware\Encoder;
6+
7+
use GreenTurtle\Middleware\Exception\FailedToEncode;
8+
9+
final class Gzip implements ContentEncoder
10+
{
11+
private const SUPPORTED_ENCODINGS = [
12+
'gzip',
13+
'x-gzip',
14+
];
15+
16+
public function supports(string $encoding): bool
17+
{
18+
return in_array(strtolower($encoding), self::SUPPORTED_ENCODINGS);
19+
}
20+
21+
public function encode(string $content): string
22+
{
23+
$encodedContent = gzencode($content);
24+
25+
if (!$encodedContent) {
26+
throw FailedToEncode::errorOccured();
27+
}
28+
29+
return $encodedContent;
30+
}
31+
}

0 commit comments

Comments
 (0)