├── .gitignore ├── rector.php ├── src └── apicore │ ├── client.php │ ├── interfaces │ ├── loggingInterface.php │ └── coreInterface.php │ └── structure │ ├── response.php │ ├── request.php │ └── apicore.php ├── .release-it.json ├── CHANGELOG.md ├── package.json ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .idea 3 | composer.lock 4 | .ddev 5 | .php-cs-fixer.cache 6 | /node_modules 7 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | import(SetList::CODE_QUALITY); 10 | }; 11 | -------------------------------------------------------------------------------- /src/apicore/client.php: -------------------------------------------------------------------------------- 1 | request; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | }, 5 | "npm": { 6 | "publish": false 7 | }, 8 | "git": { 9 | "changelog": "npx auto-changelog --stdout --commit-limit false -u --template https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs" 10 | }, 11 | "hooks": { 12 | "after:bump": "npx auto-changelog -p" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/apicore/interfaces/loggingInterface.php: -------------------------------------------------------------------------------- 1 | verb = $verb; 53 | } 54 | 55 | /** 56 | * Process the provided guzzle response object. 57 | * @param GuzzleHttp $guzzleResponse 58 | */ 59 | public static function processResult($guzzleResponse) 60 | { 61 | $dataType = $guzzleResponse->getHeader('Content-Type'); 62 | self::$response->dataType = array_shift($dataType); 63 | self::$response->statusCode = $guzzleResponse->getStatusCode(); 64 | self::$response->rawBodyData = (string)$guzzleResponse->getBody(); 65 | self::$response->url = $guzzleResponse->getHeaderLine('Location'); 66 | 67 | switch(self::$response->dataType) { 68 | case 'text/html': //for now, as some Apis dont set correct headers for XML. 69 | //@todo add the ability to override these defaults before a request is made. 70 | case 'text/xml': 71 | self::$response->data = self::$response->xml2array(simplexml_load_string(self::$response->rawBodyData)); 72 | break; 73 | case 'application/json': 74 | default: 75 | self::$response->data = json_decode(self::$response->rawBodyData); 76 | break; 77 | } 78 | } 79 | 80 | /** 81 | * @param $code 82 | * @param $message 83 | */ 84 | public static function addError($code, $message) 85 | { 86 | self::$error = array('code' => $code, 'message' => $message); 87 | } 88 | 89 | public static function getError() 90 | { 91 | return self::$error ?? []; 92 | } 93 | 94 | /** 95 | * Convert simpleXML object to array. 96 | * @see http://www.php.net/manual/en/ref.simplexml.php#111227 97 | * @param $xmlObject 98 | * @param array $out 99 | * @return array 100 | */ 101 | public function xml2array($xmlObject, $out = array()) 102 | { 103 | foreach ((array) $xmlObject as $index => $node) { 104 | $out[$index] = (is_object($node) || is_array($node)) ? $this->xml2array($node) : $node; 105 | } 106 | 107 | return $out; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/apicore/structure/request.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 29 | return $this; 30 | } 31 | 32 | /** 33 | * Function to set the api server address. 34 | * @param $address 35 | * @param $port 36 | * @return mixed 37 | */ 38 | public function setServer($address, $port) 39 | { 40 | $this->server = $address.':'.$port; 41 | return $this; 42 | } 43 | 44 | /** 45 | * Function to set the base path of an API. for example, an api that always lives under '/api' 46 | * @param $path 47 | * @return $this 48 | */ 49 | public function setBasePath($path) 50 | { 51 | $this->basePath = rtrim(ltrim($path, '/'), '/'); 52 | return $this; 53 | } 54 | 55 | /** 56 | * Function to set the version and if it should be used as part of the api request path 57 | * @param string $version 58 | * @param bool $flag 59 | * @return mixed 60 | */ 61 | public function setVersion($version, $flag) 62 | { 63 | $this->version = $version; 64 | $this->useVersion = $flag; 65 | return $this; 66 | } 67 | 68 | /** 69 | * Function to add to the request path. If reset then the path is overwritten 70 | * @param $path 71 | * @param bool $reset 72 | * @return $this 73 | */ 74 | public function addPathElement($path, $reset = false) 75 | { 76 | $elements = array(); 77 | $path = ltrim($path, '/'); 78 | if (strpos($path, '/') !== false) { 79 | $elements = explode('/', $path); 80 | } else { 81 | $elements[] = $path; 82 | } 83 | if ($reset) { 84 | $this->path = $elements; 85 | } elseif (!empty($this->path)) { 86 | $this->path = array_merge($this->path, $elements); 87 | } else { 88 | $this->path = $elements; 89 | } 90 | return $this; 91 | } 92 | 93 | /** 94 | * Function to reset the path part of the request. Allows a request object to be reused. 95 | */ 96 | public function resetPath() 97 | { 98 | $this->path = array(); 99 | return $this; 100 | } 101 | 102 | /** 103 | * Function to add query string parameters to the request 104 | * @param $args 105 | */ 106 | public function addQueryString($args) 107 | { 108 | $this->queryString = '?'.http_build_query($args); 109 | } 110 | 111 | /** 112 | * Function to reset the query string. 113 | */ 114 | public function resetQueryString() 115 | { 116 | $this->queryString = ''; 117 | } 118 | 119 | /** 120 | * Function to add 121 | * @param $endPoint 122 | */ 123 | public function addEndpoint($endPoint) 124 | { 125 | $this->endpoint = $endPoint; 126 | } 127 | 128 | /** 129 | * Allow us to grab the last endpoint call that was made. 130 | * @return mixed 131 | */ 132 | public function getEndpoint() 133 | { 134 | return $this->endpoint; 135 | } 136 | 137 | /** 138 | * Function to build the final request string 139 | */ 140 | private function buildRequest() 141 | { 142 | $this->request = $this->schema . $this->server; 143 | if ($this->useVersion) { 144 | $this->request .= '/'.$this->version; 145 | } 146 | if ($this->basePath) { 147 | $this->request .= '/'.$this->basePath; 148 | } 149 | if (!empty($this->path)) { 150 | $this->request .= '/' . implode('/', $this->path); 151 | } 152 | $this->request .= '/'.$this->endpoint.$this->queryString; 153 | return $this; 154 | } 155 | 156 | /** 157 | * Implements php magic __toString method. Return the specific request made as a string (Sans arguments) 158 | * @return mixed 159 | */ 160 | public function __toString() 161 | { 162 | $this->buildRequest(); 163 | return $this->request; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP API Core - Agnostic RESTful client for Resful APIs 2 | 3 | ### Description 4 | 5 | This PHP class is designed to provide a single library for all RESTful API integrations, it prevents the need to download and use individual libraries for multiple services used in a project. The core class uses a combination of php Magic methods to construct api calls based on chaining, meaning that the class itself needs to have no knowledge of API structure and can be used with any integration that provides a RESTful series of endpoints. 6 | 7 | ### Installation 8 | 9 | If you can, this should be installed with composer. Please note this is not currently available through packagist, so you will need to add the following repository to your composer.json: 10 | 11 | ```json 12 | { 13 | "repositories": [ 14 | { 15 | "type": "composer", 16 | "url": "https:\/\/codestore.sc.vg" 17 | } 18 | ] 19 | } 20 | ``` 21 | ```bash 22 | $ composer require n1ghteyes/apicore 23 | ``` 24 | ### Basic request structure 25 | 26 | In order to make a request after setting some default values (info below), you must constuct a chain. the general structure is as follows: 27 | 28 | ```php 29 | $response = $api->{HTTP VERB}->{PATH 1}->{PATH 2}->{ENDPOINT}({Args as array}); 30 | ``` 31 | 32 | In more general terms, using a request to twitter as an example: 33 | see: https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline.html 34 | 35 | > *In this instance twitter requires us to specify a file ending. As such the endpoint must be called as an encapsulated string* 36 | 37 | ```php 38 | $response = $api->GET->statuses->{"user_timeline.json"}(["screen_name" => "noradio"]); 39 | ``` 40 | 41 | ### Basic usage - Twitter Example 42 | 43 | This example assumes you have already included the vendor autoload file. 44 | ```php 45 | use n1ghteyes\apicore\client 46 | 47 | //Instanciate the class. 48 | $api = new client(); 49 | 50 | //Set the API server (In this example, twitter) - We assume the connection is HTTPS. 51 | $api->setServer("api.twitter.com"); 52 | 53 | //Set the API version - This can be an arbitrary number. If you would like to exclude the version number from the request path, pass FALSE as the second argument. 54 | $api->setVersion("1.1", FALSE); 55 | 56 | //Make a request to the oauth endpoint to generate a grant token: 57 | //@see https://developer.twitter.com/en/docs/basics/authentication/api-reference/token 58 | $response = $api->POST->oauth2->token(['grant_type' => "xxxxxx"]); 59 | ``` 60 | 61 | The above example constructs a POST request for the following URL: https://api.twitter.com/oauth2/token passing grant_type in the POST body. 62 | 63 | You can then go on to access other methods on the oauth2 route: 64 | ```php 65 | $response # $api->POST->invalidate_token(["access_token" => "xxxx"]) 66 | ``` 67 | > #### *NOTE* 68 | > *In order to use a new route (reset the path to the base url) you must call the reset path method. This may change in the future, but for now:* 69 | > 70 | > ```php 71 | > //reset the path 72 | > $api->resetPath(); 73 | > ``` 74 | 75 | ### Authentication 76 | 77 | The class supports several authentication methods. 78 | 79 | #### Basic Authentication (base62 encoded HTTP Auth) 80 | 81 | One of the most common methods of authentication with anopther service is basic http authentication. 82 | 83 | ```php 84 | //The method will automatically encode the values passed here. 85 | $api->auth("user", "password"); 86 | ``` 87 | 88 | #### Token Based Authentication 89 | 90 | If you need to pass a grant or request token generated through oauth, you can also use the auth method as follows 91 | 92 | ```php 93 | //Adding a third argument of "header" treats the first argument as the header name and the second as the header value. This can be used to add any custom header to the request. 94 | $api->auth("header-key", "token", "header") 95 | ``` 96 | 97 | ### Other Notes 98 | 99 | #### Numeric endpoints 100 | You may come across an API where the endpoint (the final path element in the URL) is numeric or starts with a number, for example if that element is a numeric ID. In this instance, you must prepend that path with an underscore to make it a valid method for PHP. 101 | 102 | The API Core will strip the underscore off the final path element if the second character in that path is numeric. 103 | 104 | For example: 105 | 106 | ```php 107 | //This is invalid, PHP will throw a fatal error. 108 | $id = 1234; 109 | $api->path1->path2->$id(); 110 | 111 | //This is valid 112 | $id = '_1234'; 113 | $api->path1->path2->$id(); 114 | 115 | //Simplified to account for unknown, potentually numeric values: 116 | $endpoint = is_numeric($id) ? '_'.$id : $id; 117 | $api->path1->path2->$endpoint(); 118 | ``` 119 | -------------------------------------------------------------------------------- /src/apicore/structure/apicore.php: -------------------------------------------------------------------------------- 1 | - www.source-control.co.uk 13 | * @copyright 2018 Toby New 14 | * @license license.txt The MIT License (MIT) 15 | */ 16 | 17 | /** 18 | * Class apiCore 19 | */ 20 | abstract class apiCore implements coreInterface 21 | { 22 | protected $request; 23 | protected $version; 24 | private $httpMethod = 'GET'; 25 | private $bodyFormat = 'body'; 26 | private $lastResult; 27 | private $args = array(); 28 | private $rawResponse = false; 29 | private $processedResponse = false; 30 | private $errors = array(); 31 | /** @var loggingInterface */ 32 | protected $logger; 33 | 34 | /** 35 | * apiCore constructor. 36 | */ 37 | public function __construct() 38 | { 39 | $this->request = new request(); 40 | $this->setSchema(); //set the defaults 41 | $this->setDefaultCurlOpts(); 42 | $this->setBodyFormat(); 43 | } 44 | 45 | public function addLogger(loggingInterface $logger) 46 | { 47 | $this->logger = $logger; 48 | } 49 | 50 | /** 51 | * Function to set request Schema 52 | * @param string $schema 53 | * @return self 54 | */ 55 | public function setSchema($schema = 'https://') 56 | { 57 | $this->request->setSchema($schema); 58 | return $this; 59 | } 60 | 61 | /** 62 | * Function to set the server address for the api call. 63 | * @param $address 64 | * @param int $port 65 | * @return self 66 | */ 67 | public function setServer($address, $port = 443) 68 | { 69 | $this->request->setServer($address, $port); 70 | return $this; 71 | } 72 | 73 | /** 74 | * Set the base request path 75 | * @param $path 76 | * @return $this 77 | */ 78 | public function setBasePath($path) 79 | { 80 | $this->request->setBasePath($path); 81 | return $this; 82 | } 83 | 84 | /** 85 | * Function to set API version and whether this should be used in the request URL 86 | * @param string $version 87 | * @param bool $flag 88 | * @return self 89 | */ 90 | public function setVersion($version, $flag = true) 91 | { 92 | $this->version = $version; 93 | $this->request->setVersion($version, $flag); 94 | return $this; 95 | } 96 | 97 | /** 98 | * Function to set the type of data being sent and received 99 | * @param $format 100 | * @return mixed 101 | */ 102 | public function setBodyFormat($format = 'body') 103 | { 104 | switch($format) { 105 | case 'form': 106 | $this->bodyFormat = 'form_params'; 107 | break; 108 | default: 109 | $this->bodyFormat = $format; 110 | break; 111 | } 112 | return $this; 113 | } 114 | 115 | /** 116 | * Get the current version provided to the API 117 | * @return mixed 118 | */ 119 | public function getVersion() 120 | { 121 | return $this->version; 122 | } 123 | 124 | /** 125 | * Function to add any auth details to 126 | * @param $key 127 | * @param $value 128 | * @param string $type 129 | * @return mixed|void 130 | */ 131 | public function auth($key, $value, $type = 'basic') 132 | { 133 | switch($type) { 134 | case "header": 135 | $this->args['headers'][$key] = $value; 136 | break; 137 | case 'basic': 138 | default: 139 | $this->args['auth'] = array($key, $value); 140 | break; 141 | } 142 | } 143 | 144 | /** 145 | * Function to set the HTTP method needed for thr request. 146 | * @param $method 147 | * @return mixed|void 148 | */ 149 | public function setHTTPMethod($method) 150 | { 151 | $this->httpMethod = strtoupper($method); 152 | } 153 | 154 | /** 155 | * Magic __get function for setting the HTTP method and API path 156 | * @param string $name 157 | * @return $this|mixed 158 | */ 159 | public function __get($name) 160 | { 161 | switch (strtolower($name)) { 162 | case 'get': 163 | $this->setHTTPMethod('GET'); 164 | break; 165 | case 'post': 166 | $this->setHTTPMethod('POST'); 167 | break; 168 | case 'put': 169 | $this->setHTTPMethod('PUT'); 170 | break; 171 | case 'delete': 172 | $this->setHTTPMethod('DELETE'); 173 | break; 174 | default: 175 | $this->request->addPathElement($name); 176 | } 177 | return $this; 178 | } 179 | 180 | /** 181 | * Occasionally we need to force a request, for example to the base domain. 182 | * @param string $name 183 | * @param array $arguments 184 | * @return mixed 185 | */ 186 | public function makeDirectRequest(string $name = '', array $arguments = []) 187 | { 188 | return $this->__call($name, $arguments); 189 | } 190 | 191 | /** 192 | * Magic __call method, will translate all function calls to object to API requests 193 | * @param $name - name of the function 194 | * @param $arguments - an array of arguments 195 | * @return mixed 196 | */ 197 | public function __call($name, $arguments) 198 | { 199 | $client = new GuzzleHttp\Client(); 200 | $query = count($arguments) < 1 || !is_array($arguments) ? array() : $arguments[0]; 201 | //Allow endpoints starting with an integer to be prepended with an underscore to make them valid method calls for PHP. 202 | if (strpos($name, '_') === 0) { 203 | $name = strlen($name) > 1 && is_numeric($name[1]) ? ltrim($name, '_') : $name; 204 | } 205 | $this->request->addEndpoint($name); 206 | $this->processArgs($query); 207 | //clear the response object from last call. 208 | response::resetData(); 209 | $response = response::getInstance(); 210 | $response::verbUsed($this->httpMethod); //set the verb used for the request, 211 | //do we have a logging class? If so, add data to it. 212 | if ($this->logger !== null) { 213 | $this->logger->addMethod($this->httpMethod); 214 | $this->logger->addRequestURL((string)$this->request); 215 | $this->logger->addRequestArgs(json_encode($this->args)); 216 | $this->logger->addRequestEndpoint($name); 217 | $this->logger->setRequestTime(time()); 218 | } 219 | try { 220 | $result = $client->request($this->httpMethod, (string)$this->request, $this->args); 221 | //$this->processResult($result); 222 | $response::processResult($result); 223 | } catch (GuzzleHttp\Exception\GuzzleException $e) { 224 | $response::addError($e->getCode(), $e->getMessage()); 225 | } 226 | 227 | if ($this->logger !== null) { 228 | $this->logger->setResponseTime(time()); 229 | if (!empty($error = $response::getError())) { 230 | $this->logger->addRawResponse($error['message']); 231 | $this->logger->addResponseStatusCode($error['code']); 232 | } else { 233 | $this->logger->addRawResponse($response->rawBodyData); 234 | $this->logger->addResponseStatusCode($response->statusCode); 235 | } 236 | } 237 | 238 | //reset some stuff post-query so we can handle the next one cleanly. Leave auth and headers in place by default. 239 | $this->request->resetQueryString(); 240 | unset($this->args['query']); 241 | unset($this->args[$this->bodyFormat]); 242 | 243 | return $response; 244 | } 245 | 246 | /** 247 | * Function to get the last result returned by an API call. 248 | * @return mixed 249 | */ 250 | public function getLastResult() 251 | { 252 | return $this->lastResult; 253 | } 254 | 255 | /** 256 | * Function to return the last endpoint we called. 257 | * @return mixed 258 | */ 259 | public function getLastCall() 260 | { 261 | return $this->request->getEndpoint(); 262 | } 263 | 264 | /** 265 | * Getter for the errors array 266 | * @return array 267 | */ 268 | public function getErrors() 269 | { 270 | return $this->errors; 271 | } 272 | 273 | /** 274 | * Function to reset the path in the api request 275 | */ 276 | public function resetPath() 277 | { 278 | $this->request->resetPath(); 279 | } 280 | 281 | public function addCurlOpts($opts) 282 | { 283 | $this->args['config']['curl'] = array_merge($this->args['config']['curl'], $opts); 284 | } 285 | 286 | /** 287 | * Function to set some default cURL arguments, such as SSL version. 288 | */ 289 | private function setDefaultCurlOpts() 290 | { 291 | $this->args['config'] = 292 | array( 293 | 'curl' => array( 294 | 'CURLOPT_SSLVERSION' => 6, 295 | ) 296 | ); 297 | } 298 | 299 | /** 300 | * Function to process the arguments passed depending on selected http method. 301 | * @param array $args 302 | * @return self 303 | */ 304 | private function processArgs($args) 305 | { 306 | if (!empty($args)) { 307 | switch ($this->httpMethod) { 308 | case 'DELETE': 309 | case 'GET': 310 | $this->args['query'] = $args; 311 | break; 312 | case 'PUT': 313 | case 'POST': 314 | $this->args[$this->bodyFormat] = $args; 315 | break; 316 | } 317 | } 318 | return $this; 319 | } 320 | } 321 | --------------------------------------------------------------------------------