diff --git a/src/Illuminate/Http/Client/Batch.php b/src/Illuminate/Http/Client/Batch.php index 5a6baf51aa31..5f5ceb327873 100644 --- a/src/Illuminate/Http/Client/Batch.php +++ b/src/Illuminate/Http/Client/Batch.php @@ -7,6 +7,8 @@ use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Promise\EachPromise; use GuzzleHttp\Utils; +use Illuminate\Http\Client\Promises\LazyPromise; +use Illuminate\Support\Collection; use Illuminate\Support\Defer\DeferredCallback; use function Illuminate\Support\defer; @@ -252,18 +254,8 @@ public function send(): array } $results = []; - $promises = []; - foreach ($this->requests as $key => $item) { - $promise = match (true) { - $item instanceof PendingRequest => $item->getPromise(), - default => $item, - }; - - $promises[$key] = $promise; - } - - if (! empty($promises)) { + if (! empty($this->requests)) { $eachPromiseOptions = [ 'fulfilled' => function ($result, $key) use (&$results) { $results[$key] = $result; @@ -311,7 +303,16 @@ public function send(): array $eachPromiseOptions['concurrency'] = $this->concurrencyLimit; } - (new EachPromise($promises, $eachPromiseOptions))->promise()->wait(); + $promiseGenerator = function () { + foreach ($this->requests as $key => $item) { + $promise = $item instanceof PendingRequest ? $item->getPromise() : $item; + yield $key => $promise instanceof LazyPromise ? $promise->buildPromise() : $promise; + } + }; + + (new EachPromise($promiseGenerator(), $eachPromiseOptions)) + ->promise() + ->wait(); } // Before returning the results, we must ensure that the results are sorted diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index dcab5cb387c1..a7531a790590 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -18,6 +18,8 @@ use Illuminate\Http\Client\Events\ConnectionFailed; use Illuminate\Http\Client\Events\RequestSending; use Illuminate\Http\Client\Events\ResponseReceived; +use Illuminate\Http\Client\Promises\FluentPromise; +use Illuminate\Http\Client\Promises\LazyPromise; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -886,7 +888,7 @@ public function delete(string $url, $data = []) * Send a pool of asynchronous requests concurrently. * * @param (callable(\Illuminate\Http\Client\Pool): mixed) $callback - * @param int|null $concurrency + * @param non-negative-int|null $concurrency * @return array */ public function pool(callable $callback, ?int $concurrency = null) @@ -896,6 +898,16 @@ public function pool(callable $callback, ?int $concurrency = null) $requests = tap(new Pool($this->factory), $callback)->getRequests(); if ($concurrency === null) { + (new Collection($requests))->each(static function ($item) { + if ($item instanceof static) { + $item = $item->getPromise(); + } + + if ($item instanceof LazyPromise) { + $item->buildPromise(); + } + }); + foreach ($requests as $key => $item) { $results[$key] = $item instanceof static ? $item->getPromise()->wait() : $item->wait(); } @@ -903,13 +915,16 @@ public function pool(callable $callback, ?int $concurrency = null) return $results; } - $promises = []; + $concurrency = $concurrency === 0 ? count($requests) : $concurrency; - foreach ($requests as $key => $item) { - $promises[$key] = $item instanceof static ? $item->getPromise() : $item; - } + $promiseGenerator = static function () use ($requests) { + foreach ($requests as $key => $item) { + $promise = $item instanceof static ? $item->getPromise() : $item; + yield $key => $promise instanceof LazyPromise ? $promise->buildPromise() : $promise; + } + }; - (new EachPromise($promises, [ + (new EachPromise($promiseGenerator(), [ 'fulfilled' => function ($result, $key) use (&$results) { $results[$key] = $result; }, @@ -939,7 +954,7 @@ public function batch(callable $callback): Batch * @param string $method * @param string $url * @param array $options - * @return \Illuminate\Http\Client\Response + * @return \Illuminate\Http\Client\Response|\Illuminate\Http\Client\Promises\LazyPromise * * @throws \Exception * @throws \Illuminate\Http\Client\ConnectionException @@ -957,7 +972,9 @@ public function send(string $method, string $url, array $options = []) [$this->pendingBody, $this->pendingFiles] = [null, []]; if ($this->async) { - return $this->makePromise($method, $url, $options); + return $this->promise = new LazyPromise( + fn () => $this->makePromise($method, $url, $options) + ); } $shouldRetry = null; @@ -1198,7 +1215,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 */ diff --git a/src/Illuminate/Http/Client/FluentPromise.php b/src/Illuminate/Http/Client/Promises/FluentPromise.php similarity index 98% rename from src/Illuminate/Http/Client/FluentPromise.php rename to src/Illuminate/Http/Client/Promises/FluentPromise.php index 5ee296273936..1db30e408f27 100644 --- a/src/Illuminate/Http/Client/FluentPromise.php +++ b/src/Illuminate/Http/Client/Promises/FluentPromise.php @@ -1,6 +1,6 @@ + */ + protected array $pending = []; + + /** + * The promise built by the creator. + * + * @var \GuzzleHttp\Promise\PromiseInterface + */ + protected PromiseInterface $guzzlePromise; + + /** + * Create a new lazy promise instance. + * + * @param (\Closure(): \GuzzleHttp\Promise\PromiseInterface) $promiseBuilder The callback to build a new PromiseInterface. + */ + public function __construct(protected Closure $promiseBuilder) + { + } + + /** + * Build the promise from the promise builder. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @throws \RuntimeException If the promise has already been built + */ + public function buildPromise(): PromiseInterface + { + if (! $this->promiseNeedsBuilt()) { + throw new RuntimeException('Promise already built'); + } + + $this->guzzlePromise = call_user_func($this->promiseBuilder); + + foreach ($this->pending as $pendingCallback) { + $pendingCallback($this->guzzlePromise); + } + + $this->pending = []; + + return $this->guzzlePromise; + } + + #[\Override] + public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface + { + if ($this->promiseNeedsBuilt()) { + $this->pending[] = static fn (PromiseInterface $promise) => $promise->then($onFulfilled, $onRejected); + + return $this; + } + + return $this->guzzlePromise->then($onFulfilled, $onRejected); + } + + #[\Override] + public function otherwise(callable $onRejected): PromiseInterface + { + if ($this->promiseNeedsBuilt()) { + $this->pending[] = static fn (PromiseInterface $promise) => $promise->otherwise($onRejected); + + return $this; + } + + return $this->guzzlePromise->otherwise($onRejected); + } + + #[\Override] + public function getState(): string + { + if ($this->promiseNeedsBuilt()) { + return PromiseInterface::PENDING; + } + + return $this->guzzlePromise->getState(); + } + + #[\Override] + public function resolve($value): void + { + throw new \LogicException('Cannot resolve a lazy promise.'); + } + + #[\Override] + public function reject($reason): void + { + throw new \LogicException('Cannot reject a lazy promise.'); + } + + #[\Override] + public function cancel(): void + { + throw new \LogicException('Cannot cancel a lazy promise.'); + } + + #[\Override] + public function wait(bool $unwrap = true) + { + if ($this->promiseNeedsBuilt()) { + $this->buildPromise(); + } + + return $this->guzzlePromise->wait($unwrap); + } + + /** + * Determine if the promise has been created from the promise builder. + * + * @return bool + */ + public function promiseNeedsBuilt(): bool + { + return ! isset($this->guzzlePromise); + } +} diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index febc488c3a91..2aa862a169f5 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -3534,7 +3534,7 @@ public function testItCanEnforceFakingInThePool() return [ $pool->get('https://laravel.com'), ]; - }); + }, null); } public function testPreventingStrayRequests()