Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions src/Illuminate/Http/Client/Batch.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
33 changes: 24 additions & 9 deletions src/Illuminate/Http/Client/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<array-key, \Illuminate\Http\Client\Response|\Illuminate\Http\Client\ConnectionException|\Illuminate\Http\Client\RequestException>
*/
public function pool(callable $callback, ?int $concurrency = null)
Expand All @@ -896,20 +898,31 @@ 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();
}

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;
},
Expand Down Expand Up @@ -939,7 +952,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
Expand All @@ -957,7 +970,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;
Expand Down Expand Up @@ -1198,7 +1213,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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Illuminate\Http\Client;
namespace Illuminate\Http\Client\Promises;

use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Support\Traits\ForwardsCalls;
Expand Down
119 changes: 119 additions & 0 deletions src/Illuminate/Http/Client/Promises/LazyPromise.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace Illuminate\Http\Client\Promises;

use Closure;
use GuzzleHttp\Promise\PromiseInterface;
use RuntimeException;

class LazyPromise implements PromiseInterface
{
/**
* The callbacks to execute after the Guzzle Promise has been built.
*
* @var list<callable>
*/
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could possibly merge the behavior of FluentPromise with this by doing something like:

$result = $pendingCallback($this->guzzlePromise);
if ($result instanceof PromiseInterface) {
    $this->guzzlePromise = $result;
}

}

return $this->guzzlePromise;
}

/**
* If the promise has been created from the promise builder.
*
* @return bool
*/
public function promiseNeedsBuilt(): bool
{
return ! isset($this->guzzlePromise);
}

#[\Override]
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface
{
$this->pending[] = static fn (PromiseInterface $promise) => $promise->then($onFulfilled, $onRejected);

return $this;
}

#[\Override]
public function otherwise(callable $onRejected): PromiseInterface
{
$this->pending[] = static fn (PromiseInterface $promise) => $promise->otherwise($onRejected);

return $this;
}

#[\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);
}
}
2 changes: 1 addition & 1 deletion tests/Http/HttpClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3534,7 +3534,7 @@ public function testItCanEnforceFakingInThePool()
return [
$pool->get('https://laravel.com'),
];
});
}, null);
}

public function testPreventingStrayRequests()
Expand Down