Passed
Push — develop ( 6fc9a5...b51090 )
by nguereza
01:44
created

HttpClient   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 656
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 155
c 1
b 0
f 0
dl 0
loc 656
rs 2.7199
wmc 71

42 Methods

Rating   Name   Duplication   Size   Complexity  
A verifySslCertificate() 0 5 1
A form() 0 5 1
A authorization() 0 3 1
A isVerifySslCertificate() 0 3 1
A cookie() 0 5 1
A parameters() 0 7 2
A buildUrl() 0 23 4
A processBody() 0 19 6
A headers() 0 7 2
A header() 0 8 2
A processHeaders() 0 11 4
A processCookies() 0 8 3
A json() 0 5 1
A getPassword() 0 3 1
A getBaseUrl() 0 3 1
A mutlipart() 0 5 1
A timeout() 0 5 1
A contentType() 0 13 3
A connect() 0 3 1
A delete() 0 3 1
A request() 0 14 1
A getUsername() 0 3 1
A get() 0 3 1
A head() 0 3 1
A parseBoundaryFromContentType() 0 10 2
A accept() 0 3 1
A put() 0 3 1
A buildMultipartBody() 0 27 5
A send() 0 39 5
A setBaseUrl() 0 4 1
A getHeaders() 0 3 1
A getCookies() 0 3 1
A basicAuthentication() 0 6 1
A getParameters() 0 3 1
A processUrl() 0 4 1
A cookies() 0 11 3
A parameter() 0 5 1
A trace() 0 3 1
A options() 0 3 1
A post() 0 3 1
A __construct() 0 3 1
A getTimeout() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like HttpClient often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HttpClient, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Platine HTTP
5
 *
6
 * Platine HTTP Message is the implementation of PSR 7
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine HTTP
11
 * Copyright (c) 2011 - 2017 rehyved.com
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file HttpClient.php
34
 *
35
 *  The Http Client class
36
 *
37
 *  @package    Platine\Http\Client
38
 *  @author Platine Developers team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Http\Client;
49
50
use InvalidArgumentException;
51
use Platine\Http\Client\Exception\HttpClientException;
52
use Platine\Stdlib\Helper\Json;
53
54
/**
55
 * @class HttpClient
56
 * @package Platine\Http\Client
57
 */
