Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 162 additions & 5 deletions src/CurlClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,58 @@ class CurlClient
const HTTP_ERROR = 500;

const RATE_LIMIT_RESET = "x-rate-limit-reset";
const RATE_LIMIT = "x-rate-limit";
const RATE_LIMIT_REMAINING = "x-rate-limit-remaining";
const DEFAULT_RATE_LIMIT = 180;

/**
* @var array of headers sent with HTTP requests
*/
private $_requestHeaders = array();

/**
* @var array of headers received with HTTP responses
*/
private static $_responseHeaders = array();

/**
* @var bool keep the request rate below the API rate limit
*/
private $_keepBelowRateLimit;

/**
* @var array of extra CURL options
*/
private $_curlOptions = array();

/**
* @var bool log curl requests and responses
*/
private $_debug;

/**
* @var string file name for curl debugging log
*/
private $_logFile = '/tmp/curl_data.log';

/**
* @var int the last HTTP status code received
*/
private $_lastStatusCode;

public function __construct($apiKey = "", $siteID = "")
public $MockCurl = false;

/**
* Array key should be part of the URL to which the response belongs
* @var MockCurlResponse[]
*/
public $UrlMockResponse = [];

public function __construct($apiKey = "", $siteID = "", $keepBelowRateLimit = true, $debug = false)
{
$this->setCredentials($apiKey, $siteID);
$this->_keepBelowRateLimit = $keepBelowRateLimit;
$this->_debug = $debug;
}

/**
Expand Down Expand Up @@ -196,6 +229,11 @@ public function httpRequest($requestParams, $url, $method, $requiredParams, $opt
return false;
}

if ($this->_keepBelowRateLimit)
{
$this->_checkRateLimit();
}

if ($options &&
is_array($options) &&
array_key_exists("headers", $options) &&
Expand Down Expand Up @@ -274,22 +312,97 @@ function($curl, $header) use (&$headers)
return $len;
}
);
curl_setopt($curlHandle, CURLOPT_TIMEOUT, 60);
curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($curlHandle, CURLOPT_TIMEOUT, 60 * 2);

if ($this->MockCurl)
$result = $this->DoMockCurl($url, $method, $headers);
else
$result = $this->DoCurl($curlHandle, $requestParams, $headers);

unset($this->_requestHeaders["Content-Type"]);
return $result;
}

private function DoCurl($curlHandle, $requestParams, $headers)
{
/** ********** DEBUGGER ********** */
if ($this->_debug) {

// capture the PHP output
ob_start();
$out = fopen('php://output', 'w');

curl_setopt($curlHandle, CURLOPT_VERBOSE, true);
curl_setopt($curlHandle, CURLOPT_STDERR, $out);
}

$result = curl_exec($curlHandle);

/** ********** DEBUGGER ********** */
if ($this->_debug) {

/** @noinspection PhpUndefinedVariableInspection */
fclose($out);

// get the curl output
$data = ob_get_clean();

// insert the request parameters sent
$data = preg_replace('/(\r?\n){2}/', "\n\n$requestParams\n\n", $data, 1);

// append the response received
$data .= "\n\n" . $result . "\n\n";

// write to a log file
file_put_contents($this->_logFile, $data, FILE_APPEND);
}

self::$_responseHeaders = $headers;

$this->_lastStatusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
if ($this->_lastStatusCode == self::HTTP_RATE_LIMIT)
{
$result = $this->retry($headers[self::RATE_LIMIT_RESET], $curlHandle);
}

curl_close($curlHandle);

unset($this->_requestHeaders["Content-Type"]);
return $result;
}

private function DoMockCurl($url, $method, $headers)
{
/** @var ICurlResponse $response */
$response = null;
$return_key = '';

// look for a mock response that matches the URL
foreach ($this->UrlMockResponse as $key => $value) {

// does this response match the requested URL?
if (!str_contains($url, $key))
continue;

if (strtoupper($value->Method) == strtoupper($method)) {
$response = $value->Response;
$return_key = $key;
break;
}
}

self::$_responseHeaders = $headers;

if (empty($return_key))
return '';

$this->_lastStatusCode = $response->HttpCode;

// remove this response from the stack
unset($this->UrlMockResponse[$return_key]);

// return the value
return $response->Content;
}

/**
* @brief get the last HTTP status code received
*
Expand Down Expand Up @@ -325,4 +438,48 @@ public function retry($wait, $curlHandle)
$this->_lastStatusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
return $result;
}

private function _checkRateLimit()
{
// If no API calls have been made, no need to delay.
if (empty(self::$_responseHeaders))
{
return;
}

// Default the $limit and $remaining values if not set in the last response header.
/** @var int $limit */
$limit = isset(self::$_responseHeaders[self::RATE_LIMIT])
? (int)self::$_responseHeaders[self::RATE_LIMIT]
: self::DEFAULT_RATE_LIMIT;

/** @var int $remaining */
$remaining = isset(self::$_responseHeaders[self::RATE_LIMIT_REMAINING])
? (int)self::$_responseHeaders[self::RATE_LIMIT_REMAINING]
: self::DEFAULT_RATE_LIMIT;

// If no API calls have been made, no need to delay.
if ($limit == $remaining)
{
return;
}

// If we are below 5% remaining, sleep for 0.50 seconds.
if ($remaining / $limit < 0.05)
{
usleep(500000);
return;
}

// If we are below 10% remaining, sleep for 0.25 seconds.
if ($remaining / $limit < 0.1)
{
usleep(250000);
}
}

public function setLogFile($fileName)
{
$this->_logFile = $fileName;
}
}
13 changes: 13 additions & 0 deletions src/ICurlResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace OntraportAPI;

/**
* @property string Content
* @property int HttpCode
* @property int ErrorNumber
* @property string ErrorMessage
*/
interface ICurlResponse
{
}
21 changes: 21 additions & 0 deletions src/MockCurlResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace OntraportAPI;

class MockCurlResponse
{
public string $Method;

/** @var ICurlResponse */
public $Response;

/**
* @param string $method
* @param ICurlResponse|mixed $response
*/
public function __construct(string $method, $response)
{
$this->Method = $method;
$this->Response = $response;
}
}