Skip to content

Decoder and ContentLength plugin #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 18, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ env:
- TEST_COMMAND="composer test"

matrix:
allow_failures:
- php: 7.0
fast_finish: true
include:
- php: 5.4
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"php-http/authentication": "^0.1@dev",
"php-http/cookie": "^0.1@dev",
"symfony/stopwatch": "^2.3",
"psr/log": "^1.0"
"psr/log": "^1.0",
"php-http/encoding": "^0.1@dev"
},
"autoload": {
"psr-4": {
Expand All @@ -32,7 +33,8 @@
"php-http/authentication": "Allow to use the AuthenticationPlugin",
"php-http/cookie": "Allow to use CookiePlugin",
"symfony/stopwatch": "Allow to use the StopwatchPlugin",
"psr/log-implementation": "Allow to use the LoggerPlugin"
"psr/log-implementation": "Allow to use the LoggerPlugin",
"php-http/encoding": "Allow to use the Decoder and Encoder plugin"
},
"scripts": {
"test": "vendor/bin/phpspec run",
Expand Down
47 changes: 47 additions & 0 deletions spec/ContentLengthPluginSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace spec\Http\Client\Plugin;

use PhpSpec\Exception\Example\SkippingException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;

class ContentLengthPluginSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('Http\Client\Plugin\ContentLengthPlugin');
$this->shouldImplement('Http\Client\Plugin\Plugin');
}

function it_adds_content_length_header(RequestInterface $request, StreamInterface $stream)
{
$request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false);
$request->getBody()->shouldBeCalled()->willReturn($stream);
$stream->getSize()->shouldBeCalled()->willReturn(100);
$request->withHeader('Content-Length', 100)->shouldBeCalled()->willReturn($request);

$this->handleRequest($request, function () {}, function () {});
}

function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterface $stream)
{
if(defined('HHVM_VERSION')) {
throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm');
}

$request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false);
$request->getBody()->shouldBeCalled()->willReturn($stream);

$stream->getSize()->shouldBeCalled()->willReturn(null);
$stream->isReadable()->shouldBeCalled()->willReturn(true);
$stream->isWritable()->shouldBeCalled()->willReturn(false);
$stream->eof()->shouldBeCalled()->willReturn(false);

$request->withBody(Argument::type('Http\Encoding\ChunkStream'))->shouldBeCalled()->willReturn($request);

$this->handleRequest($request, function () {}, function () {});
}
}
132 changes: 132 additions & 0 deletions spec/DecoderPluginSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace spec\Http\Client\Plugin;

use Http\Client\Utils\Promise\FulfilledPromise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use PhpSpec\Exception\Example\SkippingException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class DecoderPluginSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('Http\Client\Plugin\DecoderPlugin');
}

function it_is_a_plugin()
{
$this->shouldImplement('Http\Client\Plugin\Plugin');
}

function it_decodes(RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
{
if(defined('HHVM_VERSION')) {
throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm');
}

$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request);
$next = function () use($response) {
return new FulfilledPromise($response->getWrappedObject());
};

$response->hasHeader('Transfer-Encoding')->willReturn(true);
$response->getHeader('Transfer-Encoding')->willReturn(['chunked']);
$response->getBody()->willReturn($stream);
$response->withBody(Argument::type('Http\Encoding\DechunkStream'))->willReturn($response);
$response->withHeader('Transfer-Encoding', [])->willReturn($response);
$response->hasHeader('Content-Encoding')->willReturn(false);

$stream->isReadable()->willReturn(true);
$stream->isWritable()->willReturn(false);
$stream->eof()->willReturn(false);

$this->handleRequest($request, $next, function () {});
}

function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
{
$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request);
$next = function () use($response) {
return new FulfilledPromise($response->getWrappedObject());
};

$response->hasHeader('Transfer-Encoding')->willReturn(false);
$response->hasHeader('Content-Encoding')->willReturn(true);
$response->getHeader('Content-Encoding')->willReturn(['gzip']);
$response->getBody()->willReturn($stream);
$response->withBody(Argument::type('Http\Encoding\GzipDecodeStream'))->willReturn($response);
$response->withHeader('Content-Encoding', [])->willReturn($response);

$stream->isReadable()->willReturn(true);
$stream->isWritable()->willReturn(false);
$stream->eof()->willReturn(false);

$this->handleRequest($request, $next, function () {});
}

function it_decodes_deflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
{
$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request);
$next = function () use($response) {
return new FulfilledPromise($response->getWrappedObject());
};

$response->hasHeader('Transfer-Encoding')->willReturn(false);
$response->hasHeader('Content-Encoding')->willReturn(true);
$response->getHeader('Content-Encoding')->willReturn(['deflate']);
$response->getBody()->willReturn($stream);
$response->withBody(Argument::type('Http\Encoding\InflateStream'))->willReturn($response);
$response->withHeader('Content-Encoding', [])->willReturn($response);

$stream->isReadable()->willReturn(true);
$stream->isWritable()->willReturn(false);
$stream->eof()->willReturn(false);

