Skip to content

Commit

Permalink
[HttpClient] Add Psr18Client - aka a PSR-18 adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Mar 1, 2019
1 parent de0ad70 commit d9622bf
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 0 deletions.
2 changes: 2 additions & 0 deletions composer.json
Expand Up @@ -102,8 +102,10 @@
"doctrine/reflection": "~1.0",
"doctrine/doctrine-bundle": "~1.4",
"monolog/monolog": "~1.11",
"nyholm/psr7": "^1.0",
"ocramius/proxy-manager": "~0.4|~1.0|~2.0",
"predis/predis": "~1.1",
"psr/http-client": "^1.0",
"egulias/email-validator": "~1.2,>=1.2.8|~2.0",
"symfony/phpunit-bridge": "~3.4|~4.0",
"symfony/security-acl": "~2.8|~3.0",
Expand Down
107 changes: 107 additions & 0 deletions src/Symfony/Component/HttpClient/Psr18Client.php
@@ -0,0 +1,107 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpClient;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* An adapter to turn a Symfony HttpClientInterface into a PSR-18 ClientInterface.
*
* Run "composer require psr/http-client" to install the base ClientInterface. Run
* "composer require nyholm/psr7" to install an efficient implementation of response
* and stream factories with flex-provided autowiring aliases.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Psr18Client implements ClientInterface
{
private $client;
private $responseFactory;
private $streamFactory;

public function __construct(HttpClientInterface $client, ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
{
$this->client = $client;
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory;
}

public function sendRequest(RequestInterface $request): ResponseInterface
{
try {
$response = $this->client->request($request->getMethod(), (string) $request->getUri(), [
'headers' => $request->getHeaders(),
'body' => (string) $request->getBody(),
'http_version' => '1.0' === $request->getProtocolVersion() ? 1.0 : null,
]);

$psrResponse = $this->responseFactory->createResponse($response->getStatusCode());

foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
$psrResponse = $psrResponse->withAddedHeader($name, $value);
}
}

return $psrResponse->withBody($this->streamFactory->createStream($response->getContent()));
} catch (TransportExceptionInterface $e) {
if ($e instanceof \InvalidArgumentException) {
throw new Psr18RequestException($e, $request);
}

throw new Psr18NetworkException($e, $request);
}
}
}

/**
* @internal
*/
trait Psr18ExceptionTrait
{
private $request;

public function __construct(TransportExceptionInterface $e, RequestInterface $request)
{
parent::__construct($e->getMessage(), 0, $e);
$this->request = $request;
}

public function getRequest(): RequestInterface
{
return $this->request;
}
}

/**
* @internal
*/
class Psr18NetworkException extends \RuntimeException implements NetworkExceptionInterface
{
use Psr18ExceptionTrait;
}

/**
* @internal
*/
class Psr18RequestException extends \InvalidArgumentException implements RequestExceptionInterface
{
use Psr18ExceptionTrait;
}
77 changes: 77 additions & 0 deletions src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php
@@ -0,0 +1,77 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpClient\Tests;

use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\NativeHttpClient;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Component\HttpClient\Psr18NetworkException;
use Symfony\Component\HttpClient\Psr18RequestException;
use Symfony\Contracts\HttpClient\Test\TestHttpServer;

class Psr18ClientTest extends TestCase
{
private static $server;

public static function setUpBeforeClass()
{
TestHttpServer::start();
}

public function testSendRequest()
{
$factory = new Psr17Factory();
$client = new Psr18Client(new NativeHttpClient(), $factory, $factory);

$response = $client->sendRequest($factory->createRequest('GET', 'http://localhost:8057'));

$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/json', $response->getHeaderLine('content-type'));

$body = json_decode((string) $response->getBody(), true);

$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
}

public function testPostRequest()
{
$factory = new Psr17Factory();
$client = new Psr18Client(new NativeHttpClient(), $factory, $factory);

$request = $factory->createRequest('POST', 'http://localhost:8057/post')
->withBody($factory->createStream('foo=0123456789'));

$response = $client->sendRequest($request);
$body = json_decode((string) $response->getBody(), true);

$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
}

public function testNetworkException()
{
$factory = new Psr17Factory();
$client = new Psr18Client(new NativeHttpClient(), $factory, $factory);

$this->expectException(Psr18NetworkException::class);
$client->sendRequest($factory->createRequest('GET', 'http://localhost:8058'));
}

public function testRequestException()
{
$factory = new Psr17Factory();
$client = new Psr18Client(new NativeHttpClient(), $factory, $factory);

$this->expectException(Psr18RequestException::class);
$client->sendRequest($factory->createRequest('BAD.METHOD', 'http://localhost:8057'));
}
}
4 changes: 4 additions & 0 deletions src/Symfony/Component/HttpClient/composer.json
Expand Up @@ -22,6 +22,10 @@
"php": "^7.1.3",
"symfony/contracts": "^1.1"
},
"require-dev": {
"nyholm/psr7": "^1.0",
"psr/http-client": "^1.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },
"exclude-from-classmap": [
Expand Down

0 comments on commit d9622bf

Please sign in to comment.