Skip to content

Commit af934a9

Browse files
authored
Merge pull request #56 from Sammyjo20/feature/async-requests
Feature | Asynchronous requests
2 parents e6a0a2b + 6b1d71b commit af934a9

File tree

6 files changed

+326
-19
lines changed

6 files changed

+326
-19
lines changed

src/Http/SaloonConnector.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use ReflectionClass;
66
use Illuminate\Support\Str;
77
use Illuminate\Support\Collection;
8+
use GuzzleHttp\Promise\PromiseInterface;
89
use Sammyjo20\Saloon\Clients\MockClient;
910
use Sammyjo20\Saloon\Traits\CollectsData;
1011
use Sammyjo20\Saloon\Traits\CollectsConfig;
@@ -160,6 +161,21 @@ public function send(SaloonRequest $request, MockClient $mockClient = null): Sal
160161
return $this->request($request)->send($mockClient);
161162
}
162163

164+
/**
165+
* Send an asynchronous Saloon request with the current instance of the connector.
166+
*
167+
* @param SaloonRequest $request
168+
* @param MockClient|null $mockClient
169+
* @return PromiseInterface
170+
* @throws \GuzzleHttp\Exception\GuzzleException
171+
* @throws \ReflectionException
172+
* @throws \Sammyjo20\Saloon\Exceptions\SaloonException
173+
*/
174+
public function sendAsync(SaloonRequest $request, MockClient $mockClient = null): PromiseInterface
175+
{
176+
return $this->request($request)->sendAsync($mockClient);
177+
}
178+
163179
/**
164180
* Dynamically proxy other methods to try and call a requests.
165181
*

src/Managers/RequestManager.php

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
use Exception;
66
use GuzzleHttp\Psr7\Response;
77
use GuzzleHttp\RequestOptions;
8+
use GuzzleHttp\Client as GuzzleClient;
9+
use Psr\Http\Message\ResponseInterface;
10+
use GuzzleHttp\Promise\PromiseInterface;
811
use Sammyjo20\Saloon\Clients\MockClient;
912
use Sammyjo20\Saloon\Http\SaloonRequest;
13+
use GuzzleHttp\Exception\GuzzleException;
1014
use Sammyjo20\Saloon\Http\SaloonResponse;
1115
use GuzzleHttp\Exception\RequestException;
1216
use Sammyjo20\Saloon\Http\SaloonConnector;
@@ -15,6 +19,7 @@
1519
use Sammyjo20\Saloon\Traits\ManagesPlugins;
1620
use Sammyjo20\Saloon\Clients\BaseMockClient;
1721
use Sammyjo20\Saloon\Traits\CollectsHeaders;
22+
use GuzzleHttp\Psr7\Request as GuzzleRequest;
1823
use Sammyjo20\Saloon\Traits\CollectsHandlers;
1924
use GuzzleHttp\Exception\BadResponseException;
2025
use Sammyjo20\Saloon\Traits\CollectsQueryParams;
@@ -69,20 +74,29 @@ class RequestManager
6974
*/
7075
protected ?BaseMockClient $mockClient = null;
7176

77+
/**
78+
* Determines if the request should be sent asynchronously.
79+
*
80+
* @var bool
81+
*/
82+
protected bool $asynchronous = false;
83+
7284
/**
7385
* Construct the request manager
7486
*
7587
* @param SaloonRequest $request
7688
* @param MockClient|null $mockClient
89+
* @param bool $asynchronous
7790
* @throws SaloonMultipleMockMethodsException
7891
* @throws SaloonNoMockResponsesProvidedException
7992
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidConnectorException
8093
*/
81-
public function __construct(SaloonRequest $request, MockClient $mockClient = null)
94+
public function __construct(SaloonRequest $request, MockClient $mockClient = null, bool $asynchronous = false)
8295
{
8396
$this->request = $request;
8497
$this->connector = $request->getConnector();
8598
$this->inLaravelEnvironment = $this->detectLaravel();
99+
$this->asynchronous = $asynchronous;
86100

87101
$this->bootLaravelManager();
88102
$this->bootMockClient($mockClient);
@@ -151,42 +165,97 @@ public function hydrate(): void
151165
}
152166

