暂存
This commit is contained in:
+451
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Client;
|
||||
|
||||
use Exception;
|
||||
|
||||
class HttpClientException extends Exception
|
||||
{
|
||||
//
|
||||
}
|
||||
+1897
File diff suppressed because it is too large
Load Diff
+96
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user