Passed
Push — master ( d16243...311ea7 )
by Shahrad
01:34
created

Client   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 407
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 158
c 4
b 0
f 0
dl 0
loc 407
rs 8.5599
wmc 48

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setTempPath() 0 6 2
A __construct() 0 3 1
A setHasSelfSignedCertificate() 0 3 1
B bulk() 0 55 11
A request() 0 23 4
A getFileSize() 0 12 1
A upload() 0 33 5
A getChunkSize() 0 9 2
A createCurlHandler() 0 19 6
B setCurlOpts() 0 47 7
B download() 0 59 8

How to fix   Complexity   

Complex Class

Complex classes like Client 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 Client, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace EasyHttp;
4
5
use CurlHandle;
6
use EasyHttp\Model\DownloadResult;
7
use EasyHttp\Model\HttpOptions;
8
use EasyHttp\Model\HttpResponse;
9
use EasyHttp\Model\UploadResult;
10
use EasyHttp\Traits\ClientTrait;
11
use EasyHttp\Util\Utils;
12
13
/**
14
 * Client
15
 *
16
 * @link    https://github.com/shahradelahi/easy-http
17
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
18
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
19
 */
20
class Client
21
{
22
23
    use ClientTrait;
24
25
    /**
26
     * This variable is used to defied is certificate self-signed or not
27
     *
28
     * @var bool
29
     */
30
    private bool $isSelfSigned = true;
31
32
    /**
33
     * The temp directory to download files - default is $_SERVER['TEMP']
34
     *
35
     * @var ?string
36
     */
37
    private ?string $tempDir;
38
39
    /**
40
     * The Max count of chunk to download file
41
     *
42
     * @var int
43
     */
44
    public int $maxChunkCount = 10;
45
46
    /**
47
     * The constructor of the client
48
     */
49
    public function __construct()
50
    {
51
        $this->tempDir = $_SERVER['TEMP'] ?? null;
52
    }
53
54
    /**
55
     * Set has self-signed certificate
56
     *
57
     * This is used to set the curl option CURLOPT_SSL_VERIFYPEER
58
     * and CURLOPT_SSL_VERIFYHOST to false. This is useful when you are
59
     * in local environment, or you have self-signed certificate.
60
     *
61
     * @param bool $has
62
     *
63
     * @return void
64
     */
65
    public function setHasSelfSignedCertificate(bool $has): void
66
    {
67
        $this->isSelfSigned = $has;
68
    }
69
70
    /**
71
     * Set the temporary directory path to save the downloaded files
72
     *
73
     * @param string $path
74
     *
75
     * @return void
76
     */
77
    public function setTempPath(string $path): void
78
    {
79
        if (!file_exists($path)) {
80
            throw new \InvalidArgumentException('The directory path is not exists');
81
        }
82
        $this->tempDir = $path;
83
    }
84
85
    /**
86
     * This method is used to send a http request to a given url.
87
     *
88
     * @param string $method
89
     * @param string $uri
90
     * @param array|HttpOptions $options
91
     *
92
     * @return HttpResponse
93
     */
94
    public function request(string $method, string $uri, array|HttpOptions $options = []): HttpResponse
95
    {
96
        $CurlHandle = $this->createCurlHandler($method, $uri, $options);
97
        if (!$CurlHandle) throw new \RuntimeException('Curl handle has not been created');
98
99
        $result = new HttpResponse();
100
        $result->setCurlHandle($CurlHandle);
101
102
        $response = curl_exec($CurlHandle);
103
        if (curl_errno($CurlHandle) || !$response) {
104
            $result->setError(curl_error($CurlHandle));
105
            $result->setErrorCode(curl_errno($CurlHandle));
106
            return $result;
107
        }
108
109
        $result->setStatusCode(curl_getinfo($CurlHandle, CURLINFO_HTTP_CODE));
110
        $result->setHeaderSize(curl_getinfo($CurlHandle, CURLINFO_HEADER_SIZE));
111
        $result->setHeaders(substr((string)$response, 0, $result->getHeaderSize()));
112
        $result->setBody(substr((string)$response, $result->getHeaderSize()));
113
114
        curl_close($CurlHandle);
115
116
        return $result;
117
    }
118
119
    /**
120
     * Send multiple requests to a given url.
121
     *
122
     * @param array $requests [{method, uri, options}, ...]
123
     *
124
     * @return array<HttpResponse>
125
     */
126
    public function bulk(array $requests): array
127
    {
128
        $result = [];
129
        $handlers = [];
130
        $multi_handler = curl_multi_init();
131
        foreach ($requests as $request) {
132
133
            $CurlHandle = $this->createCurlHandler(
134
                $request['method'] ?? null,
135
                $request['uri'],
136
                $request['options'] ?? []
137
            );
138
            if (!$CurlHandle) throw new \RuntimeException('Curl handle has not been created');
139
            $handlers[] = $CurlHandle;
140
            curl_multi_add_handle($multi_handler, $CurlHandle);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_add_handle() does only seem to accept CurlMultiHandle|resource, 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

140
            curl_multi_add_handle(/** @scrutinizer ignore-type */ $multi_handler, $CurlHandle);
Loading history...
141
142
        }
143
144
        $active = null;
145
        do {
146
            $mrc = curl_multi_exec($multi_handler, $active);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_exec() does only seem to accept CurlMultiHandle|resource, 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

146
            $mrc = curl_multi_exec(/** @scrutinizer ignore-type */ $multi_handler, $active);
Loading history...
147
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
148
149
        while ($active && $mrc == CURLM_OK) {
150
            if (curl_multi_select($multi_handler) != -1) {
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_select() does only seem to accept CurlMultiHandle|resource, 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

150
            if (curl_multi_select(/** @scrutinizer ignore-type */ $multi_handler) != -1) {
Loading history...
151
                do {
152
                    $mrc = curl_multi_exec($multi_handler, $active);
153
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
154
            }
155
        }
156
157
        foreach ($handlers as $handler) {
158
            curl_multi_remove_handle($multi_handler, $handler);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_remove_handle() does only seem to accept CurlMultiHandle|resource, 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

158
            curl_multi_remove_handle(/** @scrutinizer ignore-type */ $multi_handler, $handler);
Loading history...
159
        }
160
        curl_multi_close($multi_handler);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_close() does only seem to accept CurlMultiHandle|resource, 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

160
        curl_multi_close(/** @scrutinizer ignore-type */ $multi_handler);
Loading history...
161
162
        foreach ($handlers as $handler) {
163
            $content = curl_multi_getcontent($handler);
164
            $response = new HttpResponse();
165
166
            if (curl_errno($handler)) {
167
                $response->setError(curl_error($handler));
168
                $response->setErrorCode(curl_errno($handler));
169
            }
170
171
            $response->setCurlHandle($handler);
172
            $response->setStatusCode(curl_getinfo($handler, CURLINFO_HTTP_CODE));
173
            $response->setHeaderSize(curl_getinfo($handler, CURLINFO_HEADER_SIZE));
174
            $response->setHeaders(substr($content, 0, $response->getHeaderSize()));
175
            $response->setBody(substr($content, $response->getHeaderSize()));
176
177
            $result[] = $response;
178
        }
179
180
        return $result;
181
    }
182
183
    /**
184
     * Create curl handler.
185
     *
186
     * @param ?string $method
187
     * @param string $uri
188
     * @param array|HttpOptions $options
189
     *
190
     * @return false|CurlHandle
191
     */
192
    private function createCurlHandler(?string $method, string $uri, array|HttpOptions $options = []): false|CurlHandle
193
    {
194
        $handler = curl_init();
195
        if (is_resource($handler) || !$handler) return false;
196
197
        if (gettype($options) === 'array') {
198
            $options = new HttpOptions($options);
199
        }
200
201
        if (count($options->queries) > 0) {
202
            if (!str_contains($uri, '?')) $uri .= '?';
203
            $uri .= $options->getQueryString();
204
        }
205
206
        curl_setopt($handler, CURLOPT_URL, $uri);
207
208
        $this->setCurlOpts($handler, $method, $options);
209
210
        return $handler;
211
    }
212
213
    /**
214
     * Setup curl options based on the given method and our options.
215
     *
216
     * @param CurlHandle $cHandler
217
     * @param ?string $method
218
     * @param HttpOptions $options
219
     *
220
     * @return void
221
     */
222
    private function setCurlOpts(CurlHandle $cHandler, ?string $method, HttpOptions $options): void
223
    {
224
        curl_setopt($cHandler, CURLOPT_HEADER, true);
225
        curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'GET');
226
227
        # Fetch the header
228
        $fetchedHeaders = [];
229
        foreach ($options->headers as $header => $value) {
230
            $fetchedHeaders[] = $header . ': ' . $value;
231
        }
232
233
        # Set headers
234
        curl_setopt($cHandler, CURLOPT_HTTPHEADER, $fetchedHeaders ?? []);
235
236
237
        # Add body if we have one.
238
        if ($options->body) {
239
            curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'POST');
240
            curl_setopt($cHandler, CURLOPT_POSTFIELDS, $options->body);
241
            curl_setopt($cHandler, CURLOPT_POST, true);
242
        }
243
244
        # Check for a proxy
245
        if ($options->proxy != null) {
246
            curl_setopt($cHandler, CURLOPT_PROXY, $options->proxy->getProxy());
247
            curl_setopt($cHandler, CURLOPT_PROXYUSERPWD, $options->proxy->getAuth());
248
            if ($options->proxy->type !== null) {
249
                curl_setopt($cHandler, CURLOPT_PROXYTYPE, $options->proxy->type);
250
                curl_setopt($cHandler, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
251
            }
252
        }
253
254
        curl_setopt($cHandler, CURLOPT_RETURNTRANSFER, true);
255
        curl_setopt($cHandler, CURLOPT_FOLLOWLOCATION, true);
256
257
        # Add and override the custom curl options.
258
        foreach ($options->curlOptions as $option => $value) {
259
            curl_setopt($cHandler, $option, $value);
260
        }
261
262
        # if we have a timeout, set it.
263
        curl_setopt($cHandler, CURLOPT_TIMEOUT, $options->timeout ?? 10);
264
265
        # If self-signed certs are allowed, set it.
266
        if ($this->isSelfSigned === true) {
267
            curl_setopt($cHandler, CURLOPT_SSL_VERIFYPEER, false);
268
            curl_setopt($cHandler, CURLOPT_SSL_VERIFYHOST, false);
269
        }
270
    }
271
272
    /**
273
     * Download large files.
274
     *
275
     * This method is used to download large files with creating multiple requests.
276
     *
277
     * Change `max_chunk_count` variable to change the number of chunks. (default: 10)
278
     *
279
     * @param string $url The direct url to the file.
280
     * @param array|HttpOptions $options The options to use.
281
     *
282
     * @return DownloadResult
283
     */
284
    public function download(string $url, array|HttpOptions $options = []): DownloadResult
285
    {
286
        if (empty($this->tempDir)) {
287
            throw new \RuntimeException('No temp directory set.');
288
        }
289
290
        if (!file_exists($this->tempDir)) {
291
            if (mkdir($this->tempDir, 0777, true) === false) {
292
                throw new \RuntimeException('Could not create temp directory.');
293
            }
294
        }
295
296
        if (gettype($options) === 'array') {
297
            $options = new HttpOptions($options);
298
        }
299
300
        $fileSize = $this->getFileSize($url);
301
        $chunkSize = $this->getChunkSize($fileSize);
302
303
        $result = new DownloadResult();
304
305
        $result->id = uniqid();
306
        $result->chunksPath = $this->tempDir . '/' . $result->id . '/';
307
        mkdir($result->chunksPath, 0777, true);
308
309
        $result->fileSize = $fileSize;
310
        $result->chunkSize = $chunkSize;
311
        $result->chunks = ceil($fileSize / $chunkSize);
312
313
        $result->startTime = time();
314
315
        $requests = [];
316
        for ($i = 0; $i < $result->chunks; $i++) {
317
            $range = $i * $chunkSize . '-' . ($i + 1) * $chunkSize;
318
            if ($i + 1 === $result->chunks) {
319
                $range = $i * $chunkSize . '-' . $fileSize;
320
            }
321
            $requests[] = [
322
                'method' => 'GET',
323
                'uri' => $url,
324
                'options' => array_merge($options->toArray(), [
325
                    'CurlOptions' => [
326
                        CURLOPT_RANGE => $range
327
                    ],
328
                ])
329
            ];
330
        }
331
332
        foreach ($this->bulk($requests) as $response) {
333
            $result->addChunk(
334
                Utils::randomString(16),
335
                $response->getBody(),
336
                $response->getCurlInfo()->TOTAL_TIME
337
            );
338
        }
339
340
        $result->endTime = time();
341
342
        return $result;
343
    }
344
345
    /**
346
     * Upload single or multiple files with request method of POST.
347
     *
348
     * @param string $url The direct url to the file.
349
     * @param string|array $filePath The path to the file.
350
     * @param array|HttpOptions $options The options to use.
351
     *
352
     * @return UploadResult
353
     */
354
    public function upload(string $url, string|array $filePath, array|HttpOptions $options = []): UploadResult
355
    {
356
        if (gettype($options) === 'array') {
357
            $options = new HttpOptions($options);
358
        }
359
360
        if (gettype($filePath) === 'string') {
361
            $filePath = [$filePath];
362
        }
363
364
        $result = new UploadResult();
365
        $result->startTime = time();
366
367
        foreach ($filePath as $file) {
368
            $options->addMultiPart('file', [
369
                'name' => basename($file),
370
                'contents' => fopen($file, 'r')
371
            ]);
372
        }
373
374
        $response = $this->request('POST', $url, array_merge($options->toArray(), [
375
            'header' => [
376
                'Content-Type' => 'multipart/form-data'
377
            ]
378
        ]));
379
380
        $result->endTime = time();
381
        $result->response = $response;
382
        if ($response->getStatusCode() === 200) {
383
            $result->success = true;
384
        }
385
386
        return $result;
387
    }
388
389
    /**
390
     * Get file size.
391
     *
392
     * @param string $url The direct url to the file.
393
     * @return int
394
     */
395
    private function getFileSize(string $url): int
396
    {
397
        $response = $this->get($url, [
398
            'headers' => [
399
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36',
400
            ],
401
            'CurlOptions' => [
402
                CURLOPT_NOBODY => true,
403
            ]
404
        ]);
405
406
        return (int)$response->getHeaderLine('Content-Length') ?? 0;
407
    }
408
409
    /**
410
     * Get the size of each chunk.
411
     *
412
     * For default, we're dividing filesize to 10 as max size of each chunk.
413
     * If the file size was smaller than 2MB, we'll use the filesize as single chunk.
414
     *
415
     * @param int $fileSize The file size.
416
     * @return int
417
     */
418
    private function getChunkSize(int $fileSize): int
419
    {
420
        $maxChunkSize = $fileSize / $this->maxChunkCount;
421
422
        if ($fileSize <= 2 * 1024 * 1024) {
423
            return $fileSize;
424
        }
425
426
        return min($maxChunkSize, $fileSize);
427
    }
428
429
}