Skip to content

Commit f64f2ab

Browse files
[HttpClient] Add Psr18Client - aka a PSR-18 adapter
1 parent eabe04c commit f64f2ab

File tree

7 files changed

+190
-1193
lines changed

7 files changed

+190
-1193
lines changed

composer.json

+2
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@
102102
"doctrine/reflection": "~1.0",
103103
"doctrine/doctrine-bundle": "~1.4",
104104
"monolog/monolog": "~1.11",
105+
"nyholm/psr7": "^1.0",
105106
"ocramius/proxy-manager": "~0.4|~1.0|~2.0",
106107
"predis/predis": "~1.1",
108+
"psr/http-client": "^1.0",
107109
"egulias/email-validator": "~1.2,>=1.2.8|~2.0",
108110
"symfony/phpunit-bridge": "~3.4|~4.0",
109111
"symfony/security-acl": "~2.8|~3.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient;
13+
14+
use Psr\Http\Client\ClientInterface;
15+
use Psr\Http\Client\NetworkExceptionInterface;
16+
use Psr\Http\Client\RequestExceptionInterface;
17+
use Psr\Http\Message\RequestInterface;
18+
use Psr\Http\Message\ResponseFactoryInterface;
19+
use Psr\Http\Message\ResponseInterface;
20+
use Psr\Http\Message\StreamFactoryInterface;
21+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
22+
use Symfony\Contracts\HttpClient\HttpClientInterface;
23+
24+
/**
25+
* An adapter to turn a Symfony HttpClientInterface into a PSR-18 ClientInterface.
26+
*
27+
* Run "composer require psr/http-client" to install the base ClientInterface. Run
28+
* "composer require nyholm/psr7" to install an efficient implementation of response
29+
* and stream factories with flex-provided autowiring aliases.
30+
*
31+
* @author Nicolas Grekas <p@tchwork.com>
32+
*
33+
* @experimental in 4.3
34+
*/
35+
final class Psr18Client implements ClientInterface
36+
{
37+
private $client;
38+
private $responseFactory;
39+
private $streamFactory;
40+
41+
public function __construct(HttpClientInterface $client, ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
42+
{
43+
$this->client = $client;
44+
$this->responseFactory = $responseFactory;
45+
$this->streamFactory = $streamFactory;
46+
}
47+
48+
public function sendRequest(RequestInterface $request): ResponseInterface
49+
{
50+
try {
51+
$response = $this->client->request($request->getMethod(), (string) $request->getUri(), [
52+
'headers' => $request->getHeaders(),
53+
'body' => (string) $request->getBody(),
54+
'http_version' => '1.0' === $request->getProtocolVersion() ? '1.0' : null,
55+
]);
56+
57+
$psrResponse = $this->responseFactory->createResponse($response->getStatusCode());
58+
59+
foreach ($response->getHeaders() as $name => $values) {
60+
foreach ($values as $value) {
61+
$psrResponse = $psrResponse->withAddedHeader($name, $value);
62+
}
63+
}
64+
65+
return $psrResponse->withBody($this->streamFactory->createStream($response->getContent()));
66+
} catch (TransportExceptionInterface $e) {
67+
if ($e instanceof \InvalidArgumentException) {
68+
throw new Psr18RequestException($e, $request);
69+
}
70+
71+
throw new Psr18NetworkException($e, $request);
72+
}
73+
}
74+
}
75+
76+
/**
77+
* @internal
78+
*/
79+
trait Psr18ExceptionTrait
80+
{
81+
private $request;
82+
83+
public function __construct(TransportExceptionInterface $e, RequestInterface $request)
84+
{
85+
parent::__construct($e->getMessage(), 0, $e);
86+
$this->request = $request;
87+
}
88+
89+
public function getRequest(): RequestInterface
90+
{
91+
return $this->request;
92+
}
93+
}
94+
95+
/**
96+
* @internal
97+
*/
98+
class Psr18NetworkException extends \RuntimeException implements NetworkExceptionInterface
99+
{
100+
use Psr18ExceptionTrait;
101+
}
102+
103+
/**
104+
* @internal
105+
*/
106+
class Psr18RequestException extends \InvalidArgumentException implements RequestExceptionInterface
107+
{
108+
use Psr18ExceptionTrait;
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Tests;
13+
14+
use Nyholm\Psr7\Factory\Psr17Factory;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\HttpClient\NativeHttpClient;
17+
use Symfony\Component\HttpClient\Psr18Client;
18+
use Symfony\Component\HttpClient\Psr18NetworkException;
19+
use Symfony\Component\HttpClient\Psr18RequestException;
20+
use Symfony\Contracts\HttpClient\Test\TestHttpServer;
21+
22+
class Psr18ClientTest extends TestCase
23+
{
24+
private static $server;
25+
26+
public static function setUpBeforeClass()
27+
{
28+
TestHttpServer::start();
29+
}
30+
31+
public function testSendRequest()
32+
{
33+
$factory = new Psr17Factory();
34+
$client = new Psr18Client(new NativeHttpClient(), $factory, $factory);
35+
36+
$response = $client->sendRequest($factory->createRequest('GET', 'http://localhost:8057'));
37+
38+
$this->assertSame(200, $response->getStatusCode());
39+
$this->assertSame('application/json', $response->getHeaderLine('content-type'));
40+
41+
$body = json_decode((string) $response->getBody(), true);
42+
43+
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
44+
}
45+
46+
public function testPostRequest()
47+
{
48+
$factory = new Psr17Factory();
49+
$client = new Psr18Client(new NativeHttpClient(), $factory, $factory);
50+
51+
$request = $factory->createRequest('POST', 'http://localhost:8057/post')
52+
->withBody($factory->createStream('foo=0123456789'));
53+
54+
$response = $client->sendRequest($request);
55+
$body = json_decode((string) $response->getBody(), true);
56+
57+
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
58+
}
59+
60+
public function testNetworkException()
61+
{
62+
$factory = new Psr17Factory();
63+
$client = new Psr18Client(new NativeHttpClient(), $factory, $factory);
64+
65+
$this->expectException(Psr18NetworkException::class);
66+
$client->sendRequest($factory->createRequest('GET', 'http://localhost:8058'));
67+
}
68+
69+
public function testRequestException()
70+
{
71+
$factory = new Psr17Factory();
72+
$client = new Psr18Client(new NativeHttpClient(), $factory, $factory);
73+
74+
$this->expectException(Psr18RequestException::class);
75+
$client->sendRequest($factory->createRequest('BAD.METHOD', 'http://localhost:8057'));
76+
}
77+
}

src/Symfony/Component/HttpClient/composer.json

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"symfony/contracts": "^1.1"
2424
},
2525
"require-dev": {
26+
"nyholm/psr7": "^1.0",
27+
"psr/http-client": "^1.0",
2628
"symfony/process": "~4.2"
2729
},
2830
"autoload": {

0 commit comments

Comments
 (0)