This commit is contained in:
2026-05-01 23:40:14 +08:00
commit b8f599a617
3867 changed files with 478663 additions and 0 deletions
+451
View File
@@ -0,0 +1,451 @@
<?php
namespace Illuminate\Http\Client;
use Carbon\CarbonImmutable;
use Closure;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\EachPromise;
use GuzzleHttp\Utils;
use Illuminate\Http\Client\Promises\LazyPromise;
use Illuminate\Support\Defer\DeferredCallback;
use function Illuminate\Support\defer;
/**
* @mixin \Illuminate\Http\Client\Factory
*/
class Batch
{
/**
* The factory instance.
*
* @var \Illuminate\Http\Client\Factory
*/
protected $factory;
/**
* The array of requests.
*
* @var array<array-key, \Illuminate\Http\Client\PendingRequest>
*/
protected $requests = [];
/**
* The total number of requests that belong to the batch.
*
* @var non-negative-int
*/
public $totalRequests = 0;
/**
* The total number of requests that are still pending.
*
* @var non-negative-int
*/
public $pendingRequests = 0;
/**
* The total number of requests that have failed.
*
* @var non-negative-int
*/
public $failedRequests = 0;
/**
* The handler function for the Guzzle client.
*
* @var callable
*/
protected $handler;
/**
* The callback to run before the first request from the batch runs.
*
* @var (\Closure($this): void)|null
*/
protected $beforeCallback = null;
/**
* The callback to run after a request from the batch succeeds.
*
* @var (\Closure($this, int|string, \Illuminate\Http\Client\Response): void)|null
*/
protected $progressCallback = null;
/**
* The callback to run after a request from the batch fails.
*
* @var (\Closure($this, int|string, \Illuminate\Http\Client\Response|\Illuminate\Http\Client\RequestException|\Illuminate\Http\Client\ConnectionException): void)|null
*/
protected $catchCallback = null;
/**
* The callback to run if all the requests from the batch succeeded.
*
* @var (\Closure($this, array<int|string, \Illuminate\Http\Client\Response>): void)|null
*/
protected $thenCallback = null;
/**
* The callback to run after all the requests from the batch finish.
*
* @var (\Closure($this, array<int|string, \Illuminate\Http\Client\Response>): void)|null
*/
protected $finallyCallback = null;
/**
* If the batch already was sent.
*
* @var bool
*/
protected $inProgress = false;
/**
* The date when the batch was created.
*
* @var \Carbon\CarbonImmutable|null
*/
public $createdAt = null;
/**
* The date when the batch finished.
*
* @var \Carbon\CarbonImmutable|null
*/
public $finishedAt = null;
/**
* The maximum number of concurrent requests.
*
* @var int|null
*/
protected $concurrencyLimit = null;
/**
* Create a new request batch instance.
*/
public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory;
$this->handler = Utils::chooseHandler();
$this->createdAt = new CarbonImmutable;
}
/**
* Add a request to the batch with a key.
*
* @param string $key
* @return \Illuminate\Http\Client\PendingRequest
*
* @throws \Illuminate\Http\Client\BatchInProgressException
*/
public function as(string $key)
{
if ($this->inProgress) {
throw new BatchInProgressException();
}
$this->incrementPendingRequests();
return $this->requests[$key] = $this->asyncRequest();
}
/**
* Add a request to the batch with a numeric index.
*
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
*
* @throws \Illuminate\Http\Client\BatchInProgressException
*/
public function newRequest()
{
if ($this->inProgress) {
throw new BatchInProgressException();
}
$this->incrementPendingRequests();
return $this->requests[] = $this->asyncRequest();
}
/**
* Register a callback to run before the first request from the batch runs.
*
* @param (\Closure($this): void) $callback
* @return Batch
*/
public function before(Closure $callback): self
{
$this->beforeCallback = $callback;
return $this;
}
/**
* Register a callback to run after a request from the batch succeeds.
*
* @param (\Closure($this, int|string, \Illuminate\Http\Client\Response): void) $callback
* @return Batch
*/
public function progress(Closure $callback): self
{
$this->progressCallback = $callback;
return $this;
}
/**
* Register a callback to run after a request from the batch fails.
*
* @param (\Closure($this, int|string, \Illuminate\Http\Client\Response|\Illuminate\Http\Client\RequestException|\Illuminate\Http\Client\ConnectionException): void) $callback
* @return Batch
*/
public function catch(Closure $callback): self
{
$this->catchCallback = $callback;
return $this;
}
/**
* Register a callback to run after all the requests from the batch succeed.
*
* @param (\Closure($this, array<int|string, \Illuminate\Http\Client\Response>): void) $callback
* @return Batch
*/
public function then(Closure $callback): self
{
$this->thenCallback = $callback;
return $this;
}
/**
* Register a callback to run after all the requests from the batch finish.
*
* @param (\Closure($this, array<int|string, \Illuminate\Http\Client\Response>): void) $callback
* @return Batch
*/
public function finally(Closure $callback): self
{
$this->finallyCallback = $callback;
return $this;
}
/**
* Set the maximum number of concurrent requests.
*
* @param int $limit
* @return Batch
*/
public function concurrency(int $limit): self
{
$this->concurrencyLimit = $limit;
return $this;
}
/**
* Defer the batch to run in the background after the current task has finished.
*
* @return \Illuminate\Support\Defer\DeferredCallback
*/
public function defer(): DeferredCallback
{
return defer(fn () => $this->send());
}
/**
* Send all of the requests in the batch.
*
* @return array<int|string, \Illuminate\Http\Client\Response|\Illuminate\Http\Client\RequestException>
*/
public function send(): array
{
$this->inProgress = true;
if ($this->beforeCallback !== null) {
call_user_func($this->beforeCallback, $this);
}
$results = [];
if (! empty($this->requests)) {
$eachPromiseOptions = [
'fulfilled' => function ($result, $key) use (&$results) {
$results[$key] = $result;
$this->decrementPendingRequests();
if ($result instanceof Response && $result->successful()) {
if ($this->progressCallback !== null) {
call_user_func($this->progressCallback, $this, $key, $result);
}
return $result;
}
if (
($result instanceof Response && $result->failed()) ||
$result instanceof RequestException ||
$result instanceof ConnectionException
) {
$this->incrementFailedRequests();
if ($this->catchCallback !== null) {
call_user_func($this->catchCallback, $this, $key, $result);
}
}
return $result;
},
'rejected' => function ($reason, $key) {
$this->decrementPendingRequests();
if ($reason instanceof RequestException || $reason instanceof ConnectionException) {
$this->incrementFailedRequests();
if ($this->catchCallback !== null) {
call_user_func($this->catchCallback, $this, $key, $reason);
}
}
return $reason;
},
];
if ($this->concurrencyLimit !== null) {
$eachPromiseOptions['concurrency'] = $this->concurrencyLimit;
}
$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
// in the same order as the requests were defined, respecting any custom
// key names that were assigned to this request using the "as" method.
uksort($results, function ($key1, $key2) {
return array_search($key1, array_keys($this->requests), true) <=>
array_search($key2, array_keys($this->requests), true);
});
if (! $this->hasFailures() && $this->thenCallback !== null) {
call_user_func($this->thenCallback, $this, $results);
}
if ($this->finallyCallback !== null) {
call_user_func($this->finallyCallback, $this, $results);
}
$this->finishedAt = new CarbonImmutable;
$this->inProgress = false;
return $results;
}
/**
* Retrieve a new async pending request.
*
* @return \Illuminate\Http\Client\PendingRequest
*/
protected function asyncRequest()
{
return $this->factory->setHandler($this->handler)->async();
}
/**
* Get the total number of requests that have been processed by the batch thus far.
*
* @return non-negative-int
*/
public function processedRequests(): int
{
return $this->totalRequests - $this->pendingRequests;
}
/**
* Determine if the batch has finished executing.
*
* @return bool
*/
public function finished(): bool
{
return ! is_null($this->finishedAt);
}
/**
* Increment the count of total and pending requests in the batch.
*
* @return void
*/
protected function incrementPendingRequests(): void
{
$this->totalRequests++;
$this->pendingRequests++;
}
/**
* Decrement the count of pending requests in the batch.
*
* @return void
*/
protected function decrementPendingRequests(): void
{
$this->pendingRequests--;
}
/**
* Determine if the batch has job failures.
*
* @return bool
*/
public function hasFailures(): bool
{
return $this->failedRequests > 0;
}
/**
* Increment the count of failed requests in the batch.
*
* @return void
*/
protected function incrementFailedRequests(): void
{
$this->failedRequests++;
}
/**
* Get the requests in the batch.
*
* @return array<array-key, \Illuminate\Http\Client\PendingRequest>
*/
public function getRequests(): array
{
return $this->requests;
}
/**
* Add a request to the batch with a numeric index.
*
* @param string $method
* @param array $parameters
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
*
* @throws \Illuminate\Http\Client\BatchInProgressException
*/
public function __call(string $method, array $parameters)
{
return $this->newRequest()->{$method}(...$parameters);
}
}
@@ -0,0 +1,11 @@
<?php
namespace Illuminate\Http\Client;
class BatchInProgressException extends HttpClientException
{
public function __construct()
{
parent::__construct('You cannot add requests to a batch that is already in progress.');
}
}
@@ -0,0 +1,177 @@
<?php
namespace Illuminate\Http\Client\Concerns;
trait DeterminesStatusCode
{
/**
* Determine if the response code was 200 "OK" response.
*
* @return bool
*/
public function ok()
{
return $this->status() === 200;
}
/**
* Determine if the response code was 201 "Created" response.
*
* @return bool
*/
public function created()
{
return $this->status() === 201;
}
/**
* Determine if the response code was 202 "Accepted" response.
*
* @return bool
*/
public function accepted()
{
return $this->status() === 202;
}
/**
* Determine if the response code was the given status code and the body has no content.
*
* @param int $status
* @return bool
*/
public function noContent($status = 204)
{
return $this->status() === $status && $this->body() === '';
}
/**
* Determine if the response code was a 301 "Moved Permanently".
*
* @return bool
*/
public function movedPermanently()
{
return $this->status() === 301;
}
/**
* Determine if the response code was a 302 "Found" response.
*
* @return bool
*/
public function found()
{
return $this->status() === 302;
}
/**
* Determine if the response code was a 304 "Not Modified" response.
*
* @return bool
*/
public function notModified()
{
return $this->status() === 304;
}
/**
* Determine if the response was a 400 "Bad Request" response.
*
* @return bool
*/
public function badRequest()
{
return $this->status() === 400;
}
/**
* Determine if the response was a 401 "Unauthorized" response.
*
* @return bool
*/
public function unauthorized()
{
return $this->status() === 401;
}
/**
* Determine if the response was a 402 "Payment Required" response.
*
* @return bool
*/
public function paymentRequired()
{
return $this->status() === 402;
}
/**
* Determine if the response was a 403 "Forbidden" response.
*
* @return bool
*/
public function forbidden()
{
return $this->status() === 403;
}
/**
* Determine if the response was a 404 "Not Found" response.
*
* @return bool
*/
public function notFound()
{
return $this->status() === 404;
}
/**
* Determine if the response was a 408 "Request Timeout" response.
*
* @return bool
*/
public function requestTimeout()
{
return $this->status() === 408;
}
/**
* Determine if the response was a 409 "Conflict" response.
*
* @return bool
*/
public function conflict()
{
return $this->status() === 409;
}
/**
* Determine if the response was a 422 "Unprocessable Content" response.
*
* @return bool
*/
public function unprocessableContent()
{
return $this->status() === 422;
}
/**
* Determine if the response was a 422 "Unprocessable Content" response.
*
* @return bool
*/
public function unprocessableEntity()
{
return $this->unprocessableContent();
}
/**
* Determine if the response was a 429 "Too Many Requests" response.
*
* @return bool
*/
public function tooManyRequests()
{
return $this->status() === 429;
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Http\Client;
class ConnectionException extends HttpClientException
{
//
}
@@ -0,0 +1,35 @@
<?php
namespace Illuminate\Http\Client\Events;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Request;
class ConnectionFailed
{
/**
* The request instance.
*
* @var \Illuminate\Http\Client\Request
*/
public $request;
/**
* The exception instance.
*
* @var \Illuminate\Http\Client\ConnectionException
*/
public $exception;
/**
* Create a new event instance.
*
* @param \Illuminate\Http\Client\Request $request
* @param \Illuminate\Http\Client\ConnectionException $exception
*/
public function __construct(Request $request, ConnectionException $exception)
{
$this->request = $request;
$this->exception = $exception;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Illuminate\Http\Client\Events;
use Illuminate\Http\Client\Request;
class RequestSending
{
/**
* The request instance.
*
* @var \Illuminate\Http\Client\Request
*/
public $request;
/**
* Create a new event instance.
*
* @param \Illuminate\Http\Client\Request $request
*/
public function __construct(Request $request)
{
$this->request = $request;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Illuminate\Http\Client\Events;
use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\Response;
class ResponseReceived
{
/**
* The request instance.
*
* @var \Illuminate\Http\Client\Request
*/
public $request;
/**
* The response instance.
*
* @var \Illuminate\Http\Client\Response
*/
public $response;
/**
* Create a new event instance.
*
* @param \Illuminate\Http\Client\Request $request
* @param \Illuminate\Http\Client\Response $response
*/
public function __construct(Request $request, Response $response)
{
$this->request = $request;
$this->response = $response;
}
}
+557
View File
@@ -0,0 +1,557 @@
<?php
namespace Illuminate\Http\Client;
use Closure;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Response as Psr7Response;
use GuzzleHttp\TransferStats;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use PHPUnit\Framework\Assert as PHPUnit;
/**
* @mixin \Illuminate\Http\Client\PendingRequest
*/
class Factory
{
use Macroable {
__call as macroCall;
}
/**
* The event dispatcher implementation.
*
* @var \Illuminate\Contracts\Events\Dispatcher|null
*/
protected $dispatcher;
/**
* The middleware to apply to every request.
*
* @var array
*/
protected $globalMiddleware = [];
/**
* The options to apply to every request.
*
* @var \Closure|array
*/
protected $globalOptions = [];
/**
* The stub callables that will handle requests.
*
* @var \Illuminate\Support\Collection
*/
protected $stubCallbacks;
/**
* Indicates if the factory is recording requests and responses.
*
* @var bool
*/
protected $recording = false;
/**
* The recorded response array.
*
* @var list<array{0: \Illuminate\Http\Client\Request, 1: \Illuminate\Http\Client\Response|null}>
*/
protected $recorded = [];
/**
* All created response sequences.
*
* @var list<\Illuminate\Http\Client\ResponseSequence>
*/
protected $responseSequences = [];
/**
* Indicates that an exception should be thrown if any request is not faked.
*
* @var bool
*/
protected $preventStrayRequests = false;
/**
* A list of URL patterns that are allowed to bypass the stray request guard.
*
* @var array<int, string>
*/
protected $allowedStrayRequestUrls = [];
/**
* Create a new factory instance.
*
* @param \Illuminate\Contracts\Events\Dispatcher|null $dispatcher
*/
public function __construct(?Dispatcher $dispatcher = null)
{
$this->dispatcher = $dispatcher;
$this->stubCallbacks = new Collection;
}
/**
* Add middleware to apply to every request.
*
* @param callable $middleware
* @return $this
*/
public function globalMiddleware($middleware)
{
$this->globalMiddleware[] = $middleware;
return $this;
}
/**
* Add request middleware to apply to every request.
*
* @param callable $middleware
* @return $this
*/
public function globalRequestMiddleware($middleware)
{
$this->globalMiddleware[] = Middleware::mapRequest($middleware);
return $this;
}
/**
* Add response middleware to apply to every request.
*
* @param callable $middleware
* @return $this
*/
public function globalResponseMiddleware($middleware)
{
$this->globalMiddleware[] = Middleware::mapResponse($middleware);
return $this;
}
/**
* Set the options to apply to every request.
*
* @param \Closure|array $options
* @return $this
*/
public function globalOptions($options)
{
$this->globalOptions = $options;
return $this;
}
/**
* Create a new response instance for use during stubbing.
*
* @param array|string|null $body
* @param int $status
* @param array $headers
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public static function response($body = null, $status = 200, $headers = [])
{
return Create::promiseFor(
static::psr7Response($body, $status, $headers)
);
}
/**
* Create a new PSR-7 response instance for use during stubbing.
*
* @param array|string|null $body
* @param int $status
* @param array<string, mixed> $headers
* @return \GuzzleHttp\Psr7\Response
*/
public static function psr7Response($body = null, $status = 200, $headers = [])
{
if (is_array($body)) {
$body = json_encode($body);
$headers['Content-Type'] = 'application/json';
}
return new Psr7Response($status, $headers, $body);
}
/**
* Create a new RequestException instance for use during stubbing.
*
* @param array|string|null $body
* @param int $status
* @param array<string, mixed> $headers
* @return \Illuminate\Http\Client\RequestException
*/
public static function failedRequest($body = null, $status = 200, $headers = [])
{
return new RequestException(new Response(static::psr7Response($body, $status, $headers)));
}
/**
* Create a new connection exception for use during stubbing.
*
* @param string|null $message
* @return \Closure(\Illuminate\Http\Client\Request): \GuzzleHttp\Promise\PromiseInterface
*/
public static function failedConnection($message = null)
{
return function ($request) use ($message) {
return Create::rejectionFor(new ConnectException(
$message ?? "cURL error 6: Could not resolve host: {$request->toPsrRequest()->getUri()->getHost()} (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for {$request->toPsrRequest()->getUri()}.",
$request->toPsrRequest(),
));
};
}
/**
* Get an invokable object that returns a sequence of responses in order for use during stubbing.
*
* @param array $responses
* @return \Illuminate\Http\Client\ResponseSequence
*/
public function sequence(array $responses = [])
{
return $this->responseSequences[] = new ResponseSequence($responses);
}
/**
* Register a stub callable that will intercept requests and be able to return stub responses.
*
* @param callable|array<string, mixed>|null $callback
* @return $this
*/
public function fake($callback = null)
{
$this->record();
$this->recorded = [];
if (is_null($callback)) {
$callback = function () {
return static::response();
};
}
if (is_array($callback)) {
foreach ($callback as $url => $callable) {
$this->stubUrl($url, $callable);
}
return $this;
}
$this->stubCallbacks = $this->stubCallbacks->merge(new Collection([
function ($request, $options) use ($callback) {
$response = $callback;
while ($response instanceof Closure) {
$response = $response($request, $options);
}
if ($response instanceof PromiseInterface) {
$options['on_stats'](new TransferStats(
$request->toPsrRequest(),
$response->wait(),
));
}
return $response;
},
]));
return $this;
}
/**
* Register a response sequence for the given URL pattern.
*
* @param string $url
* @return \Illuminate\Http\Client\ResponseSequence
*/
public function fakeSequence($url = '*')
{
return tap($this->sequence(), function ($sequence) use ($url) {
$this->fake([$url => $sequence]);
});
}
/**
* Stub the given URL using the given callback.
*
* @param string $url
* @param \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|callable|int|string|array|\Illuminate\Http\Client\ResponseSequence $callback
* @return $this
*/
public function stubUrl($url, $callback)
{
return $this->fake(function ($request, $options) use ($url, $callback) {
if (! Str::is(Str::start($url, '*'), $request->url())) {
return;
}
if (is_int($callback) && $callback >= 100 && $callback < 600) {
return static::response(status: $callback);
}
if (is_int($callback) || is_string($callback)) {
return static::response($callback);
}
if ($callback instanceof Closure || $callback instanceof ResponseSequence) {
return $callback($request, $options);
}
return $callback;
});
}
/**
* Indicate that an exception should be thrown if any request is not faked.
*
* @param bool $prevent
* @return $this
*/
public function preventStrayRequests($prevent = true)
{
$this->preventStrayRequests = $prevent;
return $this;
}
/**
* Determine if stray requests are being prevented.
*
* @return bool
*/
public function preventingStrayRequests()
{
return $this->preventStrayRequests;
}
/**
* Allow stray, unfaked requests entirely, or optionally allow only specific URLs.
*
* @param array<int, string>|null $only
* @return $this
*/
public function allowStrayRequests(?array $only = null)
{
if (is_null($only)) {
$this->preventStrayRequests(false);
$this->allowedStrayRequestUrls = [];
} else {
$this->allowedStrayRequestUrls = array_values($only);
}
return $this;
}
/**
* Begin recording request / response pairs.
*
* @return $this
*/
public function record()
{
$this->recording = true;
return $this;
}
/**
* Record a request response pair.
*
* @param \Illuminate\Http\Client\Request $request
* @param \Illuminate\Http\Client\Response|null $response
* @return void
*/
public function recordRequestResponsePair($request, $response)
{
if ($this->recording) {
$this->recorded[] = [$request, $response];
}
}
/**
* Assert that a request / response pair was recorded matching a given truth test.
*
* @param callable|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool) $callback
* @return void
*/
public function assertSent($callback)
{
PHPUnit::assertTrue(
$this->recorded($callback)->count() > 0,
'An expected request was not recorded.'
);
}
/**
* Assert that the given request was sent in the given order.
*
* @param list<string|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool)|callable> $callbacks
* @return void
*/
public function assertSentInOrder($callbacks)
{
$this->assertSentCount(count($callbacks));
foreach ($callbacks as $index => $url) {
$callback = is_callable($url) ? $url : function ($request) use ($url) {
return $request->url() == $url;
};
PHPUnit::assertTrue($callback(
$this->recorded[$index][0],
$this->recorded[$index][1]
), 'An expected request (#'.($index + 1).') was not recorded.');
}
}
/**
* Assert that a request / response pair was not recorded matching a given truth test.
*
* @param callable|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool) $callback
* @return void
*/
public function assertNotSent($callback)
{
PHPUnit::assertFalse(
$this->recorded($callback)->count() > 0,
'Unexpected request was recorded.'
);
}
/**
* Assert that no request / response pair was recorded.
*
* @return void
*/
public function assertNothingSent()
{
PHPUnit::assertEmpty(
$this->recorded,
'Requests were recorded.'
);
}
/**
* Assert how many requests have been recorded.
*
* @param int $count
* @return void
*/
public function assertSentCount($count)
{
PHPUnit::assertCount($count, $this->recorded);
}
/**
* Assert that every created response sequence is empty.
*
* @return void
*/
public function assertSequencesAreEmpty()
{
foreach ($this->responseSequences as $responseSequence) {
PHPUnit::assertTrue(
$responseSequence->isEmpty(),
'Not all response sequences are empty.'
);
}
}
/**
* Get a collection of the request / response pairs matching the given truth test.
*
* @param (\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool)|callable $callback
* @return \Illuminate\Support\Collection<int, array{0: \Illuminate\Http\Client\Request, 1: \Illuminate\Http\Client\Response|null}>
*/
public function recorded($callback = null)
{
if (empty($this->recorded)) {
return new Collection;
}
$collect = new Collection($this->recorded);
if ($callback) {
return $collect->filter(fn ($pair) => $callback($pair[0], $pair[1]));
}
return $collect;
}
/**
* Create a new pending request instance for this factory.
*
* @return \Illuminate\Http\Client\PendingRequest
*/
public function createPendingRequest()
{
return tap($this->newPendingRequest(), function ($request) {
$request
->stub($this->stubCallbacks)
->preventStrayRequests($this->preventStrayRequests)
->allowStrayRequests($this->allowedStrayRequestUrls);
});
}
/**
* Instantiate a new pending request instance for this factory.
*
* @return \Illuminate\Http\Client\PendingRequest
*/
protected function newPendingRequest()
{
return (new PendingRequest($this, $this->globalMiddleware))->withOptions(value($this->globalOptions));
}
/**
* Get the current event dispatcher implementation.
*
* @return \Illuminate\Contracts\Events\Dispatcher|null
*/
public function getDispatcher()
{
return $this->dispatcher;
}
/**
* Get the array of global middleware.
*
* @return array
*/
public function getGlobalMiddleware()
{
return $this->globalMiddleware;
}
/**
* Execute a method against a new pending request instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
return $this->createPendingRequest()->{$method}(...$parameters);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Http\Client;
use Exception;
class HttpClientException extends Exception
{
//
}
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
<?php
namespace Illuminate\Http\Client;
use GuzzleHttp\Utils;
/**
* @mixin \Illuminate\Http\Client\Factory
*/
class Pool
{
/**
* The factory instance.
*
* @var \Illuminate\Http\Client\Factory
*/
protected $factory;
/**
* The handler function for the Guzzle client.
*
* @var callable
*/
protected $handler;
/**
* The pool of requests.
*
* @var array<array-key, \Illuminate\Http\Client\PendingRequest>
*/
protected $pool = [];
/**
* Create a new requests pool.
*
* @param \Illuminate\Http\Client\Factory|null $factory
*/
public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory();
$this->handler = Utils::chooseHandler();
}
/**
* Add a request to the pool with a numeric index.
*
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
*/
public function newRequest()
{
return $this->pool[] = $this->asyncRequest();
}
/**
* Add a request to the pool with a key.
*
* @param string $key
* @return \Illuminate\Http\Client\PendingRequest
*/
public function as(string $key)
{
return $this->pool[$key] = $this->asyncRequest();
}
/**
* Retrieve a new async pending request.
*
* @return \Illuminate\Http\Client\PendingRequest
*/
protected function asyncRequest()
{
return $this->factory->setHandler($this->handler)->async();
}
/**
* Retrieve the requests in the pool.
*
* @return array<array-key, \Illuminate\Http\Client\PendingRequest>
*/
public function getRequests()
{
return $this->pool;
}
/**
* Add a request to the pool with a numeric index and forward the method call to the request.
*
* @param string $method
* @param array $parameters
* @return \Illuminate\Http\Client\PendingRequest|\GuzzleHttp\Promise\Promise
*/
public function __call($method, $parameters)
{
return $this->newRequest()->{$method}(...$parameters);
}
}
@@ -0,0 +1,95 @@
<?php
namespace Illuminate\Http\Client\Promises;
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 \GuzzleHttp\Promise\PromiseInterface $guzzlePromise
*/
public function __construct(protected PromiseInterface $guzzlePromise)
{
}
#[\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 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]);
}
#[\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;
}
}
+129
View File
@@ -0,0 +1,129 @@
<?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);
}
$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);
}
}
+335
View File
@@ -0,0 +1,335 @@
<?php
namespace Illuminate\Http\Client;
use ArrayAccess;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Traits\Macroable;
use LogicException;
class Request implements ArrayAccess
{
use Macroable;
/**
* The underlying PSR request.
*
* @var \Psr\Http\Message\RequestInterface
*/
protected $request;
/**
* The decoded payload for the request.
*
* @var array
*/
protected $data;
/**
* The attribute data passed when building the PendingRequest.
*
* @var array<array-key, mixed>
*/
protected $attributes = [];
/**
* Create a new request instance.
*
* @param \Psr\Http\Message\RequestInterface $request
*/
public function __construct($request)
{
$this->request = $request;
}
/**
* Get the request method.
*
* @return string
*/
public function method()
{
return $this->request->getMethod();
}
/**
* Get the URL of the request.
*
* @return string
*/
public function url()
{
return (string) $this->request->getUri();
}
/**
* Determine if the request has a given header.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function hasHeader($key, $value = null)
{
if (is_null($value)) {
return ! empty($this->request->getHeaders()[$key]);
}
$headers = $this->headers();
if (! Arr::has($headers, $key)) {
return false;
}
$value = is_array($value) ? $value : [$value];
return empty(array_diff($value, $headers[$key]));
}
/**
* Determine if the request has the given headers.
*
* @param array|string $headers
* @return bool
*/
public function hasHeaders($headers)
{
if (is_string($headers)) {
$headers = [$headers => null];
}
foreach ($headers as $key => $value) {
if (! $this->hasHeader($key, $value)) {
return false;
}
}
return true;
}
/**
* Get the values for the header with the given name.
*
* @param string $key
* @return array
*/
public function header($key)
{
return Arr::get($this->headers(), $key, []);
}
/**
* Get the request headers.
*
* @return array
*/
public function headers()
{
return $this->request->getHeaders();
}
/**
* Get the body of the request.
*
* @return string
*/
public function body()
{
return (string) $this->request->getBody();
}
/**
* Determine if the request contains the given file.
*
* @param string $name
* @param string|null $value
* @param string|null $filename
* @return bool
*/
public function hasFile($name, $value = null, $filename = null)
{
if (! $this->isMultipart()) {
return false;
}
return (new Collection($this->data))->reject(function ($file) use ($name, $value, $filename) {
return $file['name'] != $name ||
($value && $file['contents'] != $value) ||
($filename && $file['filename'] != $filename);
})->count() > 0;
}
/**
* Get the request's data (form parameters or JSON).
*
* @return array
*/
public function data()
{
if ($this->isForm()) {
return $this->parameters();
} elseif ($this->isJson()) {
return $this->json();
}
return $this->data ?? [];
}
/**
* Get the request's form parameters.
*
* @return array
*/
protected function parameters()
{
if (! $this->data) {
parse_str($this->body(), $parameters);
$this->data = $parameters;
}
return $this->data;
}
/**
* Get the decoded JSON body of the request.
*
* @return array
*/
protected function json()
{
if (! $this->data) {
$this->data = json_decode($this->body(), true) ?? [];
}
return $this->data;
}
/**
* Determine if the request is simple form data.
*
* @return bool
*/
public function isForm()
{
return $this->hasHeader('Content-Type', 'application/x-www-form-urlencoded');
}
/**
* Determine if the request is JSON.
*
* @return bool
*/
public function isJson()
{
return $this->hasHeader('Content-Type') &&
str_contains($this->header('Content-Type')[0], 'json');
}
/**
* Determine if the request is multipart.
*
* @return bool
*/
public function isMultipart()
{
return $this->hasHeader('Content-Type') &&
str_contains($this->header('Content-Type')[0], 'multipart');
}
/**
* Set the decoded data on the request.
*
* @param array $data
* @return $this
*/
public function withData(array $data)
{
$this->data = $data;
return $this;
}
/**
* Get the attribute data from the request.
*
* @return array<array-key, mixed>
*/
public function attributes()
{
return $this->attributes;
}
/**
* Set the request's attribute data.
*
* @param array<array-key, mixed> $attributes
* @return $this
*/
public function setRequestAttributes($attributes)
{
$this->attributes = $attributes;
return $this;
}
/**
* Get the underlying PSR compliant request instance.
*
* @return \Psr\Http\Message\RequestInterface
*/
public function toPsrRequest()
{
return $this->request;
}
/**
* Determine if the given offset exists.
*
* @param string $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->data()[$offset]);
}
/**
* Get the value for a given offset.
*
* @param string $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->data()[$offset];
}
/**
* Set the value at the given offset.
*
* @param string $offset
* @param mixed $value
* @return void
*
* @throws \LogicException
*/
public function offsetSet($offset, $value): void
{
throw new LogicException('Request data may not be mutated using array access.');
}
/**
* Unset the value at the given offset.
*
* @param string $offset
* @return void
*
* @throws \LogicException
*/
public function offsetUnset($offset): void
{
throw new LogicException('Request data may not be mutated using array access.');
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
namespace Illuminate\Http\Client;
use GuzzleHttp\Psr7\Message;
class RequestException extends HttpClientException
{
/**
* The response instance.
*
* @var \Illuminate\Http\Client\Response
*/
public $response;
/**
* The current truncation length for the exception message.
*
* @var int|false|null
*/
public $truncateExceptionsAt;
/**
* The global truncation length for the exception message.
*
* @var int|false
*/
public static $truncateAt = 120;
/**
* Whether the response has been summarized in the message.
*
* @var bool
*/
public $hasBeenSummarized = false;
/**
* Create a new exception instance.
*
* @param \Illuminate\Http\Client\Response $response
* @param int|false|null $truncateExceptionsAt
*/
public function __construct(Response $response, $truncateExceptionsAt = null)
{
$this->truncateExceptionsAt = $truncateExceptionsAt;
$this->response = $response;
parent::__construct($this->prepareMessage($response), $response->status());
}
/**
* Enable truncation of request exception messages.
*
* @return void
*/
public static function truncate()
{
static::$truncateAt = 120;
}
/**
* Set the truncation length for request exception messages.
*
* @param int $length
* @return void
*/
public static function truncateAt(int $length)
{
static::$truncateAt = $length;
}
/**
* Disable truncation of request exception messages.
*
* @return void
*/
public static function dontTruncate()
{
static::$truncateAt = false;
}
/**
* Prepare the exception message.
*
* @return bool
*/
public function report()
{
if (! $this->hasBeenSummarized) {
$this->message = $this->prepareMessage($this->response);
$this->hasBeenSummarized = true;
}
return false;
}
/**
* Prepare the exception message.
*
* @param \Illuminate\Http\Client\Response $response
* @return string
*/
protected function prepareMessage(Response $response)
{
$message = "HTTP request returned status code {$response->status()}";
$truncateExceptionsAt = $this->truncateExceptionsAt ?? static::$truncateAt;
$psrResponse = $response->toPsrResponse();
$summary = null;
if (is_int($truncateExceptionsAt)) {
$summary = Message::bodySummary($psrResponse, $truncateExceptionsAt);
} elseif (($body = $psrResponse->getBody())->isSeekable() && $body->isReadable()) {
$summary = Message::toString($psrResponse);
}
return is_null($summary) ? $message : $message.":\n{$summary}\n";
}
}
+610
View File
@@ -0,0 +1,610 @@
<?php
namespace Illuminate\Http\Client;
use ArrayAccess;
use GuzzleHttp\Psr7\StreamWrapper;
use Illuminate\Support\Collection;
use Illuminate\Support\Fluent;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\Tappable;
use LogicException;
use Stringable;
/**
* @mixin \Psr\Http\Message\ResponseInterface
*/
class Response implements ArrayAccess, Stringable
{
use Concerns\DeterminesStatusCode, Tappable, Macroable {
__call as macroCall;
}
/**
* The underlying PSR response.
*
* @var \Psr\Http\Message\ResponseInterface
*/
protected $response;
/**
* The decoded JSON response.
*
* @var array
*/
protected $decoded;
/**
* The flags that were used when decoding the JSON response.
*
* @var int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>
*/
protected int $decodingFlags;
/**
* The request cookies.
*
* @var \GuzzleHttp\Cookie\CookieJar
*/
public $cookies;
/**
* The transfer stats for the request.
*
* @var \GuzzleHttp\TransferStats|null
*/
public $transferStats;
/**
* The length at which request exceptions will be truncated.
*
* @var int<1, max>|false|null
*/
protected $truncateExceptionsAt = null;
/**
* The flags passed to `json_decode` by default.
*
* @var int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>
*/
public static int $defaultJsonDecodingFlags = 0;
/**
* Create a new response instance.
*
* @param \Psr\Http\Message\MessageInterface $response
*/
public function __construct($response)
{
$this->response = $response;
}
/**
* Get the body of the response.
*
* @return string
*/
public function body()
{
return (string) $this->response->getBody();
}
/**
* Get the decoded JSON body of the response as an array or scalar value.
*
* @param string|null $key
* @param mixed $default
* @param int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>|null $flags
* @return mixed
*/
public function json($key = null, $default = null, $flags = null)
{
$flags ??= self::$defaultJsonDecodingFlags;
if (! $this->decoded || (isset($this->decodingFlags) && $this->decodingFlags !== $flags)) {
$this->decoded = json_decode(
$this->body(), true, flags: $flags
);
$this->decodingFlags = $flags;
}
if (is_null($key)) {
return $this->decoded;
}
return data_get($this->decoded, $key, $default);
}
/**
* Get the decoded JSON body of the response as an object.
*
* @param int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>|null $flags
* @return object|null
*/
public function object($flags = null)
{
return json_decode($this->body(), false, flags: $flags ?? self::$defaultJsonDecodingFlags);
}
/**
* Get the decoded JSON body of the response as a collection.
*
* @param string|null $key
* @param int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>|null $flags
* @return \Illuminate\Support\Collection
*/
public function collect($key = null, $flags = null)
{
return new Collection($this->json($key, flags: $flags));
}
/**
* Get the decoded JSON body of the response as a fluent object.
*
* @param string|null $key
* @param int-mask<JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR>|null $flags
* @return \Illuminate\Support\Fluent
*/
public function fluent($key = null, $flags = null)
{
return new Fluent((array) $this->json($key, flags: $flags));
}
/**
* Get the body of the response as a PHP resource.
*
* @return resource
*
* @throws \InvalidArgumentException
*/
public function resource()
{
return StreamWrapper::getResource($this->response->getBody());
}
/**
* Get a header from the response.
*
* @param string $header
* @return string
*/
public function header(string $header)
{
return $this->response->getHeaderLine($header);
}
/**
* Get the headers from the response.
*
* @return array
*/
public function headers()
{
return $this->response->getHeaders();
}
/**
* Get the status code of the response.
*
* @return int
*/
public function status()
{
return (int) $this->response->getStatusCode();
}
/**
* Get the reason phrase of the response.
*
* @return string
*/
public function reason()
{
return $this->response->getReasonPhrase();
}
/**
* Get the effective URI of the response.
*
* @return \Psr\Http\Message\UriInterface|null
*/
public function effectiveUri()
{
return $this->transferStats?->getEffectiveUri();
}
/**
* Determine if the request was successful.
*
* @return bool
*/
public function successful()
{
return $this->status() >= 200 && $this->status() < 300;
}
/**
* Determine if the response was a redirect.
*
* @return bool
*/
public function redirect()
{
return $this->status() >= 300 && $this->status() < 400;
}
/**
* Determine if the response indicates a client or server error occurred.
*
* @return bool
*/
public function failed()
{
return $this->serverError() || $this->clientError();
}
/**
* Determine if the response indicates a client error occurred.
*
* @return bool
*/
public function clientError()
{
return $this->status() >= 400 && $this->status() < 500;
}
/**
* Determine if the response indicates a server error occurred.
*
* @return bool
*/
public function serverError()
{
return $this->status() >= 500;
}
/**
* Execute the given callback if there was a server or client error.
*
* @param callable|(\Closure(\Illuminate\Http\Client\Response): mixed) $callback
* @return $this
*/
public function onError(callable $callback)
{
if ($this->failed()) {
$callback($this);
}
return $this;
}
/**
* Get the response cookies.
*
* @return \GuzzleHttp\Cookie\CookieJar
*/
public function cookies()
{
return $this->cookies;
}
/**
* Get the handler stats of the response.
*
* @return array
*/
public function handlerStats()
{
return $this->transferStats?->getHandlerStats() ?? [];
}
/**
* Close the stream and any underlying resources.
*
* @return $this
*/
public function close()
{
$this->response->getBody()->close();
return $this;
}
/**
* Get the underlying PSR response for the response.
*
* @return \Psr\Http\Message\ResponseInterface
*/
public function toPsrResponse()
{
return $this->response;
}
/**
* Create an exception if a server or client error occurred.
*
* @return \Illuminate\Http\Client\RequestException|null
*/
public function toException()
{
if ($this->failed()) {
return new RequestException($this, $this->truncateExceptionsAt);
}
}
/**
* Throw an exception if a server or client error occurred.
*
* @param null|(\Closure(\Illuminate\Http\Client\Response, \Illuminate\Http\Client\RequestException): mixed) $callback
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throw($callback = null)
{
if ($this->failed()) {
throw tap($this->toException(), function ($exception) use ($callback) {
if ($callback && is_callable($callback)) {
$callback($this, $exception);
}
});
}
return $this;
}
/**
* Throw an exception if a server or client error occurred and the given condition evaluates to true.
*
* @param \Closure|bool $condition
* @param null|(\Closure(\Illuminate\Http\Client\Response, \Illuminate\Http\Client\RequestException): mixed) $callback
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwIf($condition, $callback = null)
{
return value($condition, $this) ? $this->throw($callback) : $this;
}
/**
* Throw an exception if a server or client error occurred and the given condition evaluates to false.
*
* @param \Closure|bool $condition
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwUnless($condition)
{
return $this->throwIf(! $condition);
}
/**
* Throw an exception if the response status code matches the given code.
*
* @param int|(\Closure(int, \Illuminate\Http\Client\Response): bool)|callable $statusCode
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwIfStatus($statusCode)
{
if (is_callable($statusCode) &&
$statusCode($this->status(), $this)) {
throw new RequestException($this, $this->truncateExceptionsAt);
}
return $this->status() === $statusCode ? throw new RequestException($this, $this->truncateExceptionsAt) : $this;
}
/**
* Throw an exception unless the response status code matches the given code.
*
* @param int|(\Closure(int, \Illuminate\Http\Client\Response): bool)|callable $statusCode
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwUnlessStatus($statusCode)
{
if (is_callable($statusCode)) {
return $statusCode($this->status(), $this) ? $this : throw new RequestException($this, $this->truncateExceptionsAt);
}
return $this->status() === $statusCode ? $this : throw new RequestException($this, $this->truncateExceptionsAt);
}
/**
* Throw an exception if the response status code is a 4xx level code.
*
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwIfClientError()
{
return $this->clientError() ? $this->throw() : $this;
}
/**
* Throw an exception if the response status code is a 5xx level code.
*
* @return $this
*
* @throws \Illuminate\Http\Client\RequestException
*/
public function throwIfServerError()
{
return $this->serverError() ? $this->throw() : $this;
}
/**
* Indicate that request exceptions should be truncated to the given length.
*
* @param int<1, max> $length
* @return $this
*/
public function truncateExceptionsAt(int $length)
{
$this->truncateExceptionsAt = $length;
return $this;
}
/**
* Indicate that request exceptions should not be truncated.
*
* @return $this
*/
public function dontTruncateExceptions()
{
$this->truncateExceptionsAt = false;
return $this;
}
/**
* Dump the content from the response.
*
* @param string|null $key
* @return $this
*/
public function dump($key = null)
{
$content = $this->body();
$json = json_decode($content);
if (json_last_error() === JSON_ERROR_NONE) {
$content = $json;
}
if ($request = $this->transferStats?->getRequest()) {
dump('"'.$request->getMethod().' '.$request->getUri().'" '.$this->status());
}
dump(is_null($key) ? $content : data_get($content, $key));
return $this;
}
/**
* Dump the content from the response and end the script.
*
* @param string|null $key
* @return never
*/
public function dd($key = null)
{
$this->dump($key);
exit(1);
}
/**
* Dump the headers from the response.
*
* @return $this
*/
public function dumpHeaders()
{
dump($this->headers());
return $this;
}
/**
* Dump the headers from the response and end the script.
*
* @return never
*/
public function ddHeaders()
{
$this->dumpHeaders();
exit(1);
}
/**
* Determine if the given offset exists.
*
* @param string $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->json()[$offset]);
}
/**
* Get the value for a given offset.
*
* @param string $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->json()[$offset];
}
/**
* Set the value at the given offset.
*
* @param string $offset
* @param mixed $value
* @return void
*
* @throws \LogicException
*/
public function offsetSet($offset, $value): void
{
throw new LogicException('Response data may not be mutated using array access.');
}
/**
* Unset the value at the given offset.
*
* @param string $offset
* @return void
*
* @throws \LogicException
*/
public function offsetUnset($offset): void
{
throw new LogicException('Response data may not be mutated using array access.');
}
/**
* Get the body of the response.
*
* @return string
*/
public function __toString()
{
return $this->body();
}
/**
* Dynamically proxy other methods to the underlying response.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return static::hasMacro($method)
? $this->macroCall($method, $parameters)
: $this->response->{$method}(...$parameters);
}
/**
* Flush the global state of the Response.
*/
public static function flushState(): void
{
self::$defaultJsonDecodingFlags = 0;
}
}
+172
View File
@@ -0,0 +1,172 @@
<?php
namespace Illuminate\Http\Client;
use Closure;
use Illuminate\Support\Traits\Macroable;
use OutOfBoundsException;
class ResponseSequence
{
use Macroable;
/**
* The responses in the sequence.
*
* @var array
*/
protected $responses;
/**
* Indicates that invoking this sequence when it is empty should throw an exception.
*
* @var bool
*/
protected $failWhenEmpty = true;
/**
* The response that should be returned when the sequence is empty.
*
* @var \GuzzleHttp\Promise\PromiseInterface
*/
protected $emptyResponse;
/**
* Create a new response sequence.
*
* @param array $responses
*/
public function __construct(array $responses)
{
$this->responses = $responses;
}
/**
* Push a response to the sequence.
*
* @param string|array|null $body
* @param int $status
* @param array $headers
* @return $this
*/
public function push($body = null, int $status = 200, array $headers = [])
{
return $this->pushResponse(
Factory::response($body, $status, $headers)
);
}
/**
* Push a response with the given status code to the sequence.
*
* @param int $status
* @param array $headers
* @return $this
*/
public function pushStatus(int $status, array $headers = [])
{
return $this->pushResponse(
Factory::response('', $status, $headers)
);
}
/**
* Push a response with the contents of a file as the body to the sequence.
*
* @param string $filePath
* @param int $status
* @param array $headers
* @return $this
*/
public function pushFile(string $filePath, int $status = 200, array $headers = [])
{
$string = file_get_contents($filePath);
return $this->pushResponse(
Factory::response($string, $status, $headers)
);
}
/**
* Push a connection exception to the sequence.
*
* @param string|null $message
* @return $this
*/
public function pushFailedConnection($message = null)
{
return $this->pushResponse(
Factory::failedConnection($message)
);
}
/**
* Push a response to the sequence.
*
* @param mixed $response
* @return $this
*/
public function pushResponse($response)
{
$this->responses[] = $response;
return $this;
}
/**
* Make the sequence return a default response when it is empty.
*
* @param \GuzzleHttp\Promise\PromiseInterface|\Closure $response
* @return $this
*/
public function whenEmpty($response)
{
$this->failWhenEmpty = false;
$this->emptyResponse = $response;
return $this;
}
/**
* Make the sequence return a default response when it is empty.
*
* @return $this
*/
public function dontFailWhenEmpty()
{
return $this->whenEmpty(Factory::response());
}
/**
* Indicate that this sequence has depleted all of its responses.
*
* @return bool
*/
public function isEmpty()
{
return count($this->responses) === 0;
}
/**
* Get the next response in the sequence.
*
* @param \Illuminate\Http\Client\Request $request
* @return mixed
*
* @throws \OutOfBoundsException
*/
public function __invoke($request)
{
if ($this->failWhenEmpty && $this->isEmpty()) {
throw new OutOfBoundsException('A request was made, but the response sequence is empty.');
}
if (! $this->failWhenEmpty && $this->isEmpty()) {
return value($this->emptyResponse ?? Factory::response());
}
$response = array_shift($this->responses);
return $response instanceof Closure ? $response($request) : $response;
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Illuminate\Http\Client;
use RuntimeException;
class StrayRequestException extends RuntimeException
{
public function __construct(string $uri)
{
parent::__construct('Attempted request to ['.$uri.'] without a matching fake.');
}
}