Client   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 329
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 126
dl 0
loc 329
c 0
b 0
f 0
rs 9.52
wmc 36

10 Methods

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

143
			curl_multi_add_handle(/** @scrutinizer ignore-type */ $multi_handler, $CurlHandle);
Loading history...
144
145
		}
146
147
		$active = null;
148
		do {
149
			$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

149
			$mrc = curl_multi_exec(/** @scrutinizer ignore-type */ $multi_handler, $active);
Loading history...
150
		} while ($mrc == CURLM_CALL_MULTI_PERFORM);
151
152
		while ($active && $mrc == CURLM_OK) {
153
			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

153
			if (curl_multi_select(/** @scrutinizer ignore-type */ $multi_handler) != -1) {
Loading history...
154
				do {
155
					$mrc = curl_multi_exec($multi_handler, $active);
156
				} while ($mrc == CURLM_CALL_MULTI_PERFORM);
157
			}
158
		}
159
160
		foreach ($handlers as $handler) {
161
			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

161
			curl_multi_remove_handle(/** @scrutinizer ignore-type */ $multi_handler, $handler);
Loading history...
162
		}
163
		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

163
		curl_multi_close(/** @scrutinizer ignore-type */ $multi_handler);
Loading history...
164
165
		foreach ($handlers as $handler) {
166
			$content = curl_multi_getcontent($handler);
167
			$response = new HttpResponse();
168
169
			if (curl_errno($handler)) {
170
				$response->setErrorCode(curl_errno($handler));
171
				$response->setErrorMessage(
172
					curl_error($handler) ??
173
					ErrorCode::getMessage(curl_errno($handler))
174
				);
175
			}
176
177
			$response->setCurlHandle($handler);
178
			$response->setStatusCode(curl_getinfo($handler, CURLINFO_HTTP_CODE));
179
			$response->setHeaderSize(curl_getinfo($handler, CURLINFO_HEADER_SIZE));
180
			$response->setHeaders(substr($content, 0, $response->getHeaderSize()));
181
			$response->setBody(substr($content, $response->getHeaderSize()));
182
183
			$result[] = $response;
184
		}
185
186
		return $result;
187
	}
188
189
	/**
190
	 * Download large files.
191
	 *
192
	 * This method is used to download large files with creating multiple requests.
193
	 *
194
	 * Change `max_chunk_count` variable to change the number of chunks. (default: 10)
195
	 *
196
	 * @param string $url The direct url to the file.
197
	 * @param array|HttpOptions $options The options to use.
198
	 *
199
	 * @return DownloadResult
200
	 */
201
	public function download(string $url, array|HttpOptions $options = []): DownloadResult
202
	{
203
		if (empty($this->tempDir)) {
204
			throw new RuntimeException(
205
				'The temp directory is not set. Please set the temp directory using the `setTempDir` method.'
206
			);
207
		}
208
209
		if (!file_exists($this->tempDir)) {
210
			if (mkdir($this->tempDir, 0777, true) === false) {
211
				throw new RuntimeException(
212
					'The temp directory is not writable. Please set the temp directory using the `setTempDir` method.'
213
				);
214
			}
215
		}
216
217
		if (gettype($options) === 'array') {
218
			$options = new HttpOptions($options);
219
		}
220
221
		$fileSize = $this->get_file_size($url);
222
		$chunkSize = $this->get_chunk_size($fileSize);
223
224
		$result = new DownloadResult();
225
226
		$result->id = uniqid();
227
		$result->chunksPath = $this->tempDir . '/' . $result->id . '/';
228
		mkdir($result->chunksPath, 0777, true);
229
230
		$result->fileSize = $fileSize;
231
		$result->chunkSize = $chunkSize;
232
		$result->chunks = ceil($fileSize / $chunkSize);
233
234
		$result->startTime = time();
235
236
		$requests = [];
237
		for ($i = 0; $i < $result->chunks; $i++) {
238
			$range = $i * $chunkSize . '-' . ($i + 1) * $chunkSize;
239
			if ($i + 1 === $result->chunks) {
240
				$range = $i * $chunkSize . '-' . $fileSize;
241
			}
242
			$requests[] = [
243
				'method' => 'GET',
244
				'uri' => $url,
245
				'options' => array_merge($options->toArray(), [
246
					'CurlOptions' => [
247
						CURLOPT_RANGE => $range
248
					],
249
				])
250
			];
251
		}
252
253
		foreach ($this->bulk($requests) as $response) {
254
			$result->addChunk(
255
				Toolkit::randomString(16),
256
				$response->getBody(),
257
				$response->getInfoFromCurl()->TOTAL_TIME
258
			);
259
		}
260
261
		$result->endTime = time();
262
263
		return $result;
264
	}
265
266
	/**
267
	 * Upload single or multiple files
268
	 *
269
	 * This method is sending file with request method of POST and
270
	 * Content-Type of multipart/form-data.
271
	 *
272
	 * @param string $url The direct url to the file.
273
	 * @param array $filePath The path to the file.
274
	 * @param array|HttpOptions $options The options to use.
275
	 *
276
	 * @return HttpResponse
277
	 */
278
	public function upload(string $url, array $filePath, array|HttpOptions $options = []): HttpResponse
279
	{
280
		if (gettype($options) === 'array') {
281
			$options = new HttpOptions($options);
282
		}
283
284
		$multipart = [];
285
286
		foreach ($filePath as $key => $file) {
287
			$multipart[$key] = new \CURLFile(
288
				realpath($file),
289
				$this->get_file_type($file)
290
			);
291
		}
292
293
		$options->setMultipart($multipart);
294
		return $this->post($url, $options);
295
	}
296
297
	/**
298
	 * Get filetype with the extension.
299
	 *
300
	 * @param string $filename The absolute path to the file.
301
	 * @return string eg. image/jpeg
302
	 */
303
	public static function get_file_type(string $filename): string
304
	{
305
		return MimeType::TYPES[pathinfo($filename, PATHINFO_EXTENSION)] ?? 'application/octet-stream';
306
	}
307
308
	/**
309
	 * Get file size.
310
	 *
311
	 * @param string $url The direct url to the file.
312
	 * @return int
313
	 */
314
	public function get_file_size(string $url): int
315
	{
316
		if (file_exists($url)) {
317
			return filesize($url);
318
		}
319
320
		$response = $this->get($url, [
321
			'headers' => [
322
				'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',
323
			],
324
			'CurlOptions' => [
325
				CURLOPT_NOBODY => true,
326
			]
327
		]);
328
329
		return (int)$response->getHeaderLine('Content-Length') ?? 0;
330
	}
331
332
	/**
333
	 * Get the size of each chunk.
334
	 *
335
	 * For default, we're dividing filesize to 10 as max size of each chunk.
336
	 * If the file size was smaller than 2MB, we'll use the filesize as single chunk.
337
	 *
338
	 * @param int $fileSize The file size.
339
	 * @return int
340
	 */
341
	private function get_chunk_size(int $fileSize): int
342
	{
343
		$maxChunkSize = $fileSize / $this->maxChunkCount;
344
345
		if ($fileSize <= 2 * 1024 * 1024) {
346
			return $fileSize;
347
		}
348
349
		return min($maxChunkSize, $fileSize);
350
	}
351
352
}