153167
/**
154-
* Send off the message... 🚀
168+
* Send off the request 🚀
155169
*
156-
* @return SaloonResponse
170+
* @return SaloonResponse|PromiseInterface
171+
* @throws SaloonInvalidResponseClassException
157172
* @throws \GuzzleHttp\Exception\GuzzleException
158173
* @throws \ReflectionException
159174
* @throws \Sammyjo20\Saloon\Exceptions\SaloonDuplicateHandlerException
160175
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidConnectorException
161176
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidHandlerException
162-
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidResponseClassException
163177
* @throws \Sammyjo20\Saloon\Exceptions\SaloonMissingMockException
164178
*/
165179
public function send()
166180
{
167-
// Hydrate the manager with juicy headers, config, interceptors, handlers...
181+
// Let's firstly hydrate the request manager, which will retrieve all the attributes
182+
// from the request and connector and build them up inside the request.
168183

169184
$this->hydrate();
170185

171-
// Build up the config!
186+
// Next, we will retrieve our Guzzle client, request and build up the request options
187+
// in a way Guzzle will understand.
172188

189+
$client = $this->createGuzzleClient();
190+
$request = $this->createGuzzleRequest();
173191
$requestOptions = $this->buildRequestOptions();
174192

175-
// Boot up our Guzzle client... This will also boot up handlers...
176-
177-
$client = $this->createGuzzleClient();
193+
// Finally, we will send the request! If the asynchronous mode has been requested,
194+
// we will return a promise with the Saloon response, however if not then we will
195+
// just return a response.
178196

179-
// Send the request! 🚀
197+
return $this->asynchronous === true
198+
? $this->sendAsyncRequest($client, $request, $requestOptions)
199+
: $this->sendSyncRequest($client, $request, $requestOptions);
200+
}
180201

202+
/**
203+
* Send a traditional, synchronous request.
204+
*
205+
* @param GuzzleClient $client
206+
* @param GuzzleRequest $request
207+
* @param array $requestOptions
208+
* @return SaloonResponse
209+
* @throws SaloonInvalidResponseClassException
210+
* @throws \GuzzleHttp\Exception\GuzzleException
211+
* @throws \ReflectionException
212+
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidConnectorException
213+
*/
214+
private function sendSyncRequest(GuzzleClient $client, GuzzleRequest $request, array $requestOptions): SaloonResponse
215+
{
181216
try {
182-
$guzzleResponse = $client->send($this->createGuzzleRequest(), $requestOptions);
217+
$guzzleResponse = $client->send($request, $requestOptions);
183218
} catch (BadResponseException $exception) {
184219
return $this->createResponse($requestOptions, $exception->getResponse(), $exception);
185220
}
186221

187222
return $this->createResponse($requestOptions, $guzzleResponse);
188223
}
189224

225+
/**
226+
* Prepare an asynchronous request, and return a promise.
227+
*
228+
* @param GuzzleClient $client
229+
* @param GuzzleRequest $request
230+
* @param array $requestOptions
231+
* @return PromiseInterface
232+
*/
233+
private function sendAsyncRequest(GuzzleClient $client, GuzzleRequest $request, array $requestOptions): PromiseInterface
234+
{
235+
return $client->sendAsync($request, $requestOptions)
236+
->then(
237+
function (ResponseInterface $guzzleResponse) use ($requestOptions) {
238+
// Instead of the promise returning a Guzzle response, we want to return
239+
// a Saloon response.
240+
241+
return $this->createResponse($requestOptions, $guzzleResponse);
242+
},
243+
function (GuzzleException $guzzleException) use ($requestOptions) {
244+
// If the exception was a connect exception, we should return that in the
245+
// promise instead rather than trying to convert it into a
246+
// SaloonResponse, since there was no response.
247+
248+
if (! $guzzleException instanceof RequestException) {
249+
throw $guzzleException;
250+
}
251+
252+
$response = $this->createResponse($requestOptions, $guzzleException->getResponse(), $guzzleException);
253+
254+
throw $response->toException();
255+
}
256+
);
257+
}
258+
190259
/**
191260
* Create a response.
192261
*

src/Traits/SendsRequests.php

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,56 @@
22

33
namespace Sammyjo20\Saloon\Traits;
44

5+
use GuzzleHttp\Promise\PromiseInterface;
56
use Sammyjo20\Saloon\Clients\MockClient;
67
use Sammyjo20\Saloon\Http\SaloonResponse;
78
use Sammyjo20\Saloon\Managers\RequestManager;
9+
use Sammyjo20\Saloon\Exceptions\SaloonException;
810

911
trait SendsRequests
1012
{
1113
/**
12-
* Send the request.
14+
* Send the request synchronously.
1315
*
1416
* @param MockClient|null $mockClient
17+
* @param bool $asynchronous
1518
* @return SaloonResponse
19+
* @throws SaloonException
1620
* @throws \GuzzleHttp\Exception\GuzzleException
1721
* @throws \ReflectionException
18-
* @throws \Sammyjo20\Saloon\Exceptions\SaloonException
1922
*/
20-
public function send(MockClient $mockClient = null): SaloonResponse
23+
public function send(MockClient $mockClient = null, bool $asynchronous = false): SaloonResponse
2124
{
2225
// 🚀 ... 🌑 ... 💫
2326

24-
return $this->getRequestManager($mockClient)->send();
27+
return $this->getRequestManager($mockClient, $asynchronous)->send();
28+
}
29+
30+
/**
31+
* Send the request asynchronously
32+
*
33+
* @param MockClient|null $mockClient
34+
* @return PromiseInterface
35+
* @throws SaloonException
36+
* @throws \GuzzleHttp\Exception\GuzzleException
37+
* @throws \ReflectionException
38+
* @throws \Sammyjo20\Saloon\Exceptions\SaloonException
39+
*/
40+
public function sendAsync(MockClient $mockClient = null): PromiseInterface
41+
{
42+
return $this->getRequestManager($mockClient, true)->send();
2543
}
2644

