From 0b2d6b235804b7ed078c802fe269accc5b9450f8 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Fri, 28 Nov 2025 11:39:59 -0500 Subject: [PATCH 1/5] laravel fluent promise --- src/Illuminate/Http/Client/FluentPromise.php | 72 +++++++++++++++++++ src/Illuminate/Http/Client/PendingRequest.php | 8 ++- tests/Integration/Http/HttpClientTest.php | 51 +++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Http/Client/FluentPromise.php diff --git a/src/Illuminate/Http/Client/FluentPromise.php b/src/Illuminate/Http/Client/FluentPromise.php new file mode 100644 index 000000000000..c856494635d1 --- /dev/null +++ b/src/Illuminate/Http/Client/FluentPromise.php @@ -0,0 +1,72 @@ +forwardCallTo($this->guzzlePromise, $method, $parameters); + + if (! $result instanceof PromiseInterface) { + return $result; + } + + $this->guzzlePromise = $result; + + return $this; + } + + #[\Override] + public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface + { + return $this->__call('then', [$onFulfilled, $onRejected]); + } + + #[\Override] + public function otherwise(callable $onRejected): PromiseInterface + { + return $this->__call('otherwise', [$onRejected]); + } + + #[\Override] + public function getState(): string + { + return $this->guzzlePromise->getState(); + } + + #[\Override] + public function resolve($value): void + { + $this->guzzlePromise->resolve($value); + } + + #[\Override] + public function reject($reason): void + { + $this->guzzlePromise->reject($reason); + } + + #[\Override] + public function cancel(): void + { + $this->guzzlePromise->cancel(); + } + + #[\Override] + public function wait(bool $unwrap = true) + { + return $this->__call('wait', [$unwrap]); + } +} diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index c3c7e2c39624..79d7d5a2ddb3 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; @@ -1220,7 +1221,12 @@ 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 = new FluentPromise($result); + } + + return $result; } /** diff --git a/tests/Integration/Http/HttpClientTest.php b/tests/Integration/Http/HttpClientTest.php index 8e2b75dfaaf6..d4f832ce3052 100644 --- a/tests/Integration/Http/HttpClientTest.php +++ b/tests/Integration/Http/HttpClientTest.php @@ -3,6 +3,9 @@ namespace Illuminate\Tests\Integration\Http; use Illuminate\Http\Client\Events\RequestSending; +use Illuminate\Http\Client\PendingRequest; +use Illuminate\Http\Client\Pool; +use Illuminate\Http\Client\Response; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Facade; @@ -37,4 +40,52 @@ public function testGlobalMiddlewarePersistsAfterFacadeFlush(): void $this->assertCount(2, Http::getGlobalMiddleware()); } + + public function testPoolCanForwardToUnderlyingPromise() + { + Http::fake([ + 'https://laravel.com*' => Http::response('Laravel'), + 'https://forge.laravel.com*' => Http::response('Forge'), + 'https://nightwatch.laravel.com*' => Http::response('Tim n Jess'), + ]); + + $responses = Http::pool(function (Pool $pool) { + $pool->as('laravel')->get('https://laravel.com'); + + $pool->as('forge') + ->get('https://forge.laravel.com') + ->then(function (Response $response): int { + return strlen($response->getBody()); + }); + + $pool->as('nightwatch') + ->get('https://nightwatch.laravel.com') + ->then(fn (): int => 1) + ->then(fn ($i): int => $i + 199); + }, 3); + + $this->assertInstanceOf(Response::class, $responses['laravel']); + $this->assertEquals(5, $responses['forge']); + $this->assertEquals(200, $responses['nightwatch']); + + $this->assertCount(3, Http::recorded()); + } + + public function testForwardsCallsToPromise() + { + Http::fake(['*' => Http::response('faked response')]); + + $myFakedResponse = null; + $r = Http::async() + ->get('https://laravel.com') + ->then(function (Response $response) use (&$myFakedResponse): string { + $myFakedResponse = $response->getBody(); + + return 'stub'; + }) + ->wait(); + + $this->assertEquals('faked response', $myFakedResponse); + $this->assertEquals('stub', $r); + } } From e30c27841080e2d2de4f32aad313f70b8f3394fc Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Fri, 28 Nov 2025 11:57:26 -0500 Subject: [PATCH 2/5] docblocks --- src/Illuminate/Http/Client/FluentPromise.php | 16 +++++++++++++++- src/Illuminate/Http/Client/PendingRequest.php | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Client/FluentPromise.php b/src/Illuminate/Http/Client/FluentPromise.php index c856494635d1..279522c7caa4 100644 --- a/src/Illuminate/Http/Client/FluentPromise.php +++ b/src/Illuminate/Http/Client/FluentPromise.php @@ -6,15 +6,29 @@ use GuzzleHttp\Promise\PromiseInterface; use Illuminate\Support\Traits\ForwardsCalls; +/** + * A decorated Promise which allows for chaining callbacks. + */ class FluentPromise implements PromiseInterface { use ForwardsCalls; + /** + * Create a new fluent promise instance. + * + * @param PromiseInterface $guzzlePromise + */ public function __construct(public PromiseInterface $guzzlePromise) { - } + /** + * Proxy requests to the underlying promise interface and update the local promise. + * + * @param string $method + * @param array $parameters + * @return mixed + */ public function __call($method, $parameters) { $result = $this->forwardCallTo($this->guzzlePromise, $method, $parameters); diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 79d7d5a2ddb3..7ef1097391f3 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -1198,7 +1198,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 */ @@ -1222,7 +1222,7 @@ protected function sendRequest(string $method, string $url, array $options = []) ], $options)); $result = $this->buildClient()->$clientMethod($method, $url, $mergedOptions); - if ($result instanceof PromiseInterface) { + if ($result instanceof PromiseInterface && ! $result instanceof FluentPromise) { $result = new FluentPromise($result); } From 05f4254516dc9722df0ba90713ef425317b432ab Mon Sep 17 00:00:00 2001 From: Luke Kuzmish Date: Fri, 28 Nov 2025 11:59:23 -0500 Subject: [PATCH 3/5] protected --- src/Illuminate/Http/Client/FluentPromise.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Client/FluentPromise.php b/src/Illuminate/Http/Client/FluentPromise.php index 279522c7caa4..6ffed94bc31c 100644 --- a/src/Illuminate/Http/Client/FluentPromise.php +++ b/src/Illuminate/Http/Client/FluentPromise.php @@ -2,7 +2,6 @@ namespace Illuminate\Http\Client; -use GuzzleHttp\Promise\Promise; use GuzzleHttp\Promise\PromiseInterface; use Illuminate\Support\Traits\ForwardsCalls; @@ -16,12 +15,22 @@ class FluentPromise implements PromiseInterface /** * Create a new fluent promise instance. * - * @param PromiseInterface $guzzlePromise + * @param \GuzzleHttp\Promise\PromiseInterface $guzzlePromise */ - public function __construct(public PromiseInterface $guzzlePromise) + public function __construct(protected PromiseInterface $guzzlePromise) { } + /** + * Get the underlying Guzzle promise. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function getGuzzlePromise(): PromiseInterface + { + return $this->guzzlePromise; + } + /** * Proxy requests to the underlying promise interface and update the local promise. * From b17015ab0d8e19deccaa227f5b242e2d099748cd Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 28 Nov 2025 11:55:28 -0600 Subject: [PATCH 4/5] Update PendingRequest.php --- src/Illuminate/Http/Client/PendingRequest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 7ef1097391f3..dcab5cb387c1 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -1222,6 +1222,7 @@ protected function sendRequest(string $method, string $url, array $options = []) ], $options)); $result = $this->buildClient()->$clientMethod($method, $url, $mergedOptions); + if ($result instanceof PromiseInterface && ! $result instanceof FluentPromise) { $result = new FluentPromise($result); } From c801ab25ac0a06529d657618dce8de24ee4a474a Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 28 Nov 2025 11:57:31 -0600 Subject: [PATCH 5/5] formatting --- src/Illuminate/Http/Client/FluentPromise.php | 72 ++++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/Illuminate/Http/Client/FluentPromise.php b/src/Illuminate/Http/Client/FluentPromise.php index 6ffed94bc31c..5ee296273936 100644 --- a/src/Illuminate/Http/Client/FluentPromise.php +++ b/src/Illuminate/Http/Client/FluentPromise.php @@ -21,36 +21,6 @@ public function __construct(protected PromiseInterface $guzzlePromise) { } - /** - * Get the underlying Guzzle promise. - * - * @return \GuzzleHttp\Promise\PromiseInterface - */ - public function getGuzzlePromise(): PromiseInterface - { - return $this->guzzlePromise; - } - - /** - * Proxy requests to the underlying promise interface and update the local promise. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function __call($method, $parameters) - { - $result = $this->forwardCallTo($this->guzzlePromise, $method, $parameters); - - if (! $result instanceof PromiseInterface) { - return $result; - } - - $this->guzzlePromise = $result; - - return $this; - } - #[\Override] public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface { @@ -63,12 +33,6 @@ public function otherwise(callable $onRejected): PromiseInterface return $this->__call('otherwise', [$onRejected]); } - #[\Override] - public function getState(): string - { - return $this->guzzlePromise->getState(); - } - #[\Override] public function resolve($value): void { @@ -92,4 +56,40 @@ public function wait(bool $unwrap = true) { return $this->__call('wait', [$unwrap]); } + + #[\Override] + public function getState(): string + { + return $this->guzzlePromise->getState(); + } + + /** + * Get the underlying Guzzle promise. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function getGuzzlePromise(): PromiseInterface + { + return $this->guzzlePromise; + } + + /** + * Proxy requests to the underlying promise interface and update the local promise. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + $result = $this->forwardCallTo($this->guzzlePromise, $method, $parameters); + + if (! $result instanceof PromiseInterface) { + return $result; + } + + $this->guzzlePromise = $result; + + return $this; + } }