shahradelahi /
easy-http
| 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
Loading history...
|
|||||
| 144 | |||||
| 145 | } |
||||
| 146 | |||||
| 147 | $active = null; |
||||
| 148 | do { |
||||
| 149 | $mrc = curl_multi_exec($multi_handler, $active); |
||||
|
0 ignored issues
–
show
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
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
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
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
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
Loading history...
|
|||||
| 162 | } |
||||
| 163 | curl_multi_close($multi_handler); |
||||
|
0 ignored issues
–
show
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
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 | } |