Skip to content

Commit 821e8a3

Browse files
committed
Merge pull request #9 from php-http/feature/decoder-encoder-plugin
Decoder and ContentLength plugin
2 parents 10d41a7 + 1f37656 commit 821e8a3

File tree

6 files changed

+349
-4
lines changed

6 files changed

+349
-4
lines changed

.travis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ env:
1818
- TEST_COMMAND="composer test"
1919

2020
matrix:
21-
allow_failures:
22-
- php: 7.0
2321
fast_finish: true
2422
include:
2523
- php: 5.4

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"php-http/authentication": "^0.1@dev",
2222
"php-http/cookie": "^0.1@dev",
2323
"symfony/stopwatch": "^2.3",
24-
"psr/log": "^1.0"
24+
"psr/log": "^1.0",
25+
"php-http/encoding": "^0.1@dev"
2526
},
2627
"autoload": {
2728
"psr-4": {
@@ -32,7 +33,8 @@
3233
"php-http/authentication": "Allow to use the AuthenticationPlugin",
3334
"php-http/cookie": "Allow to use CookiePlugin",
3435
"symfony/stopwatch": "Allow to use the StopwatchPlugin",
35-
"psr/log-implementation": "Allow to use the LoggerPlugin"
36+
"psr/log-implementation": "Allow to use the LoggerPlugin",
37+
"php-http/encoding": "Allow to use the Decoder and Encoder plugin"
3638
},
3739
"scripts": {
3840
"test": "vendor/bin/phpspec run",

spec/ContentLengthPluginSpec.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace spec\Http\Client\Plugin;
4+
5+
use PhpSpec\Exception\Example\SkippingException;
6+
use PhpSpec\ObjectBehavior;
7+
use Prophecy\Argument;
8+
use Psr\Http\Message\RequestInterface;
9+
use Psr\Http\Message\StreamInterface;
10+
11+
class ContentLengthPluginSpec extends ObjectBehavior
12+
{
13+
function it_is_initializable()
14+
{
15+
$this->shouldHaveType('Http\Client\Plugin\ContentLengthPlugin');
16+
$this->shouldImplement('Http\Client\Plugin\Plugin');
17+
}
18+
19+
function it_adds_content_length_header(RequestInterface $request, StreamInterface $stream)
20+
{
21+
$request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false);
22+
$request->getBody()->shouldBeCalled()->willReturn($stream);
23+
$stream->getSize()->shouldBeCalled()->willReturn(100);
24+
$request->withHeader('Content-Length', 100)->shouldBeCalled()->willReturn($request);
25+
26+
$this->handleRequest($request, function () {}, function () {});
27+
}
28+
29+
function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterface $stream)
30+
{
31+
if(defined('HHVM_VERSION')) {
32+
throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm');
33+
}
34+
35+
$request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false);
36+
$request->getBody()->shouldBeCalled()->willReturn($stream);
37+
38+
$stream->getSize()->shouldBeCalled()->willReturn(null);
39+
$stream->isReadable()->shouldBeCalled()->willReturn(true);
40+
$stream->isWritable()->shouldBeCalled()->willReturn(false);
41+
$stream->eof()->shouldBeCalled()->willReturn(false);
42+
43+
$request->withBody(Argument::type('Http\Encoding\ChunkStream'))->shouldBeCalled()->willReturn($request);
44+
45+
$this->handleRequest($request, function () {}, function () {});
46+
}
47+
}

