Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 150 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Http\Client;
4
 
5
use Exception;
6
use GuzzleHttp\Client;
7
use GuzzleHttp\Cookie\CookieJar;
8
use GuzzleHttp\Exception\ConnectException;
9
use GuzzleHttp\Exception\RequestException;
10
use GuzzleHttp\Exception\TransferException;
11
use GuzzleHttp\HandlerStack;
12
use Illuminate\Contracts\Support\Arrayable;
13
use Illuminate\Http\Client\Events\ConnectionFailed;
14
use Illuminate\Http\Client\Events\RequestSending;
15
use Illuminate\Http\Client\Events\ResponseReceived;
16
use Illuminate\Support\Arr;
17
use Illuminate\Support\Collection;
18
use Illuminate\Support\Str;
19
use Illuminate\Support\Traits\Conditionable;
20
use Illuminate\Support\Traits\Macroable;
21
use JsonSerializable;
22
use Psr\Http\Message\MessageInterface;
23
use Psr\Http\Message\RequestInterface;
24
use RuntimeException;
25
use Symfony\Component\VarDumper\VarDumper;
26
 
27
class PendingRequest
28
{
29
    use Conditionable, Macroable;
30
 
31
    /**
32
     * The factory instance.
33
     *
34
     * @var \Illuminate\Http\Client\Factory|null
35
     */
36
    protected $factory;
37
 
38
    /**
39
     * The Guzzle client instance.
40
     *
41
     * @var \GuzzleHttp\Client
42
     */
43
    protected $client;
44
 
45
    /**
46
     * The Guzzle HTTP handler.
47
     *
48
     * @var callable
49
     */
50
    protected $handler;
51
 
52
    /**
53
     * The base URL for the request.
54
     *
55
     * @var string
56
     */
57
    protected $baseUrl = '';
58
 
59
    /**
60
     * The request body format.
61
     *
62
     * @var string
63
     */
64
    protected $bodyFormat;
65
 
66
    /**
67
     * The raw body for the request.
68
     *
69
     * @var string
70
     */
71
    protected $pendingBody;
72
 
73
    /**
74
     * The pending files for the request.
75
     *
76
     * @var array
77
     */
78
    protected $pendingFiles = [];
79
 
80
    /**
81
     * The request cookies.
82
     *
83
     * @var array
84
     */
85
    protected $cookies;
86
 
87
    /**
88
     * The transfer stats for the request.
89
     *
90
     * \GuzzleHttp\TransferStats
91
     */
92
    protected $transferStats;
93
 
94
    /**
95
     * The request options.
96
     *
97
     * @var array
98
     */
99
    protected $options = [];
100
 
101
    /**
102
     * A callback to run when throwing if a server or client error occurs.
103
     *
104
     * @var \Closure
105
     */
106
    protected $throwCallback;
107
 
108
    /**
109
     * A callback to check if an exception should be thrown when a server or client error occurs.
110
     *
111
     * @var \Closure
112
     */
113
    protected $throwIfCallback;
114
 
115
    /**
116
     * The number of times to try the request.
117
     *
118
     * @var int
119
     */
120
    protected $tries = 1;
121
 
122
    /**
123
     * The number of milliseconds to wait between retries.
124
     *
125
     * @var int
126
     */
127
    protected $retryDelay = 100;
128
 
129
    /**
130
     * Whether to throw an exception when all retries fail.
131
     *
132
     * @var bool
133
     */
134
    protected $retryThrow = true;
135
 
136
    /**
137
     * The callback that will determine if the request should be retried.
138
     *
139
     * @var callable|null
140
     */
141
    protected $retryWhenCallback = null;
142
 
143
    /**
144
     * The callbacks that should execute before the request is sent.
145
     *
146
     * @var \Illuminate\Support\Collection
147
     */
148
    protected $beforeSendingCallbacks;
149
 
150
    /**
151
     * The stub callables that will handle requests.
152
     *
153
     * @var \Illuminate\Support\Collection|null
154
     */
155
    protected $stubCallbacks;
156
 
157
    /**
158
     * Indicates that an exception should be thrown if any request is not faked.
159
     *
160
     * @var bool
161
     */
162
    protected $preventStrayRequests = false;
163
 
164
    /**
165
     * The middleware callables added by users that will handle requests.
166
     *
167
     * @var \Illuminate\Support\Collection
168
     */
169
    protected $middleware;
170
 
171
    /**
172
     * Whether the requests should be asynchronous.
173
     *
174
     * @var bool
175
     */
176
    protected $async = false;
177
 
178
    /**
179
     * The pending request promise.
180
     *
181
     * @var \GuzzleHttp\Promise\PromiseInterface
182
     */
183
    protected $promise;
184
 
185
    /**
186
     * The sent request object, if a request has been made.
187
     *
188
     * @var \Illuminate\Http\Client\Request|null
189
     */
190
    protected $request;
191
 
192
    /**
193
     * The Guzzle request options that are mergable via array_merge_recursive.
194
     *
195
     * @var array
196
     */
197
    protected $mergableOptions = [
198
        'cookies',
199
        'form_params',
200
        'headers',
201
        'json',
202
        'multipart',
203
        'query',
204
    ];
205
 
206
    /**
207
     * Create a new HTTP Client instance.
208
     *
209
     * @param  \Illuminate\Http\Client\Factory|null  $factory
210
     * @return void
211
     */
212
    public function __construct(Factory $factory = null)
213
    {
214
        $this->factory = $factory;
215
        $this->middleware = new Collection;
216
 
217
        $this->asJson();
218
 
219
        $this->options = [
220
            'connect_timeout' => 10,
221
            'http_errors' => false,
222
            'timeout' => 30,
223
        ];
224
 
225
        $this->beforeSendingCallbacks = collect([function (Request $request, array $options, PendingRequest $pendingRequest) {
226
            $pendingRequest->request = $request;
227
            $pendingRequest->cookies = $options['cookies'];
228
 
229
            $pendingRequest->dispatchRequestSendingEvent();
230
        }]);
231
    }
232
 
233
    /**
234
     * Set the base URL for the pending request.
235
     *
236
     * @param  string  $url
237
     * @return $this
238
     */
239
    public function baseUrl(string $url)
240
    {
241
        $this->baseUrl = $url;
242
 
243
        return $this;
244
    }
245
 
246
    /**
247
     * Attach a raw body to the request.
248
     *
249
     * @param  string  $content
250
     * @param  string  $contentType
251
     * @return $this
252
     */
253
    public function withBody($content, $contentType)
254
    {
255
        $this->bodyFormat('body');
256
 
257
        $this->pendingBody = $content;
258
 
259
        $this->contentType($contentType);
260
 
261
        return $this;
262
    }
263
 
264
    /**
265
     * Indicate the request contains JSON.
266
     *
267
     * @return $this
268
     */
269
    public function asJson()
270
    {
271
        return $this->bodyFormat('json')->contentType('application/json');
272
    }
273
 
274
    /**
275
     * Indicate the request contains form parameters.
276
     *
277
     * @return $this
278
     */
279
    public function asForm()
280
    {
281
        return $this->bodyFormat('form_params')->contentType('application/x-www-form-urlencoded');
282
    }
283
 
284
    /**
285
     * Attach a file to the request.
286
     *
287
     * @param  string|array  $name
288
     * @param  string|resource  $contents
289
     * @param  string|null  $filename
290
     * @param  array  $headers
291
     * @return $this
292
     */
293
    public function attach($name, $contents = '', $filename = null, array $headers = [])
294
    {
295
        if (is_array($name)) {
296
            foreach ($name as $file) {
297
                $this->attach(...$file);
298
            }
299
 
300
            return $this;
301
        }
302
 
303
        $this->asMultipart();
304
 
305
        $this->pendingFiles[] = array_filter([
306
            'name' => $name,
307
            'contents' => $contents,
308
            'headers' => $headers,
309
            'filename' => $filename,
310
        ]);
311
 
312
        return $this;
313
    }
314
 
315
    /**
316
     * Indicate the request is a multi-part form request.
317
     *
318
     * @return $this
319
     */
320
    public function asMultipart()
321
    {
322
        return $this->bodyFormat('multipart');
323
    }
324
 
325
    /**
326
     * Specify the body format of the request.
327
     *
328
     * @param  string  $format
329
     * @return $this
330
     */
331
    public function bodyFormat(string $format)
332
    {
333
        return tap($this, function () use ($format) {
334
            $this->bodyFormat = $format;
335
        });
336
    }
337
 
338
    /**
339
     * Specify the request's content type.
340
     *
341
     * @param  string  $contentType
342
     * @return $this
343
     */
344
    public function contentType(string $contentType)
345
    {
346
        return $this->withHeaders(['Content-Type' => $contentType]);
347
    }
348
 
349
    /**
350
     * Indicate that JSON should be returned by the server.
351
     *
352
     * @return $this
353
     */
354
    public function acceptJson()
355
    {
356
        return $this->accept('application/json');
357
    }
358
 
359
    /**
360
     * Indicate the type of content that should be returned by the server.
361
     *
362
     * @param  string  $contentType
363
     * @return $this
364
     */
365
    public function accept($contentType)
366
    {
367
        return $this->withHeaders(['Accept' => $contentType]);
368
    }
369
 
370
    /**
371
     * Add the given headers to the request.
372
     *
373
     * @param  array  $headers
374
     * @return $this
375
     */
376
    public function withHeaders(array $headers)
377
    {
378
        return tap($this, function () use ($headers) {
379
            $this->options = array_merge_recursive($this->options, [
380
                'headers' => $headers,
381
            ]);
382
        });
383
    }
384
 
385
    /**
386
     * Specify the basic authentication username and password for the request.
387
     *
388
     * @param  string  $username
389
     * @param  string  $password
390
     * @return $this
391
     */
392
    public function withBasicAuth(string $username, string $password)
393
    {
394
        return tap($this, function () use ($username, $password) {
395
            $this->options['auth'] = [$username, $password];
396
        });
397
    }
398
 
399
    /**
400
     * Specify the digest authentication username and password for the request.
401
     *
402
     * @param  string  $username
403
     * @param  string  $password
404
     * @return $this
405
     */
406
    public function withDigestAuth($username, $password)
407
    {
408
        return tap($this, function () use ($username, $password) {
409
            $this->options['auth'] = [$username, $password, 'digest'];
410
        });
411
    }
412
 
413
    /**
414
     * Specify an authorization token for the request.
415
     *
416
     * @param  string  $token
417
     * @param  string  $type
418
     * @return $this
419
     */
420
    public function withToken($token, $type = 'Bearer')
421
    {
422
        return tap($this, function () use ($token, $type) {
423
            $this->options['headers']['Authorization'] = trim($type.' '.$token);
424
        });
425
    }
426
 
427
    /**
428
     * Specify the user agent for the request.
429
     *
430
     * @param  string  $userAgent
431
     * @return $this
432
     */
433
    public function withUserAgent($userAgent)
434
    {
435
        return tap($this, function () use ($userAgent) {
436
            $this->options['headers']['User-Agent'] = trim($userAgent);
437
        });
438
    }
439
 
440
    /**
441
     * Specify the cookies that should be included with the request.
442
     *
443
     * @param  array  $cookies
444
     * @param  string  $domain
445
     * @return $this
446
     */
447
    public function withCookies(array $cookies, string $domain)
448
    {
449
        return tap($this, function () use ($cookies, $domain) {
450
            $this->options = array_merge_recursive($this->options, [
451
                'cookies' => CookieJar::fromArray($cookies, $domain),
452
            ]);
453
        });
454
    }
455
 
456
    /**
457
     * Specify the maximum number of redirects to allow.
458
     *
459
     * @param  int  $max
460
     * @return $this
461
     */
462
    public function maxRedirects(int $max)
463
    {
464
        return tap($this, function () use ($max) {
465
            $this->options['allow_redirects']['max'] = $max;
466
        });
467
    }
468
 
469
    /**
470
     * Indicate that redirects should not be followed.
471
     *
472
     * @return $this
473
     */
474
    public function withoutRedirecting()
475
    {
476
        return tap($this, function () {
477
            $this->options['allow_redirects'] = false;
478
        });
479
    }
480
 
481
    /**
482
     * Indicate that TLS certificates should not be verified.
483
     *
484
     * @return $this
485
     */
486
    public function withoutVerifying()
487
    {
488
        return tap($this, function () {
489
            $this->options['verify'] = false;
490
        });
491
    }
492
 
493
    /**
494
     * Specify the path where the body of the response should be stored.
495
     *
496
     * @param  string|resource  $to
497
     * @return $this
498
     */
499
    public function sink($to)
500
    {
501
        return tap($this, function () use ($to) {
502
            $this->options['sink'] = $to;
503
        });
504
    }
505
 
506
    /**
507
     * Specify the timeout (in seconds) for the request.
508
     *
509
     * @param  int  $seconds
510
     * @return $this
511
     */
512
    public function timeout(int $seconds)
513
    {
514
        return tap($this, function () use ($seconds) {
515
            $this->options['timeout'] = $seconds;
516
        });
517
    }
518
 
519
    /**
520
     * Specify the connect timeout (in seconds) for the request.
521
     *
522
     * @param  int  $seconds
523
     * @return $this
524
     */
525
    public function connectTimeout(int $seconds)
526
    {
527
        return tap($this, function () use ($seconds) {
528
            $this->options['connect_timeout'] = $seconds;
529
        });
530
    }
531
 
532
    /**
533
     * Specify the number of times the request should be attempted.
534
     *
535
     * @param  int  $times
536
     * @param  int  $sleepMilliseconds
537
     * @param  callable|null  $when
538
     * @param  bool  $throw
539
     * @return $this
540
     */
541
    public function retry(int $times, int $sleepMilliseconds = 0, ?callable $when = null, bool $throw = true)
542
    {
543
        $this->tries = $times;
544
        $this->retryDelay = $sleepMilliseconds;
545
        $this->retryThrow = $throw;
546
        $this->retryWhenCallback = $when;
547
 
548
        return $this;
549
    }
550
 
551
    /**
552
     * Replace the specified options on the request.
553
     *
554
     * @param  array  $options
555
     * @return $this
556
     */
557
    public function withOptions(array $options)
558
    {
559
        return tap($this, function () use ($options) {
560
            $this->options = array_replace_recursive(
561
                array_merge_recursive($this->options, Arr::only($options, $this->mergableOptions)),
562
                $options
563
            );
564
        });
565
    }
566
 
567
    /**
568
     * Add new middleware the client handler stack.
569
     *
570
     * @param  callable  $middleware
571
     * @return $this
572
     */
573
    public function withMiddleware(callable $middleware)
574
    {
575
        $this->middleware->push($middleware);
576
 
577
        return $this;
578
    }
579
 
580
    /**
581
     * Add a new "before sending" callback to the request.
582
     *
583
     * @param  callable  $callback
584
     * @return $this
585
     */
586
    public function beforeSending($callback)
587
    {
588
        return tap($this, function () use ($callback) {
589
            $this->beforeSendingCallbacks[] = $callback;
590
        });
591
    }
592
 
593
    /**
594
     * Throw an exception if a server or client error occurs.
595
     *
596
     * @param  callable|null  $callback
597
     * @return $this
598
     */
599
    public function throw(callable $callback = null)
600
    {
601
        $this->throwCallback = $callback ?: fn () => null;
602
 
603
        return $this;
604
    }
605
 
606
    /**
607
     * Throw an exception if a server or client error occurred and the given condition evaluates to true.
608
     *
609
     * @param  callable|bool  $condition
610
     * @param  callable|null  $throwCallback
611
     * @return $this
612
     */
613
    public function throwIf($condition)
614
    {
615
        if (is_callable($condition)) {
616
            $this->throwIfCallback = $condition;
617
        }
618
 
619
        return $condition ? $this->throw(func_get_args()[1] ?? null) : $this;
620
    }
621
 
622
    /**
623
     * Throw an exception if a server or client error occurred and the given condition evaluates to false.
624
     *
625
     * @param  bool  $condition
626
     * @return $this
627
     */
628
    public function throwUnless($condition)
629
    {
630
        return $this->throwIf(! $condition);
631
    }
632
 
633
    /**
634
     * Dump the request before sending.
635
     *
636
     * @return $this
637
     */
638
    public function dump()
639
    {
640
        $values = func_get_args();
641
 
642
        return $this->beforeSending(function (Request $request, array $options) use ($values) {
643
            foreach (array_merge($values, [$request, $options]) as $value) {
644
                VarDumper::dump($value);
645
            }
646
        });
647
    }
648
 
649
    /**
650
     * Dump the request before sending and end the script.
651
     *
652
     * @return $this
653
     */
654
    public function dd()
655
    {
656
        $values = func_get_args();
657
 
658
        return $this->beforeSending(function (Request $request, array $options) use ($values) {
659
            foreach (array_merge($values, [$request, $options]) as $value) {
660
                VarDumper::dump($value);
661
            }
662
 
663
            exit(1);
664
        });
665
    }
666
 
667
    /**
668
     * Issue a GET request to the given URL.
669
     *
670
     * @param  string  $url
671
     * @param  array|string|null  $query
672
     * @return \Illuminate\Http\Client\Response
673
     */
674
    public function get(string $url, $query = null)
675
    {
676
        return $this->send('GET', $url, func_num_args() === 1 ? [] : [
677
            'query' => $query,
678
        ]);
679
    }
680
 
681
    /**
682
     * Issue a HEAD request to the given URL.
683
     *
684
     * @param  string  $url
685
     * @param  array|string|null  $query
686
     * @return \Illuminate\Http\Client\Response
687
     */
688
    public function head(string $url, $query = null)
689
    {
690
        return $this->send('HEAD', $url, func_num_args() === 1 ? [] : [
691
            'query' => $query,
692
        ]);
693
    }
694
 
695
    /**
696
     * Issue a POST request to the given URL.
697
     *
698
     * @param  string  $url
699
     * @param  array  $data
700
     * @return \Illuminate\Http\Client\Response
701
     */
702
    public function post(string $url, $data = [])
703
    {
704
        return $this->send('POST', $url, [
705
            $this->bodyFormat => $data,
706
        ]);
707
    }
708
 
709
    /**
710
     * Issue a PATCH request to the given URL.
711
     *
712
     * @param  string  $url
713
     * @param  array  $data
714
     * @return \Illuminate\Http\Client\Response
715
     */
716
    public function patch($url, $data = [])
717
    {
718
        return $this->send('PATCH', $url, [
719
            $this->bodyFormat => $data,
720
        ]);
721
    }
722
 
723
    /**
724
     * Issue a PUT request to the given URL.
725
     *
726
     * @param  string  $url
727
     * @param  array  $data
728
     * @return \Illuminate\Http\Client\Response
729
     */
730
    public function put($url, $data = [])
731
    {
732
        return $this->send('PUT', $url, [
733
            $this->bodyFormat => $data,
734
        ]);
735
    }
736
 
737
    /**
738
     * Issue a DELETE request to the given URL.
739
     *
740
     * @param  string  $url
741
     * @param  array  $data
742
     * @return \Illuminate\Http\Client\Response
743
     */
744
    public function delete($url, $data = [])
745
    {
746
        return $this->send('DELETE', $url, empty($data) ? [] : [
747
            $this->bodyFormat => $data,
748
        ]);
749
    }
750
 
751
    /**
752
     * Send a pool of asynchronous requests concurrently.
753
     *
754
     * @param  callable  $callback
755
     * @return array
756
     */
757
    public function pool(callable $callback)
758
    {
759
        $results = [];
760
 
761
        $requests = tap(new Pool($this->factory), $callback)->getRequests();
762
 
763
        foreach ($requests as $key => $item) {
764
            $results[$key] = $item instanceof static ? $item->getPromise()->wait() : $item->wait();
765
        }
766
 
767
        return $results;
768
    }
769
 
770
    /**
771
     * Send the request to the given URL.
772
     *
773
     * @param  string  $method
774
     * @param  string  $url
775
     * @param  array  $options
776
     * @return \Illuminate\Http\Client\Response
777
     *
778
     * @throws \Exception
779
     */
780
    public function send(string $method, string $url, array $options = [])
781
    {
782
        if (! Str::startsWith($url, ['http://', 'https://'])) {
783
            $url = ltrim(rtrim($this->baseUrl, '/').'/'.ltrim($url, '/'), '/');
784
        }
785
 
786
        $options = $this->parseHttpOptions($options);
787
 
788
        [$this->pendingBody, $this->pendingFiles] = [null, []];
789
 
790
        if ($this->async) {
791
            return $this->makePromise($method, $url, $options);
792
        }
793
 
794
        $shouldRetry = null;
795
 
796
        return retry($this->tries ?? 1, function ($attempt) use ($method, $url, $options, &$shouldRetry) {
797
            try {
798
                return tap(new Response($this->sendRequest($method, $url, $options)), function ($response) use ($attempt, &$shouldRetry) {
799
                    $this->populateResponse($response);
800
 
801
                    $this->dispatchResponseReceivedEvent($response);
802
 
803
                    if (! $response->successful()) {
804
                        try {
805
                            $shouldRetry = $this->retryWhenCallback ? call_user_func($this->retryWhenCallback, $response->toException(), $this) : true;
806
                        } catch (Exception $exception) {
807
                            $shouldRetry = false;
808
 
809
                            throw $exception;
810
                        }
811
 
812
                        if ($this->throwCallback &&
813
                            ($this->throwIfCallback === null ||
814
                             call_user_func($this->throwIfCallback, $response))) {
815
                            $response->throw($this->throwCallback);
816
                        }
817
 
818
                        if ($attempt < $this->tries && $shouldRetry) {
819
                            $response->throw();
820
                        }
821
 
822
                        if ($this->tries > 1 && $this->retryThrow) {
823
                            $response->throw();
824
                        }
825
                    }
826
                });
827
            } catch (ConnectException $e) {
828
                $this->dispatchConnectionFailedEvent();
829
 
830
                throw new ConnectionException($e->getMessage(), 0, $e);
831
            }
832
        }, $this->retryDelay ?? 100, function ($exception) use (&$shouldRetry) {
833
            $result = $shouldRetry ?? ($this->retryWhenCallback ? call_user_func($this->retryWhenCallback, $exception, $this) : true);
834
 
835
            $shouldRetry = null;
836
 
837
            return $result;
838
        });
839
    }
840
 
841
    /**
842
     * Parse the given HTTP options and set the appropriate additional options.
843
     *
844
     * @param  array  $options
845
     * @return array
846
     */
847
    protected function parseHttpOptions(array $options)
848
    {
849
        if (isset($options[$this->bodyFormat])) {
850
            if ($this->bodyFormat === 'multipart') {
851
                $options[$this->bodyFormat] = $this->parseMultipartBodyFormat($options[$this->bodyFormat]);
852
            } elseif ($this->bodyFormat === 'body') {
853
                $options[$this->bodyFormat] = $this->pendingBody;
854
            }
855
 
856
            if (is_array($options[$this->bodyFormat])) {
857
                $options[$this->bodyFormat] = array_merge(
858
                    $options[$this->bodyFormat], $this->pendingFiles
859
                );
860
            }
861
        } else {
862
            $options[$this->bodyFormat] = $this->pendingBody;
863
        }
864
 
865
        return collect($options)->map(function ($value, $key) {
866
            if ($key === 'json' && $value instanceof JsonSerializable) {
867
                return $value;
868
            }
869
 
870
            return $value instanceof Arrayable ? $value->toArray() : $value;
871
        })->all();
872
    }
873
 
874
    /**
875
     * Parse multi-part form data.
876
     *
877
     * @param  array  $data
878
     * @return array|array[]
879
     */
880
    protected function parseMultipartBodyFormat(array $data)
881
    {
882
        return collect($data)->map(function ($value, $key) {
883
            return is_array($value) ? $value : ['name' => $key, 'contents' => $value];
884
        })->values()->all();
885
    }
886
 
887
    /**
888
     * Send an asynchronous request to the given URL.
889
     *
890
     * @param  string  $method
891
     * @param  string  $url
892
     * @param  array  $options
893
     * @return \GuzzleHttp\Promise\PromiseInterface
894
     */
895
    protected function makePromise(string $method, string $url, array $options = [])
896
    {
897
        return $this->promise = $this->sendRequest($method, $url, $options)
898
            ->then(function (MessageInterface $message) {
899
                return tap(new Response($message), function ($response) {
900
                    $this->populateResponse($response);
901
                    $this->dispatchResponseReceivedEvent($response);
902
                });
903
            })
904
            ->otherwise(function (TransferException $e) {
905
                return $e instanceof RequestException && $e->hasResponse() ? $this->populateResponse(new Response($e->getResponse())) : $e;
906
            });
907
    }
908
 
909
    /**
910
     * Send a request either synchronously or asynchronously.
911
     *
912
     * @param  string  $method
913
     * @param  string  $url
914
     * @param  array  $options
915
     * @return \Psr\Http\Message\MessageInterface|\GuzzleHttp\Promise\PromiseInterface
916
     *
917
     * @throws \Exception
918
     */
919
    protected function sendRequest(string $method, string $url, array $options = [])
920
    {
921
        $clientMethod = $this->async ? 'requestAsync' : 'request';
922
 
923
        $laravelData = $this->parseRequestData($method, $url, $options);
924
 
925
        return $this->buildClient()->$clientMethod($method, $url, $this->mergeOptions([
926
            'laravel_data' => $laravelData,
927
            'on_stats' => function ($transferStats) {
928
                $this->transferStats = $transferStats;
929
            },
930
        ], $options));
931
    }
932
 
933
    /**
934
     * Get the request data as an array so that we can attach it to the request for convenient assertions.
935
     *
936
     * @param  string  $method
937
     * @param  string  $url
938
     * @param  array  $options
939
     * @return array
940
     */
941
    protected function parseRequestData($method, $url, array $options)
942
    {
943
        if ($this->bodyFormat === 'body') {
944
            return [];
945
        }
946
 
947
        $laravelData = $options[$this->bodyFormat] ?? $options['query'] ?? [];
948
 
949
        $urlString = Str::of($url);
950
 
951
        if (empty($laravelData) && $method === 'GET' && $urlString->contains('?')) {
952
            $laravelData = (string) $urlString->after('?');
953
        }
954
 
955
        if (is_string($laravelData)) {
956
            parse_str($laravelData, $parsedData);
957
 
958
            $laravelData = is_array($parsedData) ? $parsedData : [];
959
        }
960
 
961
        if ($laravelData instanceof JsonSerializable) {
962
            $laravelData = $laravelData->jsonSerialize();
963
        }
964
 
965
        return is_array($laravelData) ? $laravelData : [];
966
    }
967
 
968
    /**
969
     * Populate the given response with additional data.
970
     *
971
     * @param  \Illuminate\Http\Client\Response  $response
972
     * @return \Illuminate\Http\Client\Response
973
     */
974
    protected function populateResponse(Response $response)
975
    {
976
        $response->cookies = $this->cookies;
977
 
978
        $response->transferStats = $this->transferStats;
979
 
980
        return $response;
981
    }
982
 
983
    /**
984
     * Build the Guzzle client.
985
     *
986
     * @return \GuzzleHttp\Client
987
     */
988
    public function buildClient()
989
    {
990
        return $this->client ?? $this->createClient($this->buildHandlerStack());
991
    }
992
 
993
    /**
994
     * Determine if a reusable client is required.
995
     *
996
     * @return bool
997
     */
998
    protected function requestsReusableClient()
999
    {
1000
        return ! is_null($this->client) || $this->async;
1001
    }
1002
 
1003
    /**
1004
     * Retrieve a reusable Guzzle client.
1005
     *
1006
     * @return \GuzzleHttp\Client
1007
     */
1008
    protected function getReusableClient()
1009
    {
1010
        return $this->client = $this->client ?: $this->createClient($this->buildHandlerStack());
1011
    }
1012
 
1013
    /**
1014
     * Create new Guzzle client.
1015
     *
1016
     * @param  \GuzzleHttp\HandlerStack  $handlerStack
1017
     * @return \GuzzleHttp\Client
1018
     */
1019
    public function createClient($handlerStack)
1020
    {
1021
        return new Client([
1022
            'handler' => $handlerStack,
1023
            'cookies' => true,
1024
        ]);
1025
    }
1026
 
1027
    /**
1028
     * Build the Guzzle client handler stack.
1029
     *
1030
     * @return \GuzzleHttp\HandlerStack
1031
     */
1032
    public function buildHandlerStack()
1033
    {
1034
        return $this->pushHandlers(HandlerStack::create($this->handler));
1035
    }
1036
 
1037
    /**
1038
     * Add the necessary handlers to the given handler stack.
1039
     *
1040
     * @param  \GuzzleHttp\HandlerStack  $handlerStack
1041
     * @return \GuzzleHttp\HandlerStack
1042
     */
1043
    public function pushHandlers($handlerStack)
1044
    {
1045
        return tap($handlerStack, function ($stack) {
1046
            $stack->push($this->buildBeforeSendingHandler());
1047
            $stack->push($this->buildRecorderHandler());
1048
 
1049
            $this->middleware->each(function ($middleware) use ($stack) {
1050
                $stack->push($middleware);
1051
            });
1052
 
1053
            $stack->push($this->buildStubHandler());
1054
        });
1055
    }
1056
 
1057
    /**
1058
     * Build the before sending handler.
1059
     *
1060
     * @return \Closure
1061
     */
1062
    public function buildBeforeSendingHandler()
1063
    {
1064
        return function ($handler) {
1065
            return function ($request, $options) use ($handler) {
1066
                return $handler($this->runBeforeSendingCallbacks($request, $options), $options);
1067
            };
1068
        };
1069
    }
1070
 
1071
    /**
1072
     * Build the recorder handler.
1073
     *
1074
     * @return \Closure
1075
     */
1076
    public function buildRecorderHandler()
1077
    {
1078
        return function ($handler) {
1079
            return function ($request, $options) use ($handler) {
1080
                $promise = $handler($request, $options);
1081
 
1082
                return $promise->then(function ($response) use ($request, $options) {
1083
                    $this->factory?->recordRequestResponsePair(
1084
                        (new Request($request))->withData($options['laravel_data']),
1085
                        new Response($response)
1086
                    );
1087
 
1088
                    return $response;
1089
                });
1090
            };
1091
        };
1092
    }
1093
 
1094
    /**
1095
     * Build the stub handler.
1096
     *
1097
     * @return \Closure
1098
     */
1099
    public function buildStubHandler()
1100
    {
1101
        return function ($handler) {
1102
            return function ($request, $options) use ($handler) {
1103
                $response = ($this->stubCallbacks ?? collect())
1104
                     ->map
1105
                     ->__invoke((new Request($request))->withData($options['laravel_data']), $options)
1106
                     ->filter()
1107
                     ->first();
1108
 
1109
                if (is_null($response)) {
1110
                    if ($this->preventStrayRequests) {
1111
                        throw new RuntimeException('Attempted request to ['.(string) $request->getUri().'] without a matching fake.');
1112
                    }
1113
 
1114
                    return $handler($request, $options);
1115
                }
1116
 
1117
                $response = is_array($response) ? Factory::response($response) : $response;
1118
 
1119
                $sink = $options['sink'] ?? null;
1120
 
1121
                if ($sink) {
1122
                    $response->then($this->sinkStubHandler($sink));
1123
                }
1124
 
1125
                return $response;
1126
            };
1127
        };
1128
    }
1129
 
1130
    /**
1131
     * Get the sink stub handler callback.
1132
     *
1133
     * @param  string  $sink
1134
     * @return \Closure
1135
     */
1136
    protected function sinkStubHandler($sink)
1137
    {
1138
        return function ($response) use ($sink) {
1139
            $body = $response->getBody()->getContents();
1140
 
1141
            if (is_string($sink)) {
1142
                file_put_contents($sink, $body);
1143
 
1144
                return;
1145
            }
1146
 
1147
            fwrite($sink, $body);
1148
            rewind($sink);
1149
        };
1150
    }
1151
 
1152
    /**
1153
     * Execute the "before sending" callbacks.
1154
     *
1155
     * @param  \GuzzleHttp\Psr7\RequestInterface  $request
1156
     * @param  array  $options
1157
     * @return \GuzzleHttp\Psr7\RequestInterface
1158
     */
1159
    public function runBeforeSendingCallbacks($request, array $options)
1160
    {
1161
        return tap($request, function (&$request) use ($options) {
1162
            $this->beforeSendingCallbacks->each(function ($callback) use (&$request, $options) {
1163
                $callbackResult = call_user_func(
1164
                    $callback, (new Request($request))->withData($options['laravel_data']), $options, $this
1165
                );
1166
 
1167
                if ($callbackResult instanceof RequestInterface) {
1168
                    $request = $callbackResult;
1169
                } elseif ($callbackResult instanceof Request) {
1170
                    $request = $callbackResult->toPsrRequest();
1171
                }
1172
            });
1173
        });
1174
    }
1175
 
1176
    /**
1177
     * Replace the given options with the current request options.
1178
     *
1179
     * @param  array  $options
1180
     * @return array
1181
     */
1182
    public function mergeOptions(...$options)
1183
    {
1184
        return array_replace_recursive(
1185
            array_merge_recursive($this->options, Arr::only($options, $this->mergableOptions)),
1186
            ...$options
1187
        );
1188
    }
1189
 
1190
    /**
1191
     * Register a stub callable that will intercept requests and be able to return stub responses.
1192
     *
1193
     * @param  callable  $callback
1194
     * @return $this
1195
     */
1196
    public function stub($callback)
1197
    {
1198
        $this->stubCallbacks = collect($callback);
1199
 
1200
        return $this;
1201
    }
1202
 
1203
    /**
1204
     * Indicate that an exception should be thrown if any request is not faked.
1205
     *
1206
     * @param  bool  $prevent
1207
     * @return $this
1208
     */
1209
    public function preventStrayRequests($prevent = true)
1210
    {
1211
        $this->preventStrayRequests = $prevent;
1212
 
1213
        return $this;
1214
    }
1215
 
1216
    /**
1217
     * Toggle asynchronicity in requests.
1218
     *
1219
     * @param  bool  $async
1220
     * @return $this
1221
     */
1222
    public function async(bool $async = true)
1223
    {
1224
        $this->async = $async;
1225
 
1226
        return $this;
1227
    }
1228
 
1229
    /**
1230
     * Retrieve the pending request promise.
1231
     *
1232
     * @return \GuzzleHttp\Promise\PromiseInterface|null
1233
     */
1234
    public function getPromise()
1235
    {
1236
        return $this->promise;
1237
    }
1238
 
1239
    /**
1240
     * Dispatch the RequestSending event if a dispatcher is available.
1241
     *
1242
     * @return void
1243
     */
1244
    protected function dispatchRequestSendingEvent()
1245
    {
1246
        if ($dispatcher = $this->factory?->getDispatcher()) {
1247
            $dispatcher->dispatch(new RequestSending($this->request));
1248
        }
1249
    }
1250
 
1251
    /**
1252
     * Dispatch the ResponseReceived event if a dispatcher is available.
1253
     *
1254
     * @param  \Illuminate\Http\Client\Response  $response
1255
     * @return void
1256
     */
1257
    protected function dispatchResponseReceivedEvent(Response $response)
1258
    {
1259
        if (! ($dispatcher = $this->factory?->getDispatcher()) ||
1260
            ! $this->request) {
1261
            return;
1262
        }
1263
 
1264
        $dispatcher->dispatch(new ResponseReceived($this->request, $response));
1265
    }
1266
 
1267
    /**
1268
     * Dispatch the ConnectionFailed event if a dispatcher is available.
1269
     *
1270
     * @return void
1271
     */
1272
    protected function dispatchConnectionFailedEvent()
1273
    {
1274
        if ($dispatcher = $this->factory?->getDispatcher()) {
1275
            $dispatcher->dispatch(new ConnectionFailed($this->request));
1276
        }
1277
    }
1278
 
1279
    /**
1280
     * Set the client instance.
1281
     *
1282
     * @param  \GuzzleHttp\Client  $client
1283
     * @return $this
1284
     */
1285
    public function setClient(Client $client)
1286
    {
1287
        $this->client = $client;
1288
 
1289
        return $this;
1290
    }
1291
 
1292
    /**
1293
     * Create a new client instance using the given handler.
1294
     *
1295
     * @param  callable  $handler
1296
     * @return $this
1297
     */
1298
    public function setHandler($handler)
1299
    {
1300
        $this->handler = $handler;
1301
 
1302
        return $this;
1303
    }
1304
 
1305
    /**
1306
     * Get the pending request options.
1307
     *
1308
     * @return array
1309
     */
1310
    public function getOptions()
1311
    {
1312
        return $this->options;
1313
    }
1314
}