58
class HttpClient
59
{
60
    /**
61
     * The base URL
62
     * @var string
63
     */
64
    protected string $baseUrl;
65
66
    /**
67
     * The request headers
68
     * @var array<string, array<int, mixed>>
69
     */
70
    protected array $headers = [];
71
72
    /**
73
     * The request parameters
74
     * @var array<string, mixed>
75
     */
76
    protected array $parameters = [];
77
78
    /**
79
     * The request cookies
80
     * @var array<string, mixed>
81
     */
82
    protected array $cookies = [];
83
84
    /**
85
     * Indicating the number of seconds to use as a timeout for HTTP requests
86
     * @var int
87
     */
88
    protected int $timeout = 30;
89
90
    /**
91
     * Indicating if the validity of SSL certificates should be enforced in HTTP requests
92
     * @var bool
93
     */
94
    protected bool $verifySslCertificate = true;
95
96
    /**
97
     * The username to use for basic authentication
98
     * @var string
99
     */
100
    protected string $username = '';
101
102
    /**
103
     * The password to use for basic authentication
104
     * @var string
105
     */
106
    protected string $password = '';
107
108
    /**
109
     * Create new instance
110
     * @param string $baseUrl
111
     */
112
    public function __construct(string $baseUrl = '')
113
    {
114
        $this->baseUrl = $baseUrl;
115
    }
116
117
    /**
118
     * Set the base URL
119
     * @param string $baseUrl
120
     * @return $this
121
     */
122
    public function setBaseUrl(string $baseUrl): self
123
    {
124
        $this->baseUrl = $baseUrl;
125
        return $this;
126
    }
127
128
    /**
129
     * Return the base URL
130
     * @return string
131
     */
132
    public function getBaseUrl(): string
133
    {
134
        return $this->baseUrl;
135
    }
136
137
138
    /**
139
     * Add request header
140
     * @param string $name
141
     * @param mixed $value
142
     * @return $this
143
     */
144
    public function header(string $name, $value): self
145
    {
146
        if (array_key_exists($name, $this->headers) === false) {
147
            $this->headers[$name] = [];
148
        }
149
        $this->headers[$name][] = $value;
150
151
        return $this;
152
    }
153
154
    /**
155
     * Add multiple request headers
156
     * @param array<string, mixed> $headers
157
     * @return $this
158
     */
159
    public function headers(array $headers): self
160
    {
161
        foreach ($headers as $name => $value) {
162
            $this->header($name, $value);
163
        }
164
165
        return $this;
166
    }
167
168
    /**
169
     * Add request query parameter
170
     * @param string $name
171
     * @param mixed $value
172
     * @return $this
173
     */
174
    public function parameter(string $name, $value): self
175
    {
176
        $this->parameters[$name] = $value;
177
178
        return $this;
179
    }
180
181
182
    /**
183
     * Add multiple request parameter
184
     * @param array<string, mixed> $parameters
185
     * @return $this
186
     */
187
    public function parameters(array $parameters): self
188
    {
189
        foreach ($parameters as $name => $value) {
190
            $this->parameter($name, $value);
191
        }
192
193
        return $this;
194
    }
195
196
    /**
197
     * Add request cookie
198
     * @param string $name
199
     * @param mixed $value
200
     * @return $this
201
     */
202
    public function cookie(string $name, $value): self
203
    {
204
        $this->cookies[$name] = $value;
205
206
        return $this;
207
    }
208
209
    /**
210
     * Add multiple request cookie
211
     * @param array<string, mixed> $cookies
212
     * @return $this
213
     */
214
    public function cookies(?array $cookies = null): self
215
    {
216
        if ($cookies === null) {
217
            $cookies = $_COOKIE;
218
        }
219
220
        foreach ($cookies as $name => $value) {
221
            $this->cookie($name, $value);
222
        }
223
224
        return $this;
225
    }
226
227
    /**
228
     * Set the basic authentication to use on the request
229
     * @param string $usename
230
     * @param string $password
231
     * @return $this
232
     */
233
    public function basicAuthentication(string $usename, string $password = ''): self
234
    {
235
        $this->username = $usename;
236
        $this->password = $password;
237
238
        return $this;
239
    }
240
241
    /**
242
     * Set the request timeout
243
     * @param int $timeout
244
     * @return $this
245
     */
246
    public function timeout(int $timeout): self
247
    {
248
        $this->timeout = $timeout;
249
250
        return $this;
251
    }
252
253
    /**
254
     * Controls if the validity of SSL certificates should be verified.
255
     * WARNING: This should never be done in a production setup and should be used for debugging only.
256
     * @param bool $verifySslCertificate
257
     * @return self
258
     */
259
    public function verifySslCertificate(bool $verifySslCertificate): self
260
    {
261
        $this->verifySslCertificate = $verifySslCertificate;
262
263
        return $this;
264
    }
265
266
    /**
267
     * Set request content type
268
     * @param string $contentType
269
     * @return $this
270
     */
271
    public function contentType(string $contentType): self
272
    {
273
       // If this is a multipart request and boundary was not defined,
274
       // we define a boundary as this is required for multipart requests.
275
        if (stripos($contentType, 'multipart/') !== false) {
276
            if (stripos($contentType, 'boundary') === false) {
277
                $contentType .= sprintf('; boundary="%s"', uniqid((string) time()));
278
                // remove double semi-colon, except after scheme
279
                $contentType = preg_replace('/(.)(;{2,})/', '$1;', $contentType);
280
            }
281
        }
282
283
        return $this->header('Content-Type', $contentType);
284
    }
285
286
    /**
287
     * Set the request content type as JSON
288
     * @return $this
289
     */
290
    public function json(): self
291
    {
292
        $this->contentType('application/json');
293
294
        return $this;
295
    }
296
297
    /**
298
     * Set the request content type as form
299
     * @return $this
300
     */
301
    public function form(): self
302
    {
303
        $this->contentType('application/x-www-form-urlencoded');
304
305
        return $this;
306
    }
307
308
    /**
309
     * Set the request content type as multipart
310
     * @return $this
311
     */
312
    public function mutlipart(): self
313
    {
314
        $this->contentType('multipart/form-data');
315
316
        return $this;
317
    }
318
319
    /**
320
     * Set request accept content type
321
     * @param string $contentType
322
     * @return $this
323
     */
324
    public function accept(string $contentType): self
325
    {
326
        return $this->header('Accept', $contentType);
327
    }
328
329
    /**
330
     * Set request authorization header
331
     * @param string $scheme the scheme to use in the value of the Authorization header (e.g. Bearer)
332
     * @param string $value the value to set for the the Authorization header
333
     * @return $this
334
     */
335
    public function authorization(string $scheme, string $value): self
336
    {
337
        return $this->header('Authorization', sprintf('%s %s', $scheme, $value));
338
    }
339
340
    /**
341
     * Return the headers
342
     * @return array<string, array<int, mixed>>
343
     */
344
    public function getHeaders(): array
345
    {
346
        return $this->headers;
347
    }
348
349
    /**
350
     * Return the parameters
351
     * @return array<string, mixed>
352
     */
353
    public function getParameters(): array
354
    {
355
        return $this->parameters;
356
    }
357
358
    /**
359
     * Return the cookies
360
     * @return array<string, mixed>
361
     */
362
    public function getCookies(): array
363
    {
364
        return $this->cookies;
365
    }
366
367
    /**
368
     * Return the timeout
369
     * @return int
370
     */
371
    public function getTimeout(): int
372
    {
373
        return $this->timeout;
374
    }
375
376
    /**
377
     * Whether to verify SSL certificate
378
     * @return bool
379
     */
380
    public function isVerifySslCertificate(): bool
381
    {
382
        return $this->verifySslCertificate;
383
    }
384
385
    /**
386
     * Return the username for basic authentication
387
     * @return string
388
     */
389
    public function getUsername(): string
390
    {
391
        return $this->username;
392
    }
393
394
    /**
395
     * Return the password for basic authentication
396
     * @return string
397
     */
398
    public function getPassword(): string
399
    {
400
        return $this->password;
401
    }
402
403
    /**
404
     * Execute the request as a GET request to the specified path
405
     * @param string $path
406
     * @return HttpResponse
407
     */
408
    public function get(string $path = ''): HttpResponse
409
    {
410
        return $this->request($path, HttpMethod::GET);
411
    }
412
413
    /**
414
     * Execute the request as a POST request to the specified path
415
     * @param string $path
416
     * @param mixed|null $body the request body
417
     * @return HttpResponse
418
     */
419
    public function post(string $path = '', $body = null): HttpResponse
420
    {
421
        return $this->request($path, HttpMethod::POST, $body);
422
    }
423
424
    /**
425
     * Execute the request as a PUT request to the specified path
426
     * @param string $path
427
     * @param mixed|null $body the request body
428
     * @return HttpResponse
429
     */
430
    public function put(string $path = '', $body = null): HttpResponse
431
    {
432
        return $this->request($path, HttpMethod::PUT, $body);
433
    }
434
435
    /**
436
     * Execute the request as a DELETE request to the specified path
437
     * @param string $path
438
     * @param mixed|null $body the request body
439
     * @return HttpResponse
440
     */
441
    public function delete(string $path = '', $body = null): HttpResponse
442
    {
443
        return $this->request($path, HttpMethod::DELETE, $body);
444
    }
445
446
    /**
447
     * Execute the request as a HEAD request to the specified path
448
     * @param string $path
449
     * @param mixed|null $body the request body
450
     * @return HttpResponse
451
     */
452
    public function head(string $path = '', $body = null): HttpResponse
453
    {
454
        return $this->request($path, HttpMethod::HEAD, $body);
455
    }
456
457
    /**
458
     * Execute the request as a TRACE request to the specified path
459
     * @param string $path
460
     * @param mixed|null $body the request body
461
     * @return HttpResponse
462
     */
463
    public function trace(string $path = '', $body = null): HttpResponse
464
    {
465
        return $this->request($path, HttpMethod::TRACE, $body);
466
    }
467
468
    /**
469
     * Execute the request as a OPTIONS request to the specified path
470
     * @param string $path
471
     * @param mixed|null $body the request body
472
     * @return HttpResponse
473
     */
474
    public function options(string $path = '', $body = null): HttpResponse
475
    {
476
        return $this->request($path, HttpMethod::OPTIONS, $body);
477
    }
478
479
    /**
480
     * Execute the request as a CONNECT request to the specified path
481
     * @param string $path
482
     * @param mixed|null $body the request body
483
     * @return HttpResponse
484
     */
485
    public function connect(string $path = '', $body = null): HttpResponse
486
    {
487
        return $this->request($path, HttpMethod::CONNECT, $body);
488
    }
489
490
    /**
491
     * Construct the HTTP request and sends it using the provided method and request body
492
     * @param string $path
493
     * @param string $method
494
     * @param mixed|null $body
495
     * @return HttpResponse
496
     */
497
    public function request(string $path, string $method = HttpMethod::GET, $body = null): HttpResponse
498
    {
499
        $ch = curl_init();
500
501
        $this->processUrl($path, $ch);
502
503
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
504
505
        // Do body first as this might add additional headers
506
        $this->processBody($ch, $body);
507
        $this->processHeaders($ch);
508
        $this->processCookies($ch);
509
510
        return $this->send($ch);
511
    }
512
513
    /**
514
     * Send the request
515
     * @param mixed $ch the cURL handle
516
     * @return HttpResponse
517
     */
518
    protected function send($ch): HttpResponse
519
    {
520
        $responseHeaders = [];
521
        curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
522
            if (strpos($header, ':') !== false) {
523
                list($name, $value) = explode(':', $header);
524
                if (array_key_exists($name, $responseHeaders) === false) {
525
                    $responseHeaders[$name] = [];
526
                }
527
                $responseHeaders[$name][] = trim($value);
528
            }
529
530
            return strlen($header);
531
        });
532
533
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
534
        // Ensure we are coping with 300 (redirect) responses
535
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
536
        curl_setopt($ch, CURLOPT_HEADER, true);
537
538
        // Set request timeout
539
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
540
        // Set verification of SSL certificates
541
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifySslCertificate);
542
543
        if (!empty($this->username)) {
544
            curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
545
            curl_setopt($ch, CURLOPT_USERPWD, sprintf('%s:%s', $this->username, $this->password));
546
        }
547
548
        $response = curl_exec($ch);
549
        $requestInfo = curl_getinfo($ch);
550
        $error = curl_error($ch);
551
        $errorCode = curl_errno($ch);
552
        if (!empty($error)) {
553
            throw new HttpClientException($error, $errorCode);
554
        }
555
556
        return new HttpResponse($requestInfo, $responseHeaders, $response, $error);
557
    }
558
559
    /**
560
     * Process URL
561
     * @param string $path
562
     * @param mixed $ch the cURL handle
563
     * @return void
564
     */
565
    protected function processUrl(string $path, $ch): void
566
    {
567
        $url = $this->buildUrl($path);
568
        curl_setopt($ch, CURLOPT_URL, $url);
569
    }
570
571
    /**
572
     * Build the request full URL
573
     * @param string $path
574
     * @return string
575
     */
576
    protected function buildUrl(string $path): string
577
    {
578
        if (empty($this->baseUrl)) {
579
            throw new InvalidArgumentException('Base URL can not be empty or null');
580
        }
581
582
        $url = $this->baseUrl;
583
        if (!empty($path)) {
584
            $url .= $path;
585
        }
586
587
        if (count($this->parameters) > 0) {
588
            $url .= '?' . http_build_query($this->parameters);
589
        }
590
591
        // Clean url
592
        // remove double slashes, except after scheme
593
        $cleanUrl = (string) preg_replace('/([^:])(\/{2,})/', '$1/', $url);
594
        // convert arrays with indexes to arrays without
595
        // (i.e. parameter[0]=1 -> parameter[]=1)
596
        $finalUrl = (string) preg_replace('/%5B[0-9]+%5D/simU', '%5B%5D', $cleanUrl);
597
598
        return $finalUrl;
599
    }
600
601
    /**
602
     * Process the request headers
603
     * @param mixed $ch the cURL handle
604
     * @return void
605
     */
606
    protected function processHeaders($ch): void
607
    {
608
        $headers = [];
609
        foreach ($this->headers as $name => $values) {
610
            foreach ($values as $value) {
611
                $headers[] = sprintf('%s: %s', $name, $value);
612
            }
613
        }
614
615
        if (count($headers) > 0) {
616
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
617
        }
618
    }
619
620
    /**
621
     * Process the request cookies
622
     * @param mixed $ch the cURL handle
623
     * @return void
624
     */
625
    protected function processCookies($ch): void
626
    {
627
        $cookies = [];
628
        foreach ($this->cookies as $name => $value) {
629
            $cookies[] = sprintf('%s=%s', $name, $value);
630
        }
631
        if (count($cookies) > 0) {
632
            curl_setopt($ch, CURLOPT_COOKIE, implode(';', $cookies));
633
        }
634
    }
635
636
    /**
637
     * Process the request body
638
     * @param mixed $ch the cURL handle
639
     * @param array<mixed>|object|null $body the request body
640
     * @return void
641
     */
642
    protected function processBody($ch, $body = null): void
643
    {
644
        if ($body === null) {
645
            return;
646
        }
647
648
        if (isset($this->headers['Content-Type'][0])) {
649
            $contentType = $this->headers['Content-Type'][0];
650
            if (stripos($contentType, 'application/json') !== false) {
651
                $body = Json::encode($body);
652
            } elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
653
                $body = http_build_query($body);
654
            } elseif (stripos($contentType, 'multipart/form-data') !== false) {
655
                $boundary = $this->parseBoundaryFromContentType($contentType);
656
                $body = $this->buildMultipartBody(/** @var array<mixed> $body */ $body, $boundary);
657
            }
658
        }
659
660
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
661
    }
662
663
    /**
664
     * Parse boundary from request content type
665
     * @param string $contentType
666
     * @return string
667
     */
668
    protected function parseBoundaryFromContentType(string $contentType): string
669
    {
670
        $match = [];
671
        if (preg_match('/boundary="([^\"]+)"/is', $contentType, $match) > 0) {
672
            return $match[1];
673
        }
674
675
        throw new InvalidArgumentException(
676
            'The provided Content-Type header contained a "multipart/*" content type but did not '
677
                . 'define a boundary.'
678
        );
679
    }
680
681
    /**
682
     * Build the multipart body
683
     * @param array<string, mixed> $fields
684
     * @param string $boundary
685
     * @return string
686
     */
687
    protected function buildMultipartBody(array $fields, string $boundary): string
688
    {
689
        $body = '';
690
        foreach ($fields as $name => $value) {
691
            if (is_array($value)) {
692
                $data = $value['data'];
693
                $filename = $value['filename'] ?? false;
694
695
                $body .= sprintf("--%s\nContent-Disposition: form-data; name=\"%s\"", $boundary, $name);
696
                if ($filename !== false) {
697
                    $body .= sprintf(";filename=\"%s\"", $filename);
698
                }
699
700
                $body .= sprintf("\n\n%s\n", $data);
701
            } elseif (!empty($value)) {
702
                $body .= sprintf(
703
                    "--%s\nContent-Disposition: form-data; name=\"%s\"\n\n%s\n",
704
                    $boundary,
705
                    $name,
706
                    $value
707
                );
708
            }
709
        }
710
711
        $body .= sprintf('--%s--', $boundary);
712
713
        return $body;
714
    }
715
}
716