Viewing file: PendingRequest.php (23.66 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
namespace Illuminate\Http\Client;
use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\TransferException; use GuzzleHttp\HandlerStack; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; use Psr\Http\Message\MessageInterface; use Symfony\Component\VarDumper\VarDumper;
class PendingRequest { use Macroable;
/** * The factory instance. * * @var \Illuminate\Http\Client\Factory|null */ protected $factory;
/** * The Guzzle client instance. * * @var \GuzzleHttp\Client */ protected $client;
/** * The base URL for the request. * * @var string */ protected $baseUrl = '';
/** * The request body format. * * @var string */ protected $bodyFormat;
/** * The raw body for the request. * * @var string */ protected $pendingBody;
/** * The pending files for the request. * * @var array */ protected $pendingFiles = [];
/** * The request cookies. * * @var array */ protected $cookies;
/** * The transfer stats for the request. * * \GuzzleHttp\TransferStats */ protected $transferStats;
/** * The request options. * * @var array */ protected $options = [];
/** * The number of times to try the request. * * @var int */ protected $tries = 1;
/** * The number of milliseconds to wait between retries. * * @var int */ protected $retryDelay = 100;
/** * The callbacks that should execute before the request is sent. * * @var \Illuminate\Support\Collection */ protected $beforeSendingCallbacks;
/** * The stub callables that will handle requests. * * @var \Illuminate\Support\Collection|null */ protected $stubCallbacks;
/** * The middleware callables added by users that will handle requests. * * @var \Illuminate\Support\Collection */ protected $middleware;
/** * Whether the requests should be asynchronous. * * @var bool */ protected $async = false;
/** * The pending request promise. * * @var \GuzzleHttp\Promise\PromiseInterface */ protected $promise;
/** * Create a new HTTP Client instance. * * @param \Illuminate\Http\Client\Factory|null $factory * @return void */ public function __construct(Factory $factory = null) { $this->factory = $factory; $this->middleware = new Collection;
$this->asJson();
$this->options = [ 'http_errors' => false, ];
$this->beforeSendingCallbacks = collect([function (Request $request, array $options) { $this->cookies = $options['cookies']; }]); }
/** * Set the base URL for the pending request. * * @param string $url * @return $this */ public function baseUrl(string $url) { $this->baseUrl = $url;
return $this; }
/** * Attach a raw body to the request. * * @param resource|string $content * @param string $contentType * @return $this */ public function withBody($content, $contentType) { $this->bodyFormat('body');
$this->pendingBody = $content;
$this->contentType($contentType);
return $this; }
/** * Indicate the request contains JSON. * * @return $this */ public function asJson() { return $this->bodyFormat('json')->contentType('application/json'); }
/** * Indicate the request contains form parameters. * * @return $this */ public function asForm() { return $this->bodyFormat('form_params')->contentType('application/x-www-form-urlencoded'); }
/** * Attach a file to the request. * * @param string|array $name * @param string $contents * @param string|null $filename * @param array $headers * @return $this */ public function attach($name, $contents = '', $filename = null, array $headers = []) { if (is_array($name)) { foreach ($name as $file) { $this->attach(...$file); }
return $this; }
$this->asMultipart();
$this->pendingFiles[] = array_filter([ 'name' => $name, 'contents' => $contents, 'headers' => $headers, 'filename' => $filename, ]);
return $this; }
/** * Indicate the request is a multi-part form request. * * @return $this */ public function asMultipart() { return $this->bodyFormat('multipart'); }
/** * Specify the body format of the request. * * @param string $format * @return $this */ public function bodyFormat(string $format) { return tap($this, function ($request) use ($format) { $this->bodyFormat = $format; }); }
/** * Specify the request's content type. * * @param string $contentType * @return $this */ public function contentType(string $contentType) { return $this->withHeaders(['Content-Type' => $contentType]); }
/** * Indicate that JSON should be returned by the server. * * @return $this */ public function acceptJson() { return $this->accept('application/json'); }
/** * Indicate the type of content that should be returned by the server. * * @param string $contentType * @return $this */ public function accept($contentType) { return $this->withHeaders(['Accept' => $contentType]); }
/** * Add the given headers to the request. * * @param array $headers * @return $this */ public function withHeaders(array $headers) { return tap($this, function ($request) use ($headers) { return $this->options = array_merge_recursive($this->options, [ 'headers' => $headers, ]); }); }
/** * Specify the basic authentication username and password for the request. * * @param string $username * @param string $password * @return $this */ public function withBasicAuth(string $username, string $password) { return tap($this, function ($request) use ($username, $password) { return $this->options['auth'] = [$username, $password]; }); }
/** * Specify the digest authentication username and password for the request. * * @param string $username * @param string $password * @return $this */ public function withDigestAuth($username, $password) { return tap($this, function ($request) use ($username, $password) { return $this->options['auth'] = [$username, $password, 'digest']; }); }
/** * Specify an authorization token for the request. * * @param string $token * @param string $type * @return $this */ public function withToken($token, $type = 'Bearer') { return tap($this, function ($request) use ($token, $type) { return $this->options['headers']['Authorization'] = trim($type.' '.$token); }); }
/** * Specify the user agent for the request. * * @param string $userAgent * @return $this */ public function withUserAgent($userAgent) { return $this->withHeaders(['User-Agent' => $userAgent]); }
/** * Specify the cookies that should be included with the request. * * @param array $cookies * @param string $domain * @return $this */ public function withCookies(array $cookies, string $domain) { return tap($this, function ($request) use ($cookies, $domain) { return $this->options = array_merge_recursive($this->options, [ 'cookies' => CookieJar::fromArray($cookies, $domain), ]); }); }
/** * Indicate that redirects should not be followed. * * @return $this */ public function withoutRedirecting() { return tap($this, function ($request) { return $this->options['allow_redirects'] = false; }); }
/** * Indicate that TLS certificates should not be verified. * * @return $this */ public function withoutVerifying() { return tap($this, function ($request) { return $this->options['verify'] = false; }); }
/** * Specify the path where the body of the response should be stored. * * @param string|resource $to * @return $this */ public function sink($to) { return tap($this, function ($request) use ($to) { return $this->options['sink'] = $to; }); }
/** * Specify the timeout (in seconds) for the request. * * @param int $seconds * @return $this */ public function timeout(int $seconds) { return tap($this, function () use ($seconds) { $this->options['timeout'] = $seconds; }); }
/** * Specify the number of times the request should be attempted. * * @param int $times * @param int $sleep * @return $this */ public function retry(int $times, int $sleep = 0) { $this->tries = $times; $this->retryDelay = $sleep;
return $this; }
/** * Merge new options into the client. * * @param array $options * @return $this */ public function withOptions(array $options) { return tap($this, function ($request) use ($options) { return $this->options = array_merge_recursive($this->options, $options); }); }
/** * Add new middleware the client handler stack. * * @param callable $middleware * @return $this */ public function withMiddleware(callable $middleware) { $this->middleware->push($middleware);
return $this; }
/** * Add a new "before sending" callback to the request. * * @param callable $callback * @return $this */ public function beforeSending($callback) { return tap($this, function () use ($callback) { $this->beforeSendingCallbacks[] = $callback; }); }
/** * Dump the request before sending. * * @return $this */ public function dump() { $values = func_get_args();
return $this->beforeSending(function (Request $request, array $options) use ($values) { foreach (array_merge($values, [$request, $options]) as $value) { VarDumper::dump($value); } }); }
/** * Dump the request before sending and end the script. * * @return $this */ public function dd() { $values = func_get_args();
return $this->beforeSending(function (Request $request, array $options) use ($values) { foreach (array_merge($values, [$request, $options]) as $value) { VarDumper::dump($value); }
exit(1); }); }
/** * Issue a GET request to the given URL. * * @param string $url * @param array|string|null $query * @return \Illuminate\Http\Client\Response */ public function get(string $url, $query = null) { return $this->send('GET', $url, [ 'query' => $query, ]); }
/** * Issue a HEAD request to the given URL. * * @param string $url * @param array|string|null $query * @return \Illuminate\Http\Client\Response */ public function head(string $url, $query = null) { return $this->send('HEAD', $url, [ 'query' => $query, ]); }
/** * Issue a POST request to the given URL. * * @param string $url * @param array $data * @return \Illuminate\Http\Client\Response */ public function post(string $url, array $data = []) { return $this->send('POST', $url, [ $this->bodyFormat => $data, ]); }
/** * Issue a PATCH request to the given URL. * * @param string $url * @param array $data * @return \Illuminate\Http\Client\Response */ public function patch($url, $data = []) { return $this->send('PATCH', $url, [ $this->bodyFormat => $data, ]); }
/** * Issue a PUT request to the given URL. * * @param string $url * @param array $data * @return \Illuminate\Http\Client\Response */ public function put($url, $data = []) { return $this->send('PUT', $url, [ $this->bodyFormat => $data, ]); }
/** * Issue a DELETE request to the given URL. * * @param string $url * @param array $data * @return \Illuminate\Http\Client\Response */ public function delete($url, $data = []) { return $this->send('DELETE', $url, empty($data) ? [] : [ $this->bodyFormat => $data, ]); }
/** * Send a pool of asynchronous requests concurrently. * * @param callable $callback * @return array */ public function pool(callable $callback) { $results = [];
$requests = tap(new Pool($this->factory), $callback)->getRequests();
foreach ($requests as $key => $item) { $results[$key] = $item instanceof static ? $item->getPromise()->wait() : $item->wait(); }
ksort($results);
return $results; }
/** * Send the request to the given URL. * * @param string $method * @param string $url * @param array $options * @return \Illuminate\Http\Client\Response * * @throws \Exception */ public function send(string $method, string $url, array $options = []) { $url = ltrim(rtrim($this->baseUrl, '/').'/'.ltrim($url, '/'), '/');
if (isset($options[$this->bodyFormat])) { if ($this->bodyFormat === 'multipart') { $options[$this->bodyFormat] = $this->parseMultipartBodyFormat($options[$this->bodyFormat]); } elseif ($this->bodyFormat === 'body') { $options[$this->bodyFormat] = $this->pendingBody; }
if (is_array($options[$this->bodyFormat])) { $options[$this->bodyFormat] = array_merge( $options[$this->bodyFormat], $this->pendingFiles ); } } else { $options[$this->bodyFormat] = $this->pendingBody; }
[$this->pendingBody, $this->pendingFiles] = [null, []];
if ($this->async) { return $this->makePromise($method, $url, $options); }
return retry($this->tries ?? 1, function () use ($method, $url, $options) { try { return tap(new Response($this->sendRequest($method, $url, $options)), function ($response) { $this->populateResponse($response);
if ($this->tries > 1 && ! $response->successful()) { $response->throw(); } }); } catch (ConnectException $e) { throw new ConnectionException($e->getMessage(), 0, $e); } }, $this->retryDelay ?? 100); }
/** * Parse multi-part form data. * * @param array $data * @return array|array[] */ protected function parseMultipartBodyFormat(array $data) { return collect($data)->map(function ($value, $key) { return is_array($value) ? $value : ['name' => $key, 'contents' => $value]; })->values()->all(); }
/** * Send an asynchronous request to the given URL. * * @param string $method * @param string $url * @param array $options * @return \GuzzleHttp\Promise\PromiseInterface */ protected function makePromise(string $method, string $url, array $options = []) { return $this->promise = $this->sendRequest($method, $url, $options) ->then(function (MessageInterface $message) { return $this->populateResponse(new Response($message)); }) ->otherwise(function (TransferException $e) { return $e instanceof RequestException ? $this->populateResponse(new Response($e->getResponse())) : $e; }); }
/** * Send a request either synchronously or asynchronously. * * @param string $method * @param string $url * @param array $options * @return \Psr\Http\Message\MessageInterface|\GuzzleHttp\Promise\PromiseInterface * * @throws \Exception */ protected function sendRequest(string $method, string $url, array $options = []) { $clientMethod = $this->async ? 'requestAsync' : 'request';
$laravelData = $this->parseRequestData($method, $url, $options);
return $this->buildClient()->$clientMethod($method, $url, $this->mergeOptions([ 'laravel_data' => $laravelData, 'on_stats' => function ($transferStats) { $this->transferStats = $transferStats; }, ], $options)); }
/** * Get the request data as an array so that we can attach it to the request for convenient assertions. * * @param string $method * @param string $url * @param array $options * @return array */ protected function parseRequestData($method, $url, array $options) { $laravelData = $options[$this->bodyFormat] ?? $options['query'] ?? [];
$urlString = Str::of($url);
if (empty($laravelData) && $method === 'GET' && $urlString->contains('?')) { $laravelData = (string) $urlString->after('?'); }
if (is_string($laravelData)) { parse_str($laravelData, $parsedData);
$laravelData = is_array($parsedData) ? $parsedData : []; }
return $laravelData; }
/** * Populate the given response with additional data. * * @param \Illuminate\Http\Client\Response $response * @return \Illuminate\Http\Client\Response */ protected function populateResponse(Response $response) { $response->cookies = $this->cookies;
$response->transferStats = $this->transferStats;
return $response; }
/** * Build the Guzzle client. * * @return \GuzzleHttp\Client */ public function buildClient() { return $this->client = $this->client ?: new Client([ 'handler' => $this->buildHandlerStack(), 'cookies' => true, ]); }
/** * Build the before sending handler stack. * * @return \GuzzleHttp\HandlerStack */ public function buildHandlerStack() { return tap(HandlerStack::create(), function ($stack) { $stack->push($this->buildBeforeSendingHandler()); $stack->push($this->buildRecorderHandler()); $stack->push($this->buildStubHandler());
$this->middleware->each(function ($middleware) use ($stack) { $stack->push($middleware); }); }); }
/** * Build the before sending handler. * * @return \Closure */ public function buildBeforeSendingHandler() { return function ($handler) { return function ($request, $options) use ($handler) { return $handler($this->runBeforeSendingCallbacks($request, $options), $options); }; }; }
/** * Build the recorder handler. * * @return \Closure */ public function buildRecorderHandler() { return function ($handler) { return function ($request, $options) use ($handler) { $promise = $handler($this->runBeforeSendingCallbacks($request, $options), $options);
return $promise->then(function ($response) use ($request, $options) { optional($this->factory)->recordRequestResponsePair( (new Request($request))->withData($options['laravel_data']), new Response($response) );
return $response; }); }; }; }
/** * Build the stub handler. * * @return \Closure */ public function buildStubHandler() { return function ($handler) { return function ($request, $options) use ($handler) { $response = ($this->stubCallbacks ?? collect()) ->map ->__invoke((new Request($request))->withData($options['laravel_data']), $options) ->filter() ->first();
if (is_null($response)) { return $handler($request, $options); }
$response = is_array($response) ? Factory::response($response) : $response;
$sink = $options['sink'] ?? null;
if ($sink) { $response->then($this->sinkStubHandler($sink)); }
return $response; }; }; }
/** * Get the sink stub handler callback. * * @param string $sink * @return \Closure */ protected function sinkStubHandler($sink) { return function ($response) use ($sink) { $body = $response->getBody()->getContents();
if (is_string($sink)) { file_put_contents($sink, $body);
return; }
fwrite($sink, $body); rewind($sink); }; }
/** * Execute the "before sending" callbacks. * * @param \GuzzleHttp\Psr7\RequestInterface $request * @param array $options * @return \Closure */ public function runBeforeSendingCallbacks($request, array $options) { return tap($request, function ($request) use ($options) { $this->beforeSendingCallbacks->each->__invoke( (new Request($request))->withData($options['laravel_data']), $options ); }); }
/** * Merge the given options with the current request options. * * @param array $options * @return array */ public function mergeOptions(...$options) { return array_merge_recursive($this->options, ...$options); }
/** * Register a stub callable that will intercept requests and be able to return stub responses. * * @param callable $callback * @return $this */ public function stub($callback) { $this->stubCallbacks = collect($callback);
return $this; }
/** * Toggle asynchronicity in requests. * * @param bool $async * @return $this */ public function async(bool $async = true) { $this->async = $async;
return $this; }
/** * Retrieve the pending request promise. * * @return \GuzzleHttp\Promise\PromiseInterface|null */ public function getPromise() { return $this->promise; }
/** * Set the client instance. * * @param \GuzzleHttp\Client $client * @return $this */ public function setClient(Client $client) { $this->client = $client;
return $this; } }
|