Skip to content

Instantly share code, notes, and snippets.

@SimonEast
Last active September 18, 2018 07:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SimonEast/abef0112596091640396c1cb80bd6ff2 to your computer and use it in GitHub Desktop.
Save SimonEast/abef0112596091640396c1cb80bd6ff2 to your computer and use it in GitHub Desktop.
PHP Curl (The Easy Way)
<?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