spec/DecoderPluginSpec.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace spec\Http\Client\Plugin;
4+
5+
use Http\Client\Utils\Promise\FulfilledPromise;
6+
use Psr\Http\Message\RequestInterface;
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\StreamInterface;
9+
use PhpSpec\Exception\Example\SkippingException;
10+
use PhpSpec\ObjectBehavior;
11+
use Prophecy\Argument;
12+
13+
class DecoderPluginSpec extends ObjectBehavior
14+
{
15+
function it_is_initializable()
16+
{
17+
$this->shouldHaveType('Http\Client\Plugin\DecoderPlugin');
18+
}
19+
20+
function it_is_a_plugin()
21+
{
22+
$this->shouldImplement('Http\Client\Plugin\Plugin');
23+
}
24+
25+
function it_decodes(RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
26+
{
27+
if(defined('HHVM_VERSION')) {
28+
throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm');
29+
}
30+
31+
$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
32+
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request);
33+
$next = function () use($response) {
34+
return new FulfilledPromise($response->getWrappedObject());
35+
};
36+
37+
$response->hasHeader('Transfer-Encoding')->willReturn(true);
38+
$response->getHeader('Transfer-Encoding')->willReturn(['chunked']);
39+
$response->getBody()->willReturn($stream);
40+
$response->withBody(Argument::type('Http\Encoding\DechunkStream'))->willReturn($response);
41+
$response->withHeader('Transfer-Encoding', [])->willReturn($response);
42+
$response->hasHeader('Content-Encoding')->willReturn(false);
43+
44+
$stream->isReadable()->willReturn(true);
45+
$stream->isWritable()->willReturn(false);
46+
$stream->eof()->willReturn(false);
47+
48+
$this->handleRequest($request, $next, function () {});
49+
}
50+
51+
function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
52+
{
53+
$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
54+
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request);
55+
$next = function () use($response) {
56+
return new FulfilledPromise($response->getWrappedObject());
57+
};
58+
59+
$response->hasHeader('Transfer-Encoding')->willReturn(false);
60+
$response->hasHeader('Content-Encoding')->willReturn(true);
61+
$response->getHeader('Content-Encoding')->willReturn(['gzip']);
62+
$response->getBody()->willReturn($stream);
63+
$response->withBody(Argument::type('Http\Encoding\GzipDecodeStream'))->willReturn($response);
64+
$response->withHeader('Content-Encoding', [])->willReturn($response);
65+
66+
$stream->isReadable()->willReturn(true);
67+
$stream->isWritable()->willReturn(false);
68+
$stream->eof()->willReturn(false);
69+
70+
$this->handleRequest($request, $next, function () {});
71+
}
72+
73+
function it_decodes_deflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
74+
{
75+
$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
76+
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request);
77+
$next = function () use($response) {
78+
return new FulfilledPromise($response->getWrappedObject());
79+
};
80+
81+
$response->hasHeader('Transfer-Encoding')->willReturn(false);
82+
$response->hasHeader('Content-Encoding')->willReturn(true);
83+
$response->getHeader('Content-Encoding')->willReturn(['deflate']);
84+
$response->getBody()->willReturn($stream);
85+
$response->withBody(Argument::type('Http\Encoding\InflateStream'))->willReturn($response);
86+
$response->withHeader('Content-Encoding', [])->willReturn($response);
87+
88+
$stream->isReadable()->willReturn(true);
89+
$stream->isWritable()->willReturn(false);
90+
$stream->eof()->willReturn(false);
91+
92+
$this->handleRequest($request, $next, function () {});
93+
}
94+
95+
function it_decodes_inflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
96+
{
97+
$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
98+
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request);
99+
$next = function () use($response) {
100+
return new FulfilledPromise($response->getWrappedObject());
101+
};
102+
103+
$response->hasHeader('Transfer-Encoding')->willReturn(false);
104+
$response->hasHeader('Content-Encoding')->willReturn(true);
105+
$response->getHeader('Content-Encoding')->willReturn(['compress']);
106+
$response->getBody()->willReturn($stream);
107+
$response->withBody(Argument::type('Http\Encoding\DecompressStream'))->willReturn($response);
108+
$response->withHeader('Content-Encoding', [])->willReturn($response);
109+
110+
$stream->isReadable()->willReturn(true);
111+
$stream->isWritable()->willReturn(false);
112+
$stream->eof()->willReturn(false);
113+
114+
$this->handleRequest($request, $next, function () {});
115+
}
116+
117+
function it_does_not_decode_with_content_encoding(RequestInterface $request, ResponseInterface $response)
118+
{
119+
$this->beConstructedWith(false);
120+
121+
$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
122+
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldNotBeCalled();
123+
$next = function () use($response) {
124+
return new FulfilledPromise($response->getWrappedObject());
125+
};
126+
127+
$response->hasHeader('Transfer-Encoding')->willReturn(false);
128+
$response->hasHeader('Content-Encoding')->shouldNotBeCalled();
129+
130+
$this->handleRequest($request, $next, function () {});
131+
}
132+
}

src/ContentLengthPlugin.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Http\Client\Plugin;
4+
5+
use Http\Encoding\ChunkStream;
6+
use Psr\Http\Message\RequestInterface;
7+
8+
/**
9+
* Allow to set the correct content length header on the request or to transfer it as a chunk if not possible
10+
*
11+
* @author Joel Wurtz <[email protected]>
12+
*/
13+
class ContentLengthPlugin implements Plugin
14+
{
15+
/**
16+
* {@inheritDoc}
17+
*/
18+
public function handleRequest(RequestInterface $request, callable $next, callable $first)
19+
{
20+
if (!$request->hasHeader('Content-Length')) {
21+
$stream = $request->getBody();
22+
23+
// Cannot determine the size so we use a chunk stream
24+
if (null === $stream->getSize()) {
25+
$stream = new ChunkStream($stream);
26+
$request = $request->withBody($stream);
27+
} else {
28+
$request = $request->withHeader('Content-Length', $stream->getSize());
29+
}
30+
}
31+
32+
return $next($request);
33+
}
34+
}

