From 933a3443a0905c8d167a94be84179f1641a932b4 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Sun, 30 Nov 2025 18:17:51 +0000 Subject: [PATCH 1/8] Fix issue with concurrency control on batch --- src/Illuminate/Http/Client/Batch.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Http/Client/Batch.php b/src/Illuminate/Http/Client/Batch.php index 5a6baf51aa31..60fd16a3910f 100644 --- a/src/Illuminate/Http/Client/Batch.php +++ b/src/Illuminate/Http/Client/Batch.php @@ -252,18 +252,19 @@ public function send(): array } $results = []; - $promises = []; - foreach ($this->requests as $key => $item) { - $promise = match (true) { - $item instanceof PendingRequest => $item->getPromise(), - default => $item, + if (! empty($this->requests)) { + // Create a generator that yields promises on-demand for proper concurrency control + $promiseGenerator = function () { + foreach ($this->requests as $key => $item) { + yield $key => match (true) { + $item instanceof Closure => $item(), + $item instanceof PendingRequest => $item->getPromise(), + default => $item, + }; + } }; - $promises[$key] = $promise; - } - - if (! empty($promises)) { $eachPromiseOptions = [ 'fulfilled' => function ($result, $key) use (&$results) { $results[$key] = $result; @@ -311,7 +312,7 @@ public function send(): array $eachPromiseOptions['concurrency'] = $this->concurrencyLimit; } - (new EachPromise($promises, $eachPromiseOptions))->promise()->wait(); + (new EachPromise($promiseGenerator(), $eachPromiseOptions))->promise()->wait(); } // Before returning the results, we must ensure that the results are sorted @@ -432,6 +433,7 @@ public function __call(string $method, array $parameters) $this->incrementPendingRequests(); - return $this->requests[] = $this->asyncRequest()->$method(...$parameters); + // Store a closure that creates the promise on-demand for proper concurrency control + return $this->requests[] = fn () => $this->asyncRequest()->$method(...$parameters); } } From ec9b7695337c397c7903790963734bee5847210d Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Sun, 30 Nov 2025 18:26:49 +0000 Subject: [PATCH 2/8] Fix issue with concurrency control on pool --- src/Illuminate/Http/Client/PendingRequest.php | 84 +++++++++---------- src/Illuminate/Http/Client/Pool.php | 3 +- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index dcab5cb387c1..4ac7316b8943 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -12,7 +12,6 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use GuzzleHttp\Promise\EachPromise; -use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\UriTemplate\UriTemplate; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Http\Client\Events\ConnectionFailed; @@ -791,7 +790,7 @@ public function dd() * * @param string $url * @param array|string|null $query - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -807,7 +806,7 @@ public function get(string $url, $query = null) * * @param string $url * @param array|string|null $query - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -823,7 +822,7 @@ public function head(string $url, $query = null) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -839,7 +838,7 @@ public function post(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -855,7 +854,7 @@ public function patch(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -871,7 +870,7 @@ public function put(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -903,13 +902,18 @@ public function pool(callable $callback, ?int $concurrency = null) return $results; } - $promises = []; - - foreach ($requests as $key => $item) { - $promises[$key] = $item instanceof static ? $item->getPromise() : $item; - } + // Use a generator to create promises on-demand for proper concurrency control + $promiseGenerator = function () use ($requests) { + foreach ($requests as $key => $item) { + yield $key => match (true) { + $item instanceof Closure => $item(), + $item instanceof static => $item->getPromise(), + default => $item, + }; + } + }; - (new EachPromise($promises, [ + (new EachPromise($promiseGenerator(), [ 'fulfilled' => function ($result, $key) use (&$results) { $results[$key] = $result; }, @@ -969,34 +973,32 @@ public function send(string $method, string $url, array $options = []) $this->dispatchResponseReceivedEvent($response); - if ($response->successful()) { - return; - } + if (! $response->successful()) { + try { + $shouldRetry = $this->retryWhenCallback ? call_user_func($this->retryWhenCallback, $response->toException(), $this, $this->request->toPsrRequest()->getMethod()) : true; + } catch (Exception $exception) { + $shouldRetry = false; - try { - $shouldRetry = $this->retryWhenCallback ? call_user_func($this->retryWhenCallback, $response->toException(), $this, $this->request->toPsrRequest()->getMethod()) : true; - } catch (Exception $exception) { - $shouldRetry = false; + throw $exception; + } - throw $exception; - } + if ($this->throwCallback && + ($this->throwIfCallback === null || + call_user_func($this->throwIfCallback, $response))) { + $response->throw($this->throwCallback); + } - if ($this->throwCallback && - ($this->throwIfCallback === null || - call_user_func($this->throwIfCallback, $response))) { - $response->throw($this->throwCallback); - } + $potentialTries = is_array($this->tries) + ? count($this->tries) + 1 + : $this->tries; - $potentialTries = is_array($this->tries) - ? count($this->tries) + 1 - : $this->tries; + if ($attempt < $potentialTries && $shouldRetry) { + $response->throw(); + } - if ($attempt < $potentialTries && $shouldRetry) { - $response->throw(); - } - - if ($potentialTries > 1 && $this->retryThrow) { - $response->throw(); + if ($potentialTries > 1 && $this->retryThrow) { + $response->throw(); + } } }); } catch (TransferException $e) { @@ -1198,7 +1200,7 @@ protected function handlePromiseResponse(Response|ConnectionException|TransferEx * @param string $method * @param string $url * @param array $options - * @return \Psr\Http\Message\MessageInterface|\Illuminate\Http\Client\FluentPromise + * @return \Psr\Http\Message\MessageInterface|\GuzzleHttp\Promise\PromiseInterface * * @throws \Exception */ @@ -1221,13 +1223,7 @@ protected function sendRequest(string $method, string $url, array $options = []) 'on_stats' => $onStats, ], $options)); - $result = $this->buildClient()->$clientMethod($method, $url, $mergedOptions); - - if ($result instanceof PromiseInterface && ! $result instanceof FluentPromise) { - $result = new FluentPromise($result); - } - - return $result; + return $this->buildClient()->$clientMethod($method, $url, $mergedOptions); } /** diff --git a/src/Illuminate/Http/Client/Pool.php b/src/Illuminate/Http/Client/Pool.php index e9716be08571..baf564f0e2d3 100644 --- a/src/Illuminate/Http/Client/Pool.php +++ b/src/Illuminate/Http/Client/Pool.php @@ -81,6 +81,7 @@ public function getRequests() */ public function __call($method, $parameters) { - return $this->pool[] = $this->asyncRequest()->$method(...$parameters); + // Store a closure that creates the promise on-demand for proper concurrency control + return $this->pool[] = fn () => $this->asyncRequest()->$method(...$parameters); } } From fc78b27943553387f7806e581e9497a174e3cf01 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Sun, 30 Nov 2025 18:49:04 +0000 Subject: [PATCH 3/8] Fix concurrency when using alias ->as on batch and pool --- src/Illuminate/Http/Client/Batch.php | 4 +- .../Http/Client/DeferredRequest.php | 74 +++++++++++++++++++ src/Illuminate/Http/Client/PendingRequest.php | 13 +++- src/Illuminate/Http/Client/Pool.php | 4 +- 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/Illuminate/Http/Client/DeferredRequest.php diff --git a/src/Illuminate/Http/Client/Batch.php b/src/Illuminate/Http/Client/Batch.php index 60fd16a3910f..f12f6fb700c0 100644 --- a/src/Illuminate/Http/Client/Batch.php +++ b/src/Illuminate/Http/Client/Batch.php @@ -135,7 +135,7 @@ public function __construct(?Factory $factory = null) * Add a request to the batch with a key. * * @param string $key - * @return \Illuminate\Http\Client\PendingRequest + * @return \Illuminate\Http\Client\DeferredRequest * * @throws \Illuminate\Http\Client\BatchInProgressException */ @@ -147,7 +147,7 @@ public function as(string $key) $this->incrementPendingRequests(); - return $this->requests[$key] = $this->asyncRequest(); + return new DeferredRequest($this->requests, $key, $this->factory, $this->handler); } /** diff --git a/src/Illuminate/Http/Client/DeferredRequest.php b/src/Illuminate/Http/Client/DeferredRequest.php new file mode 100644 index 000000000000..5901e8a9bfbf --- /dev/null +++ b/src/Illuminate/Http/Client/DeferredRequest.php @@ -0,0 +1,74 @@ +requests = &$requests; + $this->key = $key; + $this->factory = $factory; + $this->handler = $handler; + } + + /** + * Intercept method calls and store them as closures for deferred execution. + * + * @param string $method + * @param array $parameters + * @return $this + */ + public function __call($method, $parameters) + { + // Store a closure that will create and execute the request on-demand + $this->requests[$this->key] = fn () => $this->factory + ->setHandler($this->handler) + ->async() + ->$method(...$parameters); + + return $this; + } +} diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 4ac7316b8943..5c60f091aeae 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -895,8 +895,19 @@ public function pool(callable $callback, ?int $concurrency = null) $requests = tap(new Pool($this->factory), $callback)->getRequests(); if ($concurrency === null) { + // First, create all promises to start all requests in parallel + $promises = []; foreach ($requests as $key => $item) { - $results[$key] = $item instanceof static ? $item->getPromise()->wait() : $item->wait(); + $promises[$key] = match (true) { + $item instanceof Closure => $item(), + $item instanceof static => $item->getPromise(), + default => $item, + }; + } + + // Then wait for all promises to complete + foreach ($promises as $key => $promise) { + $results[$key] = $promise->wait(); } return $results; diff --git a/src/Illuminate/Http/Client/Pool.php b/src/Illuminate/Http/Client/Pool.php index baf564f0e2d3..4f312a57d7d6 100644 --- a/src/Illuminate/Http/Client/Pool.php +++ b/src/Illuminate/Http/Client/Pool.php @@ -45,11 +45,11 @@ public function __construct(?Factory $factory = null) * Add a request to the pool with a key. * * @param string $key - * @return \Illuminate\Http\Client\PendingRequest + * @return \Illuminate\Http\Client\DeferredRequest */ public function as(string $key) { - return $this->pool[$key] = $this->asyncRequest(); + return new DeferredRequest($this->pool, $key, $this->factory, $this->handler); } /** From a48a767f2da0254a0eddbe490989313eeaa8df03 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Sun, 30 Nov 2025 18:59:42 +0000 Subject: [PATCH 4/8] Rolling back some not needed changes after tests --- .../Http/Client/DeferredRequest.php | 8 ------ src/Illuminate/Http/Client/PendingRequest.php | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/Illuminate/Http/Client/DeferredRequest.php b/src/Illuminate/Http/Client/DeferredRequest.php index 5901e8a9bfbf..86a27bd6804b 100644 --- a/src/Illuminate/Http/Client/DeferredRequest.php +++ b/src/Illuminate/Http/Client/DeferredRequest.php @@ -2,12 +2,6 @@ namespace Illuminate\Http\Client; -/** - * Deferred request wrapper for proper concurrency control. - * - * This class wraps PendingRequest to defer execution until the request - * is actually needed by the batch/pool concurrency manager. - */ class DeferredRequest { /** @@ -39,8 +33,6 @@ class DeferredRequest protected $handler; /** - * Create a new deferred request instance. - * * @param array &$requests Reference to the pool/batch requests array * @param string|int $key The key for this request * @param \Illuminate\Http\Client\Factory $factory diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 5c60f091aeae..deb61a2f6be2 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -12,6 +12,7 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use GuzzleHttp\Promise\EachPromise; +use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\UriTemplate\UriTemplate; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Http\Client\Events\ConnectionFailed; @@ -790,7 +791,7 @@ public function dd() * * @param string $url * @param array|string|null $query - * @return \Illuminate\Http\Client\Response + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -806,7 +807,7 @@ public function get(string $url, $query = null) * * @param string $url * @param array|string|null $query - * @return \Illuminate\Http\Client\Response + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -822,7 +823,7 @@ public function head(string $url, $query = null) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -838,7 +839,7 @@ public function post(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -854,7 +855,7 @@ public function patch(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -870,7 +871,7 @@ public function put(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface * * @throws \Illuminate\Http\Client\ConnectionException */ @@ -895,7 +896,6 @@ public function pool(callable $callback, ?int $concurrency = null) $requests = tap(new Pool($this->factory), $callback)->getRequests(); if ($concurrency === null) { - // First, create all promises to start all requests in parallel $promises = []; foreach ($requests as $key => $item) { $promises[$key] = match (true) { @@ -905,7 +905,6 @@ public function pool(callable $callback, ?int $concurrency = null) }; } - // Then wait for all promises to complete foreach ($promises as $key => $promise) { $results[$key] = $promise->wait(); } @@ -1211,7 +1210,7 @@ protected function handlePromiseResponse(Response|ConnectionException|TransferEx * @param string $method * @param string $url * @param array $options - * @return \Psr\Http\Message\MessageInterface|\GuzzleHttp\Promise\PromiseInterface + * @return \Psr\Http\Message\MessageInterface|\Illuminate\Http\Client\FluentPromise * * @throws \Exception */ @@ -1234,7 +1233,13 @@ protected function sendRequest(string $method, string $url, array $options = []) 'on_stats' => $onStats, ], $options)); - return $this->buildClient()->$clientMethod($method, $url, $mergedOptions); + $result = $this->buildClient()->$clientMethod($method, $url, $mergedOptions); + + if ($result instanceof PromiseInterface && ! $result instanceof FluentPromise) { + $result = new FluentPromise($result); + } + + return $result; } /** From 085bad7eca6029741c7af74f06457dc6a79df141 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Sun, 30 Nov 2025 19:31:36 +0000 Subject: [PATCH 5/8] Fix issue with method chaining --- .../Http/Client/DeferredRequest.php | 31 +++++++++++++++---- src/Illuminate/Http/Client/Pool.php | 11 +++++-- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Http/Client/DeferredRequest.php b/src/Illuminate/Http/Client/DeferredRequest.php index 86a27bd6804b..b5120295cdfa 100644 --- a/src/Illuminate/Http/Client/DeferredRequest.php +++ b/src/Illuminate/Http/Client/DeferredRequest.php @@ -32,6 +32,13 @@ class DeferredRequest */ protected $handler; + /** + * The accumulated method calls to apply. + * + * @var array + */ + protected $methodCalls = []; + /** * @param array &$requests Reference to the pool/batch requests array * @param string|int $key The key for this request @@ -47,7 +54,7 @@ public function __construct(array &$requests, $key, Factory $factory, callable $ } /** - * Intercept method calls and store them as closures for deferred execution. + * Intercept method calls and store them for deferred execution. * * @param string $method * @param array $parameters @@ -55,11 +62,23 @@ public function __construct(array &$requests, $key, Factory $factory, callable $ */ public function __call($method, $parameters) { - // Store a closure that will create and execute the request on-demand - $this->requests[$this->key] = fn () => $this->factory - ->setHandler($this->handler) - ->async() - ->$method(...$parameters); + // Accumulate method calls to apply in order + $this->methodCalls[] = ['method' => $method, 'parameters' => $parameters]; + + // Store a closure that will create and execute the request on-demand with all accumulated calls + $methodCalls = $this->methodCalls; + $factory = $this->factory; + $handler = $this->handler; + + $this->requests[$this->key] = function () use ($factory, $handler, $methodCalls) { + $request = $factory->setHandler($handler)->async(); + + foreach ($methodCalls as $call) { + $request = $request->{$call['method']}(...$call['parameters']); + } + + return $request; + }; return $this; } diff --git a/src/Illuminate/Http/Client/Pool.php b/src/Illuminate/Http/Client/Pool.php index 4f312a57d7d6..1c2eaaceef8d 100644 --- a/src/Illuminate/Http/Client/Pool.php +++ b/src/Illuminate/Http/Client/Pool.php @@ -77,11 +77,16 @@ public function getRequests() * * @param string $method * @param array $parameters - * @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise + * @return \Illuminate\Http\Client\DeferredRequest */ public function __call($method, $parameters) { - // Store a closure that creates the promise on-demand for proper concurrency control - return $this->pool[] = fn () => $this->asyncRequest()->$method(...$parameters); + // Get the next numeric index and create a DeferredRequest for method chaining + $key = count($this->pool); + + $deferred = new DeferredRequest($this->pool, $key, $this->factory, $this->handler); + + // Call the method on the DeferredRequest to start accumulating calls + return $deferred->$method(...$parameters); } } From d004f4bcef30f03ac782369337b78784e16760ee Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Sun, 30 Nov 2025 19:44:35 +0000 Subject: [PATCH 6/8] Fixed method chaining on Http::batch and added test case for it --- src/Illuminate/Http/Client/Batch.php | 11 ++++++++--- tests/Http/HttpClientTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Client/Batch.php b/src/Illuminate/Http/Client/Batch.php index f12f6fb700c0..5af555cab6cb 100644 --- a/src/Illuminate/Http/Client/Batch.php +++ b/src/Illuminate/Http/Client/Batch.php @@ -423,7 +423,7 @@ public function getRequests(): array * * @param string $method * @param array $parameters - * @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise + * @return \Illuminate\Http\Client\DeferredRequest */ public function __call(string $method, array $parameters) { @@ -433,7 +433,12 @@ public function __call(string $method, array $parameters) $this->incrementPendingRequests(); - // Store a closure that creates the promise on-demand for proper concurrency control - return $this->requests[] = fn () => $this->asyncRequest()->$method(...$parameters); + // Get the next numeric index and create a DeferredRequest for method chaining + $key = count($this->requests); + + $deferred = new DeferredRequest($this->requests, $key, $this->factory, $this->handler); + + // Call the method on the DeferredRequest to start accumulating calls + return $deferred->$method(...$parameters); } } diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index febc488c3a91..8b2601a9e4e2 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -1939,6 +1939,33 @@ public function testMiddlewareRunsInPool() $this->assertSame(['hyped-for' => 'laravel-movie'], json_decode(tap($history[0]['request']->getBody())->rewind()->getContents(), true)); } + public function testMiddlewareRunsInBatch() + { + $this->factory->fake(function (Request $request) { + return $this->factory->response('Fake'); + }); + + $history = []; + + $middleware = Middleware::history($history); + + $batch = $this->factory->batch(fn (Batch $batch) => [ + $batch->withMiddleware($middleware)->post('https://example.com', ['hyped-for' => 'laravel-movie']), + ]); + + $responses = $batch->send(); + + $response = $responses[0]; + + $this->assertSame('Fake', $response->body()); + + $this->assertCount(1, $history); + + $this->assertSame('Fake', tap($history[0]['response']->getBody())->rewind()->getContents()); + + $this->assertSame(['hyped-for' => 'laravel-movie'], json_decode(tap($history[0]['request']->getBody())->rewind()->getContents(), true)); + } + public function testPoolConcurrency() { $this->factory->fake([ From c1f4f8c76b014b13d82a2cc60296a7d348f6c1b7 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Wed, 3 Dec 2025 11:46:31 +0000 Subject: [PATCH 7/8] Alternative approach for not adding breaking changes --- src/Illuminate/Http/Client/Batch.php | 14 ++- .../Http/Client/DeferredPromise.php | 101 ++++++++++++++++++ .../Http/Client/DeferredRequest.php | 85 --------------- src/Illuminate/Http/Client/PendingRequest.php | 91 ++++++++++++++-- src/Illuminate/Http/Client/Pool.php | 14 ++- 5 files changed, 198 insertions(+), 107 deletions(-) create mode 100644 src/Illuminate/Http/Client/DeferredPromise.php delete mode 100644 src/Illuminate/Http/Client/DeferredRequest.php diff --git a/src/Illuminate/Http/Client/Batch.php b/src/Illuminate/Http/Client/Batch.php index 5af555cab6cb..6640e9c0b945 100644 --- a/src/Illuminate/Http/Client/Batch.php +++ b/src/Illuminate/Http/Client/Batch.php @@ -135,7 +135,7 @@ public function __construct(?Factory $factory = null) * Add a request to the batch with a key. * * @param string $key - * @return \Illuminate\Http\Client\DeferredRequest + * @return \Illuminate\Http\Client\PendingRequest * * @throws \Illuminate\Http\Client\BatchInProgressException */ @@ -147,7 +147,7 @@ public function as(string $key) $this->incrementPendingRequests(); - return new DeferredRequest($this->requests, $key, $this->factory, $this->handler); + return $this->factory->setHandler($this->handler)->async()->setDeferred($this->requests, $key); } /** @@ -423,7 +423,7 @@ public function getRequests(): array * * @param string $method * @param array $parameters - * @return \Illuminate\Http\Client\DeferredRequest + * @return \Illuminate\Http\Client\PendingRequest */ public function __call(string $method, array $parameters) { @@ -433,12 +433,10 @@ public function __call(string $method, array $parameters) $this->incrementPendingRequests(); - // Get the next numeric index and create a DeferredRequest for method chaining + // Get the next numeric index $key = count($this->requests); - $deferred = new DeferredRequest($this->requests, $key, $this->factory, $this->handler); - - // Call the method on the DeferredRequest to start accumulating calls - return $deferred->$method(...$parameters); + // Create a deferred PendingRequest and call the method to start chaining + return $this->factory->setHandler($this->handler)->async()->setDeferred($this->requests, $key)->$method(...$parameters); } } diff --git a/src/Illuminate/Http/Client/DeferredPromise.php b/src/Illuminate/Http/Client/DeferredPromise.php new file mode 100644 index 000000000000..d774a744046d --- /dev/null +++ b/src/Illuminate/Http/Client/DeferredPromise.php @@ -0,0 +1,101 @@ + + */ + protected $transformations = []; + + /** + * @param array &$requests Reference to the pool/batch requests array + * @param string|int $key The key for this request + * @param callable $promiseFactory Closure that returns a promise + */ + public function __construct(array &$requests, $key, callable $promiseFactory) + { + $this->requests = &$requests; + $this->key = $key; + $this->promiseFactory = $promiseFactory; + + // Store the factory in the requests array + $this->updateRequestsClosure(); + } + + /** + * Add a promise transformation (like ->then()). + * + * @param callable|null $onFulfilled + * @param callable|null $onRejected + * @return $this + */ + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + { + $this->transformations[] = ['method' => 'then', 'args' => [$onFulfilled, $onRejected]]; + $this->updateRequestsClosure(); + + return $this; + } + + /** + * Add an otherwise transformation. + * + * @param callable $onRejected + * @return $this + */ + public function otherwise(callable $onRejected) + { + $this->transformations[] = ['method' => 'otherwise', 'args' => [$onRejected]]; + $this->updateRequestsClosure(); + + return $this; + } + + /** + * Update the closure stored in the requests array to include all transformations. + * + * @return void + */ + protected function updateRequestsClosure() + { + $promiseFactory = $this->promiseFactory; + $transformations = $this->transformations; + + $this->requests[$this->key] = function () use ($promiseFactory, $transformations) { + $promise = $promiseFactory(); + + foreach ($transformations as $transformation) { + $promise = $promise->{$transformation['method']}(...$transformation['args']); + } + + return $promise; + }; + } +} diff --git a/src/Illuminate/Http/Client/DeferredRequest.php b/src/Illuminate/Http/Client/DeferredRequest.php deleted file mode 100644 index b5120295cdfa..000000000000 --- a/src/Illuminate/Http/Client/DeferredRequest.php +++ /dev/null @@ -1,85 +0,0 @@ - - */ - protected $methodCalls = []; - - /** - * @param array &$requests Reference to the pool/batch requests array - * @param string|int $key The key for this request - * @param \Illuminate\Http\Client\Factory $factory - * @param callable $handler - */ - public function __construct(array &$requests, $key, Factory $factory, callable $handler) - { - $this->requests = &$requests; - $this->key = $key; - $this->factory = $factory; - $this->handler = $handler; - } - - /** - * Intercept method calls and store them for deferred execution. - * - * @param string $method - * @param array $parameters - * @return $this - */ - public function __call($method, $parameters) - { - // Accumulate method calls to apply in order - $this->methodCalls[] = ['method' => $method, 'parameters' => $parameters]; - - // Store a closure that will create and execute the request on-demand with all accumulated calls - $methodCalls = $this->methodCalls; - $factory = $this->factory; - $handler = $this->handler; - - $this->requests[$this->key] = function () use ($factory, $handler, $methodCalls) { - $request = $factory->setHandler($handler)->async(); - - foreach ($methodCalls as $call) { - $request = $request->{$call['method']}(...$call['parameters']); - } - - return $request; - }; - - return $this; - } -} diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index deb61a2f6be2..4f385f2bd4ae 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -230,6 +230,20 @@ class PendingRequest */ protected $truncateExceptionsAt = null; + /** + * Reference to the pool/batch requests array for deferred execution. + * + * @var array|null + */ + protected $deferredRequests = null; + + /** + * The key for this request in the pool/batch for deferred execution. + * + * @var string|int|null + */ + protected $deferredKey = null; + /** * Create a new HTTP Client instance. * @@ -786,17 +800,36 @@ public function dd() }); } + /** + * Enable deferred execution mode for pool/batch requests. + * + * @param array &$requests Reference to the pool/batch requests array + * @param string|int $key The key for this request + * @return $this + */ + public function setDeferred(array &$requests, $key) + { + $this->deferredRequests = &$requests; + $this->deferredKey = $key; + + return $this; + } + /** * Issue a GET request to the given URL. * * @param string $url * @param array|string|null $query - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|$this * * @throws \Illuminate\Http\Client\ConnectionException */ public function get(string $url, $query = null) { + if ($this->deferredRequests !== null) { + return $this->deferRequest('get', func_get_args()); + } + return $this->send('GET', $url, func_num_args() === 1 ? [] : [ 'query' => $query, ]); @@ -807,12 +840,16 @@ public function get(string $url, $query = null) * * @param string $url * @param array|string|null $query - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|$this * * @throws \Illuminate\Http\Client\ConnectionException */ public function head(string $url, $query = null) { + if ($this->deferredRequests !== null) { + return $this->deferRequest('head', func_get_args()); + } + return $this->send('HEAD', $url, func_num_args() === 1 ? [] : [ 'query' => $query, ]); @@ -823,12 +860,16 @@ public function head(string $url, $query = null) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|$this * * @throws \Illuminate\Http\Client\ConnectionException */ public function post(string $url, $data = []) { + if ($this->deferredRequests !== null) { + return $this->deferRequest('post', func_get_args()); + } + return $this->send('POST', $url, [ $this->bodyFormat => $data, ]); @@ -839,12 +880,16 @@ public function post(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|$this * * @throws \Illuminate\Http\Client\ConnectionException */ public function patch(string $url, $data = []) { + if ($this->deferredRequests !== null) { + return $this->deferRequest('patch', func_get_args()); + } + return $this->send('PATCH', $url, [ $this->bodyFormat => $data, ]); @@ -855,12 +900,16 @@ public function patch(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|$this * * @throws \Illuminate\Http\Client\ConnectionException */ public function put(string $url, $data = []) { + if ($this->deferredRequests !== null) { + return $this->deferRequest('put', func_get_args()); + } + return $this->send('PUT', $url, [ $this->bodyFormat => $data, ]); @@ -871,12 +920,16 @@ public function put(string $url, $data = []) * * @param string $url * @param array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data - * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface + * @return \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|$this * * @throws \Illuminate\Http\Client\ConnectionException */ public function delete(string $url, $data = []) { + if ($this->deferredRequests !== null) { + return $this->deferRequest('delete', func_get_args()); + } + return $this->send('DELETE', $url, empty($data) ? [] : [ $this->bodyFormat => $data, ]); @@ -1771,6 +1824,32 @@ public function setHandler($handler) return $this; } + /** + * Defer a request execution for pool/batch processing. + * + * @param string $method + * @param array $parameters + * @return \Illuminate\Http\Client\DeferredPromise + */ + protected function deferRequest(string $method, array $parameters) + { + // Clone the current request state to capture all configuration + $clonedRequest = clone $this; + + // Clear deferred mode on the clone to prevent infinite recursion + // We need to break the reference completely + unset($clonedRequest->deferredRequests, $clonedRequest->deferredKey); + $clonedRequest->deferredRequests = null; + $clonedRequest->deferredKey = null; + + // Create a DeferredPromise that wraps the closure + return new DeferredPromise( + $this->deferredRequests, + $this->deferredKey, + fn () => $clonedRequest->$method(...$parameters) + ); + } + /** * Get the pending request options. * diff --git a/src/Illuminate/Http/Client/Pool.php b/src/Illuminate/Http/Client/Pool.php index 1c2eaaceef8d..5cdcc353070c 100644 --- a/src/Illuminate/Http/Client/Pool.php +++ b/src/Illuminate/Http/Client/Pool.php @@ -45,11 +45,11 @@ public function __construct(?Factory $factory = null) * Add a request to the pool with a key. * * @param string $key - * @return \Illuminate\Http\Client\DeferredRequest + * @return \Illuminate\Http\Client\PendingRequest */ public function as(string $key) { - return new DeferredRequest($this->pool, $key, $this->factory, $this->handler); + return $this->factory->setHandler($this->handler)->async()->setDeferred($this->pool, $key); } /** @@ -77,16 +77,14 @@ public function getRequests() * * @param string $method * @param array $parameters - * @return \Illuminate\Http\Client\DeferredRequest + * @return \Illuminate\Http\Client\PendingRequest */ public function __call($method, $parameters) { - // Get the next numeric index and create a DeferredRequest for method chaining + // Get the next numeric index $key = count($this->pool); - $deferred = new DeferredRequest($this->pool, $key, $this->factory, $this->handler); - - // Call the method on the DeferredRequest to start accumulating calls - return $deferred->$method(...$parameters); + // Create a deferred PendingRequest and call the method to start chaining + return $this->factory->setHandler($this->handler)->async()->setDeferred($this->pool, $key)->$method(...$parameters); } } From 9ff962655eb3adc59111c5a508dc8e5c39fa2702 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Wed, 3 Dec 2025 11:48:10 +0000 Subject: [PATCH 8/8] Remove not used import --- src/Illuminate/Http/Client/DeferredPromise.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Illuminate/Http/Client/DeferredPromise.php b/src/Illuminate/Http/Client/DeferredPromise.php index d774a744046d..baeadbff7b62 100644 --- a/src/Illuminate/Http/Client/DeferredPromise.php +++ b/src/Illuminate/Http/Client/DeferredPromise.php @@ -2,8 +2,6 @@ namespace Illuminate\Http\Client; -use GuzzleHttp\Promise\PromiseInterface; - class DeferredPromise { /**