Last active
September 18, 2018 07:39
-
-
Save SimonEast/abef0112596091640396c1cb80bd6ff2 to your computer and use it in GitHub Desktop.
PHP Curl (The Easy Way)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Easy remote HTTP requests using a single function | |
* (How curl probably SHOULD have been written originally) | |
* | |
* If you need something more advanced/robust, use 'guzzle' | |
* But this function is useful for when you don't need a massive library, | |
* but rather something simpler and more lightweight | |
* | |
* Examples: | |
* | |
* $response = curl('http://google.com'); | |
* | |
* $response = curl('http://google.com', [ | |
* 'type' => 'POST', | |
* 'postData' => ['q' => 'my search'], | |
* 'headers' => ['API-Key: XXXXXXX'], | |
* 'cacheTimeInMinutes' => 60, | |
* 'throwExceptionOnError' => true, | |
* ]); | |
* | |
* You can even upload a file via POST, just pass in: | |
* | |
* 'postData' => [ | |
* 'file' => new CURLFile($pathToFile), | |
* 'anotherField' => 123, | |
* ] | |
* | |
* For the full list of options and default values, see the $options array below. | |
* | |
* Returns an array that includes: | |
* 'success' (boolean) | |
* 'body' (string) | |
* 'headers' (array with lowercased keys & values) | |
* 'requestHeaders' (numerical array of strings) | |
* 'responseCode' (numeric) | |
* 'JSON' (array if a JSON body is detected, otherwise false) | |
* 'error' (string) | |
* 'attempts' (integer representing how many tries it took to get the response - used when retriesOnFailure is set) | |
* 'responseTime' (float that shows time taken to make request) | |
* 'cached' (boolean indicating whether result came from cache or not) | |
* | |
* Version 2.1 | |
* By Simon East, for Yump.com.au | |
* Latest version at: https://gist.github.com/SimonEast/abef0112596091640396c1cb80bd6ff2 | |
*/ | |
function curl($url, $options = []) | |
{ | |
// Set some default options | |
$options += [ | |
'type' => 'GET', | |
'postData' => [], // eg. ['name' => 'Yump'] | |
'userAgent' => 'PHP curl', | |
'headers' => [], // eg. ['Content-type: text/html'] | |
'timeout' => 20, | |
'verifySSL' => true, | |
'followRedirects' => true, | |
'throwExceptionOnError' => false, // Set this to true to enable the throwing of exceptions (disabled by default) | |
'retriesOnFailure' => 2, | |
'secondsBetweenRetries' => 1, | |
'cacheTimeInMinutes' => 0, // 0 disables caching | |
'cacheFolder' => __DIR__ . '/curl_cache', // Will be created if doesn't exist (ensure this folder cannot be accessed publicly if you're going to be caching sensitive information) | |
'cacheFile' => false, // Not needed in most cases (auto-generated) | |
'logAllRequestsTo' => '', // Not yet implemented | |
]; | |
if ($options['cacheTimeInMinutes']) { | |
// Define cache file name if only folder is specified | |
if (empty($options['cacheFile']) && !empty($options['cacheFolder'])) { | |
$options['cacheFile'] = | |
rtrim($options['cacheFolder'], '/') | |
. '/curl_' . md5($url . serialize($options)); | |
} | |
// Create cache folder if it's missing | |
if (!is_dir($options['cacheFolder'])) { | |
mkdir($options['cacheFolder']); | |
} | |
// Return cache file, if data is still fresh | |
if (file_exists($options['cacheFile']) | |
&& (filemtime($options['cacheFile']) + $options['cacheTimeInMinutes']*60) > time() | |
) { | |
$cachedResult = json_decode(file_get_contents($options['cacheFile']), true); | |
if (count($cachedResult)) { | |
$cachedResult['cached'] = true; | |
return $cachedResult; | |
} | |
} | |
} | |
// Setup cURL with the relevant options | |
// Warning: if an option cannot be set, curl_setopt_array() returns false and ignores any subsequent options | |
// We might want to handle that later. | |
$curl = curl_init(); | |
curl_setopt_array($curl, [ | |
CURLOPT_RETURNTRANSFER => true, | |
CURLOPT_URL => $url, | |
CURLOPT_USERAGENT => $options['userAgent'], | |
CURLOPT_HEADER => false, // We use a separate header callback function so do NOT combine headers with body | |
CURLOPT_CONNECTTIMEOUT => $options['timeout'], | |
CURLOPT_TIMEOUT => $options['timeout'], | |
CURLOPT_FOLLOWLOCATION => $options['followRedirects'], | |
CURLOPT_MAXREDIRS => 10, | |
CURLOPT_SSL_VERIFYPEER => $options['verifySSL'] ? 2 : 0, | |
CURLOPT_SSL_VERIFYHOST => $options['verifySSL'] ? 2 : 0, | |
CURLOPT_CUSTOMREQUEST => $options['type'], | |
CURLOPT_COOKIEJAR => dirname(__FILE__) . '/.curl_cookies.txt', // TODO: Handle case where cookie file is not writeable | |
CURLOPT_COOKIEFILE => dirname(__FILE__) . '/.curl_cookies.txt', // TODO: Handle case where cookie file is not writeable | |
CURLINFO_HEADER_OUT => !$options['logAllRequestsTo'], // also store the outgoing headers - useful for debugging | |
]); | |
// Support for logging all outgoing requests to a text file | |
// (NOT YET WORKING - or doesn't do as I initially expected) | |
if ($options['logAllRequestsTo']) { | |
$log = fopen($options['logAllRequestsTo'], 'a' /* append */); | |
curl_setopt_array($curl, [ | |
CURLOPT_VERBOSE => true, | |
CURLOPT_STDERR => $log, | |
]); | |
} | |
// Special handling for POST requests | |
if ($options['type'] == 'POST') { | |
curl_setopt_array($curl, [ | |
CURLOPT_POST => 1, | |
CURLOPT_POSTFIELDS => $options['postData'], | |
// Set this to null, otherwise a redirected POST request | |
// will make *another* POST on the second request which | |
// is NOT what browsers usually do. | |
CURLOPT_CUSTOMREQUEST => null, | |
]); | |
// For POST requests, some servers require that we provide a 'Content-Length' header too | |
// To support this, the CURLOPT_POSTFIELDS line above needs to read http_build_query($options['postData']) | |
// $options['headers'][] = 'Content-Length: ' . strlen(http_build_query($options['postData'])); | |
$result['postData'] = $options['postData']; | |
} | |
// Set outgoing headers | |
curl_setopt_array($curl, [CURLOPT_HTTPHEADER => $options['headers']]); | |
// Run the next steps inside a loop, allowing for multiple retries if necessary | |
$result['attempts'] = 0; | |
do { | |
// Obtain response HTTP headers (the right way + all lowercased) | |
// Thanks to https://stackoverflow.com/a/41135574/195835 | |
// Note: does NOT handle multiple instances of the same header. Later one will overwrite. | |
$result['headers'] = []; | |
curl_setopt($curl, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$result) { | |
$len = strlen($header); | |
$header = explode(':', $header, 2); | |
if (count($header) < 2) | |
return $len; | |
$result['headers'][strtolower(trim($header[0]))] = trim($header[1]); | |
return $len; | |
}); | |
// Run request! | |
$result['body'] = curl_exec($curl); | |
// Check for errors, including network error or HTTP error | |
if ($result['body'] === false) { | |
$result['error'] = curl_error($curl); | |
} else { | |
$result['error'] = null; | |
} | |
// TODO: Maybe at some stage we could split these headers into associative arrays | |
// (Although that can be difficult since some headers can appear multiple times) | |
$result['requestHeaders'] = explode("\r\n", trim(curl_getinfo($curl, CURLINFO_HEADER_OUT))); | |
$result['responseTime'] = curl_getinfo($curl, CURLINFO_TOTAL_TIME); | |
// Get response code (use zero if none present) | |
$result['responseCode'] = curl_getinfo($curl, CURLINFO_HTTP_CODE); | |
// Consider the request a success if response code is less than 400 (200, 301, 302 etc.) | |
$result['success'] = $result['responseCode'] && $result['responseCode'] < 400; | |
$result['error'] = curl_error($curl); | |
$result['attempts']++; | |
// Retry multiple times, if this is set in the options | |
// (Hmm... Is is OK that we don't close the previous curl session yet?) | |
if (!$result['success'] && $result['attempts'] <= $options['retriesOnFailure']) { | |
usleep($options['secondsBetweenRetries'] * 1000000); | |
continue; | |
} | |
// Cleanup | |
curl_close($curl); | |
if (isset($log)) { | |
fclose($log); | |
} | |
if (!$result['success'] && $options['throwExceptionOnError']) { | |
throw new \Exception($result['error']); | |
} | |
// If we've reached this point, request was either successful or exhausted the retries | |
// without throwing an exception, so we'll exit loop and continue | |
break; | |
} while (1); | |
// Attempt to parse response as JSON | |
$result['JSON'] = json_decode($result['body'], true); | |
// Save to cache file, if there is one (not yet implemented) | |
if ($options['cacheFile']) { | |
file_put_contents($options['cacheFile'], json_encode($result)); | |
} | |
$result['cached'] = false; | |
// Uncomment the line below to assist with debugging | |
// file_put_contents('.last_curl_body.txt', $result['body']); | |
return $result; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment