Subversion-Projekte lars-tiefland.laravel_shop

Revision

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