Skip to content

Commit 0bf2609

Browse files
committed
feat: Client now use ReactPHP Browser object.
Also upgrade all the test suite to match the new behaviour.
1 parent 2f516e5 commit 0bf2609

File tree

6 files changed

+118
-269
lines changed

6 files changed

+118
-269
lines changed

src/Client.php

Lines changed: 15 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,22 @@
44

55
use Http\Client\HttpClient;
66
use Http\Client\HttpAsyncClient;
7-
use Http\Client\Exception\HttpException;
8-
use Http\Client\Exception\RequestException;
9-
use Http\Discovery\MessageFactoryDiscovery;
10-
use Http\Discovery\StreamFactoryDiscovery;
11-
use Http\Message\ResponseFactory;
12-
use Http\Message\StreamFactory;
137
use Psr\Http\Message\RequestInterface;
148
use Psr\Http\Message\ResponseInterface;
15-
use Psr\Http\Message\StreamInterface;
169
use React\EventLoop\LoopInterface;
17-
use React\HttpClient\Client as ReactClient;
18-
use React\HttpClient\Request as ReactRequest;
19-
use React\HttpClient\Response as ReactResponse;
10+
use React\Http\Browser as ReactBrowser;
2011

2112
/**
2213
* Client for the React promise implementation.
2314
*
24-
* @author Stéphane Hulard <stephane@hlrd.me>
15+
* @author Stéphane Hulard <s.hulard@chstudio.fr>
2516
*/
2617
class Client implements HttpClient, HttpAsyncClient
2718
{
2819
/**
2920
* React HTTP client.
3021
*
31-
* @var Client
22+
* @var ReactBrowser
3223
*/
3324
private $client;
3425

@@ -39,41 +30,19 @@ class Client implements HttpClient, HttpAsyncClient
3930
*/
4031
private $loop;
4132

42-
/**
43-
* @var ResponseFactory
44-
*/
45-
private $responseFactory;
46-
47-
/**
48-
* @var StreamFactory
49-
*/
50-
private $streamFactory;
51-
5233
/**
5334
* Initialize the React client.
54-
*
55-
* @param ResponseFactory|null $responseFactory
56-
* @param LoopInterface|null $loop
57-
* @param ReactClient|null $client
58-
* @param StreamFactory|null $streamFactory
5935
*/
6036
public function __construct(
61-
ResponseFactory $responseFactory = null,
6237
LoopInterface $loop = null,
63-
ReactClient $client = null,
64-
StreamFactory $streamFactory = null
38+
ReactBrowser $client = null
6539
) {
6640
if (null !== $client && null === $loop) {
67-
throw new \RuntimeException(
68-
'You must give a LoopInterface instance with the Client'
69-
);
41+
throw new \RuntimeException('You must give a LoopInterface instance with the Client');
7042
}
7143

7244
$this->loop = $loop ?: ReactFactory::buildEventLoop();
7345
$this->client = $client ?: ReactFactory::buildHttpClient($this->loop);
74-
75-
$this->responseFactory = $responseFactory ?: MessageFactoryDiscovery::find();
76-
$this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find();
7746
}
7847

7948
/**
@@ -91,91 +60,17 @@ public function sendRequest(RequestInterface $request): ResponseInterface
9160
*/
9261
public function sendAsyncRequest(RequestInterface $request)
9362
{
94-
$reactRequest = $this->buildReactRequest($request);
95-
$promise = new Promise($this->loop);
96-
97-
$reactRequest->on('error', function (\Exception $error) use ($promise, $request) {
98-
$promise->reject(new RequestException(
99-
$error->getMessage(),
100-
$request,
101-
$error
102-
));
103-
});
104-
105-
$reactRequest->on('response', function (ReactResponse $reactResponse = null) use ($promise, $request) {
106-
$bodyStream = $this->streamFactory->createStream();
107-
$reactResponse->on('data', function ($data) use (&$bodyStream) {
108-
$bodyStream->write((string) $data);
109-
});
110-
111-
$reactResponse->on('end', function (\Exception $error = null) use ($promise, $request, $reactResponse, &$bodyStream) {
112-
$response = $this->buildResponse(
113-
$reactResponse,
114-
$bodyStream
115-
);
116-
if (null !== $error) {
117-
$promise->reject(new HttpException(
118-
$error->getMessage(),
119-
$request,
120-
$response,
121-
$error
122-
));
123-
} else {
124-
$promise->resolve($response);
125-
}
126-
});
127-
});
128-
129-
$reactRequest->end((string) $request->getBody());
130-
131-
return $promise;
132-
}
133-
134-
/**
135-
* Build a React request from the PSR7 RequestInterface.
136-
*
137-
* @param RequestInterface $request
138-
*
139-
* @return ReactRequest
140-
*/
141-
private function buildReactRequest(RequestInterface $request)
142-
{
143-
$headers = [];
144-
145-
foreach ($request->getHeaders() as $name => $value) {
146-
$headers[$name] = (is_array($value) ? $value[0] : $value);
147-
}
148-
149-
$reactRequest = $this->client->request(
150-
$request->getMethod(),
151-
(string) $request->getUri(),
152-
$headers,
153-
$request->getProtocolVersion()
63+
$promise = new Promise(
64+
$this->client->request(
65+
$request->getMethod(),
66+
$request->getUri(),
67+
$request->getHeaders(),
68+
$request->getBody()
69+
),
70+
$this->loop,
71+
$request
15472
);
15573

156-
return $reactRequest;
157-
}
158-
159-
/**
160-
* Transform a React Response to a valid PSR7 ResponseInterface instance.
161-
*
162-
* @param ReactResponse $response
163-
* @param StreamInterface $body
164-
*
165-
* @return ResponseInterface
166-
*/
167-
private function buildResponse(
168-
ReactResponse $response,
169-
StreamInterface $body
170-
) {
171-
$body->rewind();
172-
173-
return $this->responseFactory->createResponse(
174-
$response->getCode(),
175-
$response->getReasonPhrase(),
176-
$response->getHeaders(),
177-
$body,
178-
$response->getVersion()
179-
);
74+
return $promise;
18075
}
18176
}

src/Promise.php

Lines changed: 46 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22

33
namespace Http\Adapter\React;
44

5-
use React\EventLoop\LoopInterface;
6-
use Http\Client\Exception;
5+
use Http\Client\Exception as HttplugException;
76
use Http\Promise\Promise as HttpPromise;
7+
use Psr\Http\Message\RequestInterface;
88
use Psr\Http\Message\ResponseInterface;
9+
use React\EventLoop\LoopInterface;
10+
use React\Promise\PromiseInterface;
11+
use RuntimeException;
12+
use UnexpectedValueException;
913

1014
/**
1115
* React promise adapter implementation.
1216
*
13-
* @author Stéphane Hulard <stephane@hlrd.me>
17+
* @author Stéphane Hulard <s.hulard@chstudio.fr>
1418
*
1519
* @internal
1620
*/
@@ -38,128 +42,62 @@ final class Promise implements HttpPromise
3842
private $exception;
3943

4044
/**
41-
* @var callable|null
42-
*/
43-
private $onFulfilled;
44-
45-
/**
46-
* @var callable|null
47-
*/
48-
private $onRejected;
49-
50-
/**
51-
* React Event Loop used for synchronous processing.
45+
* HTTP Request.
5246
*
53-
* @var LoopInterface
47+
* @var RequestInterface
5448
*/
55-
private $loop;
56-
57-
public function __construct(LoopInterface $loop)
58-
{
59-
$this->loop = $loop;
60-
}
49+
private $request;
6150

6251
/**
63-
* Allow to apply callable when the promise resolve.
64-
*
65-
* @param callable|null $onFulfilled
66-
* @param callable|null $onRejected
52+
* Adapted ReactPHP promise.
6753
*
68-
* @return Promise
54+
* @var PromiseInterface
6955
*/
70-
public function then(callable $onFulfilled = null, callable $onRejected = null)
71-
{
72-
$newPromise = new self($this->loop);
73-
74-
$onFulfilled = null !== $onFulfilled ? $onFulfilled : function (ResponseInterface $response) {
75-
return $response;
76-
};
77-
78-
$onRejected = null !== $onRejected ? $onRejected : function (Exception $exception) {
79-
throw $exception;
80-
};
81-
82-
$this->onFulfilled = function (ResponseInterface $response) use ($onFulfilled, $newPromise) {
83-
try {
84-
$return = $onFulfilled($response);
85-
86-
$newPromise->resolve(null !== $return ? $return : $response);
87-
} catch (Exception $exception) {
88-
$newPromise->reject($exception);
89-
}
90-
};
91-
92-
$this->onRejected = function (Exception $exception) use ($onRejected, $newPromise) {
93-
try {
94-
$newPromise->resolve($onRejected($exception));
95-
} catch (Exception $exception) {
96-
$newPromise->reject($exception);
97-
}
98-
};
99-
100-
if (HttpPromise::FULFILLED === $this->state) {
101-
$this->doResolve($this->response);
102-
}
103-
104-
if (HttpPromise::REJECTED === $this->state) {
105-
$this->doReject($this->exception);
106-
}
107-
108-
return $newPromise;
109-
}
56+
private $promise;
11057

11158
/**
112-
* Resolve this promise.
59+
* ReactPHP LoopInterface.
11360
*
114-
* @param ResponseInterface $response
115-
*
116-
* @internal
61+
* @var LoopInterface
11762
*/
118-
public function resolve(ResponseInterface $response)
119-
{
120-
if (HttpPromise::PENDING !== $this->state) {
121-
throw new \RuntimeException('Promise is already resolved');
122-
}
123-
124-
$this->state = HttpPromise::FULFILLED;
125-
$this->response = $response;
126-
$this->doResolve($response);
127-
}
63+
private $loop;
12864

129-
private function doResolve(ResponseInterface $response)
65+
public function __construct(PromiseInterface $promise, LoopInterface $loop, RequestInterface $request)
13066
{
131-
$onFulfilled = $this->onFulfilled;
67+
$this->state = self::PENDING;
13268

133-
if (null !== $onFulfilled) {
134-
$onFulfilled($response);
135-
}
136-
}
137-
138-
/**
139-
* Reject this promise.
140-
*
141-
* @param Exception $exception
142-
*
143-
* @internal
144-
*/
145-
public function reject(Exception $exception)
146-
{
147-
if (HttpPromise::PENDING !== $this->state) {
148-
throw new \RuntimeException('Promise is already resolved');
149-
}
69+
$this->request = $request;
70+
$this->loop = $loop;
71+
$this->promise = $promise->then(
72+
function (?ResponseInterface $response): ResponseInterface {
73+
$this->response = $response;
74+
$this->state = self::FULFILLED;
75+
76+
return $response;
77+
},
78+
/**
79+
* @param mixed $reason
80+
*/
81+
function ($reason): void {
82+
$this->state = self::REJECTED;
83+
84+
if ($reason instanceof HttplugException) {
85+
$this->exception = $reason;
86+
} elseif ($reason instanceof RuntimeException) {
87+
$this->exception = new HttplugException\NetworkException($reason->getMessage(), $this->request, $reason);
88+
} elseif ($reason instanceof \Throwable) {
89+
$this->exception = new HttplugException\TransferException('Invalid exception returned from ReactPHP', 0, $reason);
90+
} else {
91+
$this->exception = new UnexpectedValueException('Reason returned from ReactPHP must be an Exception');
92+
}
15093

151-
$this->state = HttpPromise::REJECTED;
152-
$this->exception = $exception;
153-
$this->doReject($exception);
94+
throw $this->exception;
95+
});
15496
}
15597

156-
private function doReject(Exception $exception)
98+
public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
15799
{
158-
$onRejected = $this->onRejected;
159-
160-
if (null !== $onRejected) {
161-
$onRejected($exception);
162-
}
100+
return new self($this->promise->then($onFulfilled, $onRejected), $this->loop, $this->request);
163101
}
164102

165103
/**

src/ReactFactory.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@ public static function buildEventLoop()
2727
/**
2828
* Build a React Http Client.
2929
*
30-
* @param LoopInterface $loop
30+
* @param LoopInterface $loop
3131
* @param ConnectorInterface|null $connector Only pass this argument if you need to customize DNS
3232
* behaviour.
33-
*
34-
* @return Browser
3533
*/
3634
public static function buildHttpClient(
3735
LoopInterface $loop,

0 commit comments

Comments
 (0)