src/DecoderPlugin.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace Http\Client\Plugin;
4+
5+
use Http\Client\Exception;
6+
use Http\Encoding\DechunkStream;
7+
use Http\Encoding\DecompressStream;
8+
use Http\Encoding\GzipDecodeStream;
9+
use Http\Encoding\InflateStream;
10+
use Psr\Http\Message\RequestInterface;
11+
use Psr\Http\Message\ResponseInterface;
12+
use Psr\Http\Message\StreamInterface;
13+
14+
/**
15+
* Allow to decode response body with a chunk, deflate, compress or gzip encoding
16+
*
17+
* @author Joel Wurtz <[email protected]>
18+
*/
19+
class DecoderPlugin implements Plugin
20+
{
21+
/**
22+
* @var bool Whether this plugin decode stream with value in the Content-Encoding header (default to true).
23+
*
24+
* If set to false only the Transfer-Encoding header will be used.
25+
*/
26+
private $useContentEncoding;
27+
28+
/**
29+
* @param bool $useContentEncoding Whether this plugin decode stream with value in the Content-Encoding header (default to true).
30+
*
31+
* If set to false only the Transfer-Encoding header will be used.
32+
*/
33+
public function __construct($useContentEncoding = true)
34+
{
35+
$this->useContentEncoding = $useContentEncoding;
36+
}
37+
38+
/**
39+
* {@inheritDoc}
40+
*/
41+
public function handleRequest(RequestInterface $request, callable $next, callable $first)
42+
{
43+
$request = $request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked']);
44+
45+
if ($this->useContentEncoding) {
46+
$request = $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress']);
47+
}
48+
49+
return $next($request)->then(function (ResponseInterface $response) {
50+
return $this->decodeResponse($response);
51+
});
52+
}
53+
54+
/**
55+
* Decode a response body given its Transfer-Encoding or Content-Encoding value
56+
*
57+
* @param ResponseInterface $response Response to decode
58+
*
59+
* @return ResponseInterface New response decoded
60+
*/
61+
private function decodeResponse(ResponseInterface $response)
62+
{
63+
$response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response);
64+
65+
if ($this->useContentEncoding) {
66+
$response = $this->decodeOnEncodingHeader('Content-Encoding', $response);
67+
}
68+
69+
return $response;
70+
}
71+
72+
/**
73+
* Decode a response on a specific header (content encoding or transfer encoding mainly)
74+
*
75+
* @param string $headerName Name of the header
76+
* @param ResponseInterface $response Response
77+
*
78+
* @return ResponseInterface A new instance of the response decoded
79+
*/
80+
private function decodeOnEncodingHeader($headerName, ResponseInterface $response)
81+
{
82+
if ($response->hasHeader($headerName)) {
83+
$encodings = $response->getHeader($headerName);
84+
$newEncodings = [];
85+
86+
while ($encoding = array_pop($encodings)) {
87+
$stream = $this->decorateStream($encoding, $response->getBody());
88+
89+
if (false === $stream) {
90+
array_unshift($newEncodings, $encoding);
91+
92+
continue;
93+
}
94+
95+
$response = $response->withBody($stream);
96+
}
97+
98+
$response = $response->withHeader($headerName, $newEncodings);
99+
}
100+
101+
return $response;
102+
}
103+
104+
/**
105+
* Decorate a stream given an encoding
106+
*
107+
* @param string $encoding
108+
* @param StreamInterface $stream
109+
*
110+
* @return StreamInterface|false A new stream interface or false if encoding is not supported
111+
*/
112+
private function decorateStream($encoding, StreamInterface $stream)
113+
{
114+
if (strtolower($encoding) == 'chunked') {
115+
return new DechunkStream($stream);
116+
}
117+
118+
if (strtolower($encoding) == 'compress') {
119+
return new DecompressStream($stream);
120+
}
121+
122+
if (strtolower($encoding) == 'deflate') {
123+
return new InflateStream($stream);
124+
}
125+
126+
if (strtolower($encoding) == 'gzip') {
127+
return new GzipDecodeStream($stream);
128+
}
129+
130+
return false;
131+
}
132+
}

0 commit comments

Comments
 (0)