2745
/**
2846
* Create a request manager instance from the request.
2947
*
3048
* @param MockClient|null $mockClient
49+
* @param bool $asynchronous
3150
* @return RequestManager
32-
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidConnectorException
33-
* @throws \Sammyjo20\Saloon\Exceptions\SaloonMultipleMockMethodsException
51+
* @throws \Sammyjo20\Saloon\Exceptions\SaloonException
3452
*/
35-
public function getRequestManager(MockClient $mockClient = null): RequestManager
53+
public function getRequestManager(MockClient $mockClient = null, bool $asynchronous = false): RequestManager
3654
{
37-
return new RequestManager($this, $mockClient);
55+
return new RequestManager($this, $mockClient, $asynchronous);
3856
}
3957
}

tests/Feature/AsyncRequestTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
use GuzzleHttp\Promise\Promise;
4+
use Sammyjo20\Saloon\Http\MockResponse;
5+
use Sammyjo20\Saloon\Clients\MockClient;
6+
use Sammyjo20\Saloon\Http\SaloonResponse;
7+
use Sammyjo20\Saloon\Exceptions\SaloonRequestException;
8+
use Sammyjo20\Saloon\Tests\Fixtures\Responses\UserData;
9+
use Sammyjo20\Saloon\Tests\Fixtures\Requests\UserRequest;
10+
use Sammyjo20\Saloon\Tests\Fixtures\Requests\ErrorRequest;
11+
use Sammyjo20\Saloon\Tests\Fixtures\Responses\UserResponse;
12+
use Sammyjo20\Saloon\Tests\Fixtures\Requests\UserRequestWithCustomResponse;
13+
14+
test('an asynchronous request can be made successfully', function () {
15+
$request = new UserRequest();
16+
$promise = $request->sendAsync();
17+
18+
expect($promise)->toBeInstanceOf(Promise::class);
19+
20+
$response = $promise->wait();
21+
22+
expect($response)->toBeInstanceOf(SaloonResponse::class);
23+
24+
$data = $response->json();
25+
26+
expect($response->isMocked())->toBeFalse();
27+
expect($response->status())->toEqual(200);
28+
29+
expect($data)->toEqual([
30+
'name' => 'Sammyjo20',
31+
'actual_name' => 'Sam',
32+
'twitter' => '@carre_sam',
33+
]);
34+
});
35+
36+
test('an asynchronous request will return a custom response', function () {
37+
$mockClient = new MockClient([MockResponse::make(['foo' => 'bar'], 200)]);
38+
$request = new UserRequestWithCustomResponse();
39+
40+
$promise = $request->sendAsync($mockClient);
41+
$response = $promise->wait();
42+
43+
expect($response)->toBeInstanceOf(UserResponse::class);
44+
expect($response)->customCastMethod()->toBeInstanceOf(UserData::class);
45+
expect($response)->foo()->toBe('bar');
46+
});
47+
48+
test('an asynchronous request can handle an exception properly', function () {
49+
$request = new ErrorRequest();
50+
$promise = $request->sendAsync();
51+
52+
$this->expectException(SaloonRequestException::class);
53+
54+
$promise->wait();
55+
});

0 commit comments

Comments
 (0)