$this->handleRequest($request, $next, function () {});
}

function it_decodes_inflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
{
$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request);
$next = function () use($response) {
return new FulfilledPromise($response->getWrappedObject());
};

$response->hasHeader('Transfer-Encoding')->willReturn(false);
$response->hasHeader('Content-Encoding')->willReturn(true);
$response->getHeader('Content-Encoding')->willReturn(['compress']);
$response->getBody()->willReturn($stream);
$response->withBody(Argument::type('Http\Encoding\DecompressStream'))->willReturn($response);
$response->withHeader('Content-Encoding', [])->willReturn($response);

$stream->isReadable()->willReturn(true);
$stream->isWritable()->willReturn(false);
$stream->eof()->willReturn(false);

$this->handleRequest($request, $next, function () {});
}

function it_does_not_decode_with_content_encoding(RequestInterface $request, ResponseInterface $response)
{
$this->beConstructedWith(false);

$request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request);
$request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldNotBeCalled();
$next = function () use($response) {
return new FulfilledPromise($response->getWrappedObject());
};

$response->hasHeader('Transfer-Encoding')->willReturn(false);
$response->hasHeader('Content-Encoding')->shouldNotBeCalled();

$this->handleRequest($request, $next, function () {});
}
}
34 changes: 34 additions & 0 deletions src/ContentLengthPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Http\Client\Plugin;

use Http\Encoding\ChunkStream;
use Psr\Http\Message\RequestInterface;

/**
* Allow to set the correct content length header on the request or to transfer it as a chunk if not possible
*
* @author Joel Wurtz <[email protected]>
*/
class ContentLengthPlugin implements Plugin
{
/**
* {@inheritDoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
if (!$request->hasHeader('Content-Length')) {
$stream = $request->getBody();

// Cannot determine the size so we use a chunk stream
if (null === $stream->getSize()) {
$stream = new ChunkStream($stream);
$request = $request->withBody($stream);
} else {
$request = $request->withHeader('Content-Length', $stream->getSize());
}
}

return $next($request);
}
}
132 changes: 132 additions & 0 deletions src/DecoderPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace Http\Client\Plugin;

use Http\Client\Exception;
use Http\Encoding\DechunkStream;
use Http\Encoding\DecompressStream;
use Http\Encoding\GzipDecodeStream;
use Http\Encoding\InflateStream;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;

/**
* Allow to decode response body with a chunk, deflate, compress or gzip encoding
*
* @author Joel Wurtz <[email protected]>
*/
class DecoderPlugin implements Plugin
{
/**
* @var bool Whether this plugin decode stream with value in the Content-Encoding header (default to true).
*
* If set to false only the Transfer-Encoding header will be used.
*/
private $useContentEncoding;

/**
* @param bool $useContentEncoding Whether this plugin decode stream with value in the Content-Encoding header (default to true).
*
* If set to false only the Transfer-Encoding header will be used.
*/
public function __construct($useContentEncoding = true)
{
$this->useContentEncoding = $useContentEncoding;
}

/**
* {@inheritDoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$request = $request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked']);

if ($this->useContentEncoding) {
$request = $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress']);
}

return $next($request)->then(function (ResponseInterface $response) {
return $this->decodeResponse($response);
});
}

/**
* Decode a response body given its Transfer-Encoding or Content-Encoding value
*
* @param ResponseInterface $response Response to decode
*
* @return ResponseInterface New response decoded
*/
private function decodeResponse(ResponseInterface $response)
{
$response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response);

if ($this->useContentEncoding) {
$response = $this->decodeOnEncodingHeader('Content-Encoding', $response);
}

return $response;
}

/**
* Decode a response on a specific header (content encoding or transfer encoding mainly)
*
* @param string $headerName Name of the header
* @param ResponseInterface $response Response
*
* @return ResponseInterface A new instance of the response decoded
*/
private function decodeOnEncodingHeader($headerName, ResponseInterface $response)
{
if ($response->hasHeader($headerName)) {
$encodings = $response->getHeader($headerName);
$newEncodings = [];

while ($encoding = array_pop($encodings)) {
$stream = $this->decorateStream($encoding, $response->getBody());

if (false === $stream) {
array_unshift($newEncodings, $encoding);

continue;
}

$response = $response->withBody($stream);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplication alert. can we move this to a private method instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


$response = $response->withHeader($headerName, $newEncodings);
}

return $response;
}

/**
* Decorate a stream given an encoding
*
* @param string $encoding
* @param StreamInterface $stream
*
* @return StreamInterface|false A new stream interface or false if encoding is not supported
*/
private function decorateStream($encoding, StreamInterface $stream)
{
if (strtolower($encoding) == 'chunked') {
return new DechunkStream($stream);
}

if (strtolower($encoding) == 'compress') {
return new DecompressStream($stream);
}

if (strtolower($encoding) == 'deflate') {
return new InflateStream($stream);
}

if (strtolower($encoding) == 'gzip') {
return new GzipDecodeStream($stream);
}

return false;
}
}