Skip to content

Commit 7a19bea

Browse files
[12.x] Add Http::batch (#56946)
* Create HTTP Pool progress hook * Create HTTP Pool catch hook * Create HTTP Pool then hook * Create HTTP Pool finally hook * Update implementation to add Http::batch * Add cancel to batch implementation * Refactor * Update then and finally hooks to not run if batch is cancelled * formatting * formatting * Small fix on handling result * Fix issue with progress callback * Fix issue with progress callback order * Removed not used import * Fix Static Analysis and skip cancel test * Remove cancel feature from Http::batch * Throw exception when adding request to in progress batch * Code Review comments * formatting * remove inconsistent method --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 10bd359 commit 7a19bea

File tree

4 files changed

+666
-0
lines changed

4 files changed

+666
-0
lines changed
Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
<?php
2+
3+
namespace Illuminate\Http\Client;
4+
5+
use Carbon\CarbonImmutable;
6+
use Closure;
7+
use GuzzleHttp\Exception\RequestException;
8+
use GuzzleHttp\Promise\EachPromise;
9+
use GuzzleHttp\Utils;
10+
11+
/**
12+
* @mixin \Illuminate\Http\Client\Factory
13+
*/
14+
class Batch
15+
{
16+
/**
17+
* The factory instance.
18+
*
19+
* @var \Illuminate\Http\Client\Factory
20+
*/
21+
protected $factory;
22+
23+
/**
24+
* The array of requests.
25+
*
26+
* @var array<array-key, \Illuminate\Http\Client\PendingRequest>
27+
*/
28+
protected $requests = [];
29+
30+
/**
31+
* The total number of requests that belong to the batch.
32+
*
33+
* @var non-negative-int
34+
*/
35+
public $totalRequests = 0;
36+
37+
/**
38+
* The total number of requests that are still pending.
39+
*
40+
* @var non-negative-int
41+
*/
42+
public $pendingRequests = 0;
43+
44+
/**
45+
* The total number of requests that have failed.
46+
*
47+
* @var non-negative-int
48+
*/
49+
public $failedRequests = 0;
50+
51+
/**
52+
* The handler function for the Guzzle client.
53+
*
54+
* @var callable
55+
*/
56+
protected $handler;
57+
58+
/**
59+
* The callback to run before the first request from the batch runs.
60+
*
61+
* @var (\Closure($this): void)|null
62+
*/
63+
protected $beforeCallback = null;
64+
65+
/**
66+
* The callback to run after a request from the batch succeeds.
67+
*
68+
* @var (\Closure($this, int|string, \Illuminate\Http\Response): void)|null
69+
*/
70+
protected $progressCallback = null;
71+
72+
/**
73+
* The callback to run after a request from the batch fails.
74+
*
75+
* @var (\Closure($this, int|string, \Illuminate\Http\Response|\Illuminate\Http\Client\RequestException): void)|null
76+
*/
77+
protected $catchCallback = null;
78+
79+
/**
80+
* The callback to run if all the requests from the batch succeeded.
81+
*
82+
* @var (\Closure($this, array<int|string, \Illuminate\Http\Response>): void)|null
83+
*/
84+
protected $thenCallback = null;
85+
86+
/**
87+
* The callback to run after all the requests from the batch finish.
88+
*
89+
* @var (\Closure($this, array<int|string, \Illuminate\Http\Response>): void)|null
90+
*/
91+
protected $finallyCallback = null;
92+
93+
/**
94+
* If the batch already was sent.
95+
*
96+
* @var bool
97+
*/
98+
protected $inProgress = false;
99+
100+
/**
101+
* The date when the batch was created.
102+
*
103+
* @var \Carbon\CarbonImmutable
104+
*/
105+
public $createdAt = null;
106+
107+
/**
108+
* The date when the batch finished.
109+
*
110+
* @var \Carbon\CarbonImmutable|null
111+
*/
112+
public $finishedAt = null;
113+
114+
/**
115+
* Create a new request batch instance.
116+
*
117+
* @return void
118+
*/
119+
public function __construct(?Factory $factory = null)
120+
{
121+
$this->factory = $factory ?: new Factory;
122+
$this->handler = Utils::chooseHandler();
123+
$this->createdAt = new CarbonImmutable;
124+
}
125+
126+
/**
127+
* Add a request to the batch with a key.
128+
*
129+
* @param string $key
130+
* @return \Illuminate\Http\Client\PendingRequest
131+
*
132+
* @throws BatchInProgressException
133+
*/
134+
public function as(string $key)
135+
{
136+
if ($this->inProgress) {
137+
throw new BatchInProgressException();
138+
}
139+
140+
$this->incrementPendingRequests();
141+
142+
return $this->requests[$key] = $this->asyncRequest();
143+
}
144+
145+
/**
146+
* Register a callback to run before the first request from the batch runs.
147+
*
148+
* @param (\Closure($this): void) $callback
149+
* @return Batch
150+
*/
151+
public function before(Closure $callback): self
152+
{
153+
$this->beforeCallback = $callback;
154+
155+
return $this;
156+
}
157+
158+
/**
159+
* Register a callback to run after a request from the batch succeeds.
160+
*
161+
* @param (\Closure($this, int|string, \Illuminate\Http\Response): void) $callback
162+
* @return Batch
163+
*/
164+
public function progress(Closure $callback): self
165+
{
166+
$this->progressCallback = $callback;
167+
168+
return $this;
169+
}
170+
171+
/**
172+
* Register a callback to run after a request from the batch fails.
173+
*
174+
* @param (\Closure($this, int|string, \Illuminate\Http\Response|\Illuminate\Http\Client\RequestException): void) $callback
175+
* @return Batch
176+
*/
177+
public function catch(Closure $callback): self
178+
{
179+
$this->catchCallback = $callback;
180+
181+
return $this;
182+
}
183+
184+
/**
185+
* Register a callback to run after all the requests from the batch succeed.
186+
*
187+
* @param (\Closure($this, array<int|string, \Illuminate\Http\Response>): void) $callback
188+
* @return Batch
189+
*/
190+
public function then(Closure $callback): self
191+
{
192+
$this->thenCallback = $callback;
193+
194+
return $this;
195+
}
196+
197+
/**
198+
* Register a callback to run after all the requests from the batch finish.
199+
*
200+
* @param (\Closure($this, array<int|string, \Illuminate\Http\Response>): void) $callback
201+
* @return Batch
202+
*/
203+
public function finally(Closure $callback): self
204+
{
205+
$this->finallyCallback = $callback;
206+
207+
return $this;
208+
}
209+
210+
/**
211+
* Send all of the requests in the batch.
212+
*
213+
* @return array<int|string, \Illuminate\Http\Response|\Illuminate\Http\Client\RequestException>
214+
*/
215+
public function send(): array
216+
{
217+
$this->inProgress = true;
218+
219+
if ($this->beforeCallback !== null) {
220+
call_user_func($this->beforeCallback, $this);
221+
}
222+
223+
$results = [];
224+
$promises = [];
225+
226+
foreach ($this->requests as $key => $item) {
227+
$promise = match (true) {
228+
$item instanceof PendingRequest => $item->getPromise(),
229+
default => $item,
230+
};
231+
232+
$promises[$key] = $promise;
233+
}
234+
235+
if (! empty($promises)) {
236+
(new EachPromise($promises, [
237+
'fulfilled' => function ($result, $key) use (&$results) {
238+
$results[$key] = $result;
239+
240+
$this->decrementPendingRequests();
241+
242+
if ($result instanceof Response && $result->successful()) {
243+
if ($this->progressCallback !== null) {
244+
call_user_func($this->progressCallback, $this, $key, $result);
245+
}
246+
247+
return $result;
248+
}
249+
250+
if (($result instanceof Response && $result->failed()) ||
251+
$result instanceof RequestException) {
252+
$this->incrementFailedRequests();
253+
254+
if ($this->catchCallback !== null) {
255+
call_user_func($this->catchCallback, $this, $key, $result);
256+
}
257+
}
258+
259+
return $result;
260+
},
261+
'rejected' => function ($reason, $key) use ($catchCallback) {
262+
$this->decrementPendingRequests();
263+
264+
if ($reason instanceof RequestException) {
265+
$this->incrementFailedRequests();
266+
267+
if ($this->catchCallback !== null) {
268+
call_user_func($this->catchCallback, $this, $key, $reason);
269+
}
270+
}
271+
272+
return $reason;
273+
},
274+
]))->promise()->wait();
275+
}
276+
277+
if (! $this->hasFailures() && $this->thenCallback !== null) {
278+
call_user_func($this->thenCallback, $this, $results);
279+
}
280+
281+
if ($this->finallyCallback !== null) {
282+
call_user_func($this->finallyCallback, $this, $results);
283+
}
284+
285+
$this->finishedAt = new CarbonImmutable;
286+
$this->inProgress = false;
287+
288+
return $results;
289+
}
290+
291+
/**
292+
* Retrieve a new async pending request.
293+
*
294+
* @return \Illuminate\Http\Client\PendingRequest
295+
*/
296+
protected function asyncRequest()
297+
{
298+
return $this->factory->setHandler($this->handler)->async();
299+
}
300+
301+
/**
302+
* Get the total number of requests that have been processed by the batch thus far.
303+
*
304+
* @return non-negative-int
305+
*/
306+
public function processedRequests(): int
307+
{
308+
return $this->totalRequests - $this->pendingRequests;
309+
}
310+
311+
/**
312+
* Determine if the batch has finished executing.
313+
*
314+
* @return bool
315+
*/
316+
public function finished(): bool
317+
{
318+
return ! is_null($this->finishedAt);
319+
}
320+
321+
/**
322+
* Increment the count of total and pending requests in the batch.
323+
*
324+
* @return void
325+
*/
326+
protected function incrementPendingRequests(): void
327+
{
328+
$this->totalRequests++;
329+
$this->pendingRequests++;
330+
}
331+
332+
/**
333+
* Decrement the count of pending requests in the batch.
334+
*
335+
* @return void
336+
*/
337+
protected function decrementPendingRequests(): void
338+
{
339+
$this->pendingRequests--;
340+
}
341+
342+
/**
343+
* Determine if the batch has job failures.
344+
*
345+
* @return bool
346+
*/
347+
public function hasFailures(): bool
348+
{
349+
return $this->failedRequests > 0;
350+
}
351+
352+
/**
353+
* Increment the count of failed requests in the batch.
354+
*
355+
* @return void
356+
*/
357+
protected function incrementFailedRequests(): void
358+
{
359+
$this->failedRequests++;
360+
}
361+
362+
/**
363+
* Get the requests in the batch.
364+
*
365+
* @return array<array-key, \Illuminate\Http\Client\PendingRequest>
366+
*/
367+
public function getRequests(): array
368+
{
369+
return $this->requests;
370+
}
371+
372+
/**
373+
* Add a request to the batch with a numeric index.
374+
*
375+
* @param string $method
376+
* @param array $parameters
377+
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
378+
*/
379+
public function __call(string $method, array $parameters)
380+
{
381+
if ($this->inProgress) {
382+
throw new BatchInProgressException();
383+
}
384+
385+
$this->incrementPendingRequests();
386+
387+
return $this->requests[] = $this->asyncRequest()->$method(...$parameters);
388+
}
389+
}

0 commit comments

Comments
 (0)