Passed
Pull Request — develop (#1)
by nguereza
02:57
created

HttpClient::processBody()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

658
                    $body = $this->buildMultipartBody(/** @scrutinizer ignore-type */ $body, $boundary);
Loading history...
659
                }
660
            }
661
        }
662
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
663
    }
664
665
    /**
666
     * Parse boundary from request content type
667
     * @param string $contentType
668
     * @return string
669
     */
670
    protected function parseBoundaryFromContentType(string $contentType): string
671
    {
672
        $match = [];
673
        if (preg_match('/boundary="([^\"]+)"/is', $contentType, $match) > 0) {
674
            return $match[1];
675
        }
676
677
        throw new InvalidArgumentException(
678
            'The provided Content-Type header contained a "multipart/*" content type but did not '
679
                . 'define a boundary.'
680
        );
681
    }
682
683
    /**
684
     * Build the multipart body
685
     * @param array<string, mixed> $fields
686
     * @param string $boundary
687
     * @return string
688
     */
689
    protected function buildMultipartBody(array $fields, string $boundary): string
690
    {
691
        $body = '';
692
        foreach ($fields as $name => $value) {
693
            if (is_array($value)) {
694
                $data = $value['data'];
695
                $filename = $value['filename'] ?? false;
696
697
                $body .= sprintf("--%s\nContent-Disposition: form-data; name=\"%s\"", $boundary, $name);
698
                if ($filename !== false) {
699
                    $body .= sprintf(";filename=\"%s\"", $filename);
700
                }
701
702
                $body .= sprintf("\n\n%s\n", $data);
703
            } elseif (!empty($value)) {
704
                $body .= sprintf(
705
                    "--%s\nContent-Disposition: form-data; name=\"%s\"\n\n%s\n",
706
                    $boundary,
707
                    $name,
708
                    $value
709
                );
710
            }
711
        }
712
713
        $body .= sprintf('--%s--', $boundary);
714
715
        return $body;
716
    }
717
}
718