├── LICENSE.txt ├── README.md ├── bin └── phargen.php ├── class ├── APCCache.php ├── APIException.php ├── AbstractCache.php ├── CacheInterface.php ├── Client.php ├── Collection.php ├── CurlRequest.php ├── CustomException.php ├── Error.php ├── RequestInterface.php ├── Resource.php ├── Type.php └── VariableCache.php ├── init.php └── tests └── filters.php /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Go Daddy Operating Company, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gdapi-php 2 | ========= 3 | 4 | A PHP client for Go Daddy® REST APIs. 5 | 6 | Requirements 7 | --------- 8 | * PHP 5.3 or greater 9 | * [libcurl](http://us3.php.net/curl) PHP extension with SSL support 10 | * An account in a compatible service, such as [Cloud Servers™](http://www.godaddy.com/hosting/cloud-computing.aspx) 11 | * Your API Access and Secret key pair 12 | 13 | Getting Started 14 | -------- 15 | If you haven't already tried it, open up the base URL of the API you want to use in a browser. 16 | Enter your Access Key as the username and your Secret Key as the password. 17 | This interface will allow you to get familiar with what Resource types are available in the API 18 | and what operations and actions you can perform on them. For more information, see the [documentation site](http://docs.cloud.secureserver.net/). 19 | 20 | Setting Up 21 | --------- 22 | A PHAR archive of the latest stable version is available over in the [Downloads page](https://github.com/godaddy/gdapi-php/downloads). We recommend you use this rather than the source code from the repo directly. 23 | 24 | ### Using the PHAR Archive 25 | Download the .phar file: 26 | > curl -O https://github.com/downloads/godaddy/gdapi-php/gdapi.phar 27 | 28 | Create a client: 29 | ```php 30 | 39 | ``` 40 | 41 | ### Using the source 42 | To use the source code instead, clone it: 43 | > git clone https://github.com/godaddy/gdapi-php.git 44 | 45 | Create a client: 46 | ```php 47 | 56 | ``` 57 | 58 | ### Problems connecting 59 | Consult the [SSL Problems](#ssl-problems) section if you get an error when creating the client that says something like this: 60 | > SSL certificate problem, verify that the CA cert is OK. 61 | > Details: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed 62 | 63 | Finding Resources 64 | -------- 65 | Each resource type in the API is available as a member of the Client. 66 | 67 | ### Listing all resources in a collection 68 | ```php 69 | virtualmachine->query(); 71 | echo "There are " . count($machines) . " machines:\n"; 72 | foreach ( $machines as $machine ) 73 | { 74 | echo $machine->getName() . "\n"; 75 | } 76 | ?> 77 | ``` 78 | 79 | ### Filtering 80 | Filters allow you to search a collection for resources matching a set of conditions. 81 | ```php 82 | loadbalancers->query(array( 84 | 'publicStartPort' => 80, 85 | )); 86 | 87 | $privileged_portforwards = $client->portforwards->query(array( 88 | 'publicStartPort' => array('modifier' => 'lt', 'value' => 1024) 89 | )); 90 | 91 | $active_machines = $client->virtualmachine->query(array( 92 | 'removed' => array('modifier' => 'null') 93 | )); 94 | 95 | $complicated = $client->portforwards->query(array( 96 | 'name' => 'asdf', 97 | 'removed' => array('modifier' => 'null'), 98 | 'publicStartPort' => array( 99 | array('modifier' => 'gt', 'value' => 1024) 100 | array('modifier' => 'lt', 'value' => 2048) 101 | ), 102 | )); 103 | ?> 104 | ``` 105 | 106 | ### Getting a single Resource by ID 107 | If you know the ID of the resource you are looking for already, you can also get it directly. 108 | ```php 109 | virtualmachine->getById('your-machine-id'); 111 | ?> 112 | ``` 113 | 114 | Working with Resources 115 | -------- 116 | 117 | ### Accessing attributes 118 | Resources have a getter method for each attribute, as "get"+{attribute name}. Attributes that can be changed also have a "set"+{attribute name} method. The first character of the attribute name may be capitalized for readability, but the rest of the name must match the API. 119 | 120 | ```php 121 | virtualmachine->getById('your-machine-id'); 123 | 124 | $privateIp = $machine->getPrivateIpv4Address(); // e.g. '10.1.1.3' 125 | $size = $machine->getRamSizeMb(); // e.g. 1024 126 | ?> 127 | ``` 128 | 129 | ### Making changes 130 | ```php 131 | virtualmachine->getById('your-machine-id'); 133 | $machine->setName('bigger machine'); 134 | $machine->setOffering('2gb-4cpu'); 135 | 136 | // Save the changes 137 | $result = $machine->save(); 138 | ?> 139 | ``` 140 | 141 | ### Creating new resources 142 | ```php 143 | network->create(array( 145 | 'name' => 'My Network', 146 | 'domain' => 'mynetwork.local', 147 | 'ipv4Cidr' => '192.168.0.0/24' 148 | )); 149 | ?> 150 | ``` 151 | 152 | ### Removing resources 153 | 154 | With an instance of the resource: 155 | ```php 156 | virtualmachine->getById('your-machine-id'); 158 | $result = $machine->remove(); 159 | ?> 160 | ``` 161 | 162 | Or statically: 163 | ```php 164 | virtualmachine->remove('your-machine-id'); 166 | ?> 167 | ``` 168 | 169 | ### Executing Actions 170 | Actions are used to perform operations that go beyond simple create/read/update/delete. Resources have a "do"+{action name} method for each action. The first character of the action name may be capitalized for readability, but the rest of the name must match the API. 171 | 172 | ```php 173 | virtualmachine->getById('your-machine-id'); 175 | $result = $machine->doRestart(); 176 | ?> 177 | ``` 178 | 179 | Following Links 180 | -------- 181 | Response collections and resources generally have a "links" attribute containing URLs to related resources and collections. For example a virtual machine belongs to a network and has one or more volumes. Resources have a "fetch"+{link name} method for each link. Invoking this will return the linked resource 182 | ```php 183 | virtualmachine->getById('your-machine-id'); 185 | $network = $machine->fetchNetwork(); // Network resource 186 | $volumes = $machine->fetchVolumes(); // Collection of Volume resources 187 | ?> 188 | ``` 189 | 190 | Handling Errors 191 | -------- 192 | By default, any error response will be thrown as an exception. 193 | The most general type of exception is \GDAPI\APIException, but several more specific types are defined in class/APIException.php. 194 | ```php 195 | virtualmachine->getById('your-machine-id'); 199 | echo "I found it"; 200 | } 201 | catch ( \GDAPI\NotFoundException $e ) 202 | { 203 | echo "I couldn't find that machine"; 204 | } 205 | catch ( \GDAPI\APIException $e ) 206 | { 207 | echo "Something else went wrong"; 208 | } 209 | ?> 210 | ``` 211 | It is important that you put the catch blocks from most-specific to least-specfic classes. 212 | PHP runs through your catch statements in order and only calls the first one that matches the exception. 213 | If you flipped the two catches above, "Something else went wrong" would be printed for any exception, even if it was a NotFoundException. 214 | 215 | If you prefer to not use exceptions, you can disable them when creating the Client. 216 | When an error occurs, instead of throwing an exception the response returned will be an instance of \GDAPI\Error. 217 | ```php 218 | false 221 | ); 222 | 223 | $client = new \GDAPI\Client($url, $access_key, $secret_key, $options); 224 | 225 | $result = $client->virtualmachine->getById('your-machine-id'); 226 | if ( $result instanceof \GDAPI\Error ) 227 | { 228 | if ( $result->getStatus() == 404 ) 229 | { 230 | echo "I couldn't find that machine"; 231 | } 232 | else 233 | { 234 | echo "Something else went wrong: " . print_r($result,true); 235 | } 236 | } 237 | else 238 | { 239 | echo "I found it"; 240 | } 241 | ?> 242 | ``` 243 | 244 | Advanced Options 245 | -------- 246 | ### Mapping response objects to your own classes 247 | By default all response objects are an instance of \GDAPI\Resource, \GDAPI\Collection, or \GDAPI\Error. In many cases it is useful to map responses to your own classes and add your own behavior to them. 248 | ```php 249 | fetchNetwork(); 256 | return $this->getName() . "." . $network->getDomain(); 257 | } 258 | } 259 | 260 | class MyLoadBalancer extends \GDAPI\Resource 261 | { 262 | function getFQDN() 263 | { 264 | $network = $this->fetchNetwork(); 265 | return $this->getName() . "." . $network->getDomain(); 266 | } 267 | } 268 | 269 | $classmap = array( 270 | 'virtualmachine' => 'MyVM', 271 | 'loadbalancer' => 'MyLoadBalancer' 272 | ); 273 | 274 | $options = array( 275 | 'classmap' => $classmap 276 | ); 277 | 278 | $client = new \GDAPI\Client($url, $access_key, $secret_key, $options); 279 | 280 | $machines = $client->virtualmachine->query(); 281 | echo "There are " . count($machines) . " machines:\n"; 282 | foreach ( $machines as $machine ) 283 | { 284 | echo $machine->getFQDN() ."\n"; 285 | } 286 | ?> 287 | ``` 288 | 289 | SSL Problems 290 | -------- 291 | Some installations of libcurl do not come with certificates for any Certificate Authorities (CA). This client always verifies the certificate by default, but having no CA certificates means it won't be able to verify any SSL certificate. To fix this problem, you need a list of CA certificates to trust. Curl provides a copy that contains the same CA certs as Mozilla browsers: [cacert.pem](http://curl.haxx.se/ca/cacert.pem). 292 | 293 | If you have permission to edit your php.ini, you can fix this globally for anything that uses the libcurl extension: 294 | 295 | Add a line like this to your php.ini, then restart your web server, if applicable: 296 | > curl.cainfo = /path/to/cacert.pem 297 | 298 | If you don't have permission, or don't want to make a global change, you can configure just the GDAPI client to use the file: 299 | ```php 300 | '/path/to/cacert.pem' 303 | ); 304 | 305 | $client = new \GDAPI\Client($url, $access_key, $secret_key, $options); 306 | ?> 307 | ``` 308 | 309 | ### More options 310 | For info on other options that are available, see the $defaults array in [class/Client.php](https://github.com/godaddy/gdapi-php/blob/master/class/Client.php#L44). 311 | -------------------------------------------------------------------------------- /bin/phargen.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -d phar.readonly=0 2 | isFile()) { 48 | 49 | // add to the archive, not stripping whitespace or anything for now 50 | $oPhar->addFromString($file->getFilename(), file_get_contents($file)); 51 | 52 | } 53 | } 54 | 55 | // create the stub from the init file 56 | $stub = str_replace( 57 | "('class/", 58 | "('phar://$pharName/", 59 | file_get_contents($initPath . '/init.php') 60 | ) . " __HALT_COMPILER(); ?>"; 61 | 62 | // set the stub, and all done! 63 | $oPhar->setStub($stub); 64 | 65 | require_once('../class/Client.php'); 66 | $pharVersionedName = str_replace("VERSION", Client::VERSION , $pharVersionedName); 67 | copy($pharName, $pharVersionedName); 68 | -------------------------------------------------------------------------------- /class/APCCache.php: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /class/APIException.php: -------------------------------------------------------------------------------- 1 | response = $response; 41 | } 42 | else if ( $response ) 43 | { 44 | $this->response = json_decode($response,true); 45 | } 46 | } 47 | } 48 | 49 | public function __toString() 50 | { 51 | $msg = get_class($this) . " '{$this->message}' in {$this->file}({$this->line})\n"; 52 | 53 | if ( $this->response && $this->response instanceof Error ) 54 | { 55 | $res = $this->response; 56 | 57 | if ( $res->metaIsSet('status') ) 58 | { 59 | $msg .= "HTTP Status: " . $res->getStatus() . "\n"; 60 | } 61 | 62 | if ( $res->metaIsSet('code') ) 63 | { 64 | $msg .= "Code: " . $res->getCode() . "\n"; 65 | } 66 | 67 | if ( $res->metaIsSet('message') ) 68 | { 69 | $msg .= "Message: " . $res->getMessage() . "\n"; 70 | } 71 | 72 | if ( $res->metaIsSet('detail') ) 73 | { 74 | $msg .= "Detail: " . $res->getDetail() . "\n"; 75 | } 76 | } 77 | 78 | $msg .= "{$this->getTraceAsString()}"; 79 | return $msg; 80 | } 81 | 82 | public function getResponse() 83 | { 84 | return $this->response; 85 | } 86 | } 87 | 88 | class ClientException extends APIException {}; 89 | class SchemaException extends APIException {}; 90 | class HTTPRequestException extends APIException {}; 91 | class ParseException extends APIException {}; 92 | class UnknownTypeException extends APIException {}; 93 | 94 | class BadRequestException extends APIException {}; 95 | class UnauthorizedException extends APIException {}; 96 | class ForbiddenException extends APIException {}; 97 | class NotFoundException extends APIException {}; 98 | class MethodException extends APIException {}; 99 | class NotAcceptableException extends APIException {}; 100 | class ConflictException extends APIException {}; 101 | class ServiceException extends APIException {}; 102 | class UnavailableException extends APIException {}; 103 | 104 | APIException::$status_map = array( 105 | 'schema' => '\GDAPI\SchemaException', 106 | 'client' => '\GDAPI\ClientException', 107 | 'request' => '\GDAPI\HTTPRequestException', 108 | 'parse' => '\GDAPI\ParseException', 109 | 'type' => '\GDAPI\UnknownTypeException', 110 | 111 | 400 => '\GDAPI\BadRequestException', 112 | 401 => '\GDAPI\UnauthorizedException', 113 | 403 => '\GDAPI\ForbiddenException', 114 | 404 => '\GDAPI\NotFoundException', 115 | 405 => '\GDAPI\MethodException', 116 | 406 => '\GDAPI\NotAcceptableException', 117 | 409 => '\GDAPI\ConflictException', 118 | 500 => '\GDAPI\ServiceException', 119 | 503 => '\GDAPI\UnavailableException' 120 | ); 121 | 122 | 123 | ?> 124 | -------------------------------------------------------------------------------- /class/AbstractCache.php: -------------------------------------------------------------------------------- 1 | 65 | -------------------------------------------------------------------------------- /class/CacheInterface.php: -------------------------------------------------------------------------------- 1 | 83 | -------------------------------------------------------------------------------- /class/Client.php: -------------------------------------------------------------------------------- 1 | array( 52 | 'collection' => '\GDAPI\Collection', 53 | 'error' => '\GDAPI\Error', 54 | ), 55 | 56 | /* 57 | * Behavior when errors occur: 58 | * true: An APIException (or subclass) will be thrown on errors. 59 | * false: A response will be returned as an instance of the 'error' type defined in the classmap. 60 | */ 61 | 'throw_exceptions' => true, 62 | 63 | /* 64 | * Optional prefix to add to cache keys 65 | */ 66 | 'cache_namespace' => '', 67 | 68 | /* --------------------* 69 | * Less common options * 70 | * ------------------- */ 71 | 72 | /* 73 | * Responses will be returned as instances of this class by default 74 | */ 75 | 'default_class' => '\GDAPI\Resource', 76 | 77 | /* 78 | * HTTP client class to use. Clients must implement the RequestInterface interface 79 | */ 80 | 'request_class' => '\GDAPI\CurlRequest', 81 | 82 | /* 83 | * If set, the schema definition will be loaded from this file path instead of the base URL. 84 | */ 85 | 'schema_file' => '', 86 | 87 | /* 88 | * If set, responses will be written directly to stdout/the browser 89 | */ 90 | 'stream_output' => false, 91 | 92 | /* 93 | * Verify SSL certificate when connecting 94 | */ 95 | 'verify_ssl' => true, 96 | 97 | /* 98 | * Path to a PEM file containing certificate authorities that are trusted 99 | */ 100 | 'ca_cert' => '', 101 | 102 | /* 103 | * Path to a directory containing certificate authorities that are trusted 104 | */ 105 | 'ca_path' => '', 106 | 107 | /* 108 | * Timeout for establishing an initial connection to the API, in seconds. 109 | * cURL >= 7.16.2 support floating-point values with millisecond resolution. 110 | */ 111 | 'connect_timeout' => 5, 112 | 113 | /* 114 | * Timeout for receiving a response, in seconds. 115 | * cURL >= 7.16.2 support floating-point values with millisecond resolution. 116 | */ 117 | 'response_timeout' => 300, 118 | 119 | /* 120 | * Time to keep HTTP connections open, in seconds. 121 | */ 122 | 'keep_alive' => 120, 123 | 124 | /* 125 | * Enable GZIP compression of responses. 126 | */ 127 | 'compress' => true, 128 | 129 | /* 130 | * Network interface name, IP address, or hotname to use for HTTP requests 131 | */ 132 | 'interface' => '', 133 | 134 | /* 135 | * Name => Value mapping or HTTP headers to send with every request. 136 | */ 137 | 'headers' => array( 138 | 'Accept' => self::MIME_TYPE_JSON, 139 | ), 140 | 141 | /* -------------------------* 142 | * Even less common options * 143 | * ------------------------ */ 144 | 145 | /* 146 | * Follow HTTP redirect responses 147 | */ 148 | 'follow_redirects' => true, 149 | 150 | /* 151 | * Limit to how many HTTP redirects will be followed before giving up. 152 | */ 153 | 'max_redirects' => 10, 154 | 155 | /* 156 | * The attribute to look at to determine the class a response should be mapped to. 157 | */ 158 | 'type_attr' => 'type', 159 | 160 | /* 161 | * The maximum depth of a JSON object when parsing 162 | */ 163 | 'json_depth_limit' => 50, 164 | 165 | /* 166 | * Path to a PEM file containing a client certificate to use for requests. 167 | */ 168 | 'client_cert' => '', 169 | 170 | /* 171 | * Path to a PEM file containing the key for the client certificate. 172 | * The cert and key may be in the same file, if desired. 173 | */ 174 | 'client_cert_key' => '', 175 | 176 | /* 177 | * Password for the certificate key, if needed. 178 | * Can be provided as a string, or an anonymous function that returns a string. 179 | */ 180 | 'client_cert_pass' => '', 181 | ); 182 | 183 | /* 184 | * An array of active clients. 185 | * Only one instance per [base_url,access_key,secret_key] combination will be created. 186 | */ 187 | static $clients = array(); 188 | 189 | /* 190 | * A unique identifier for the [base_url,access_key,secret_key] combination 191 | */ 192 | protected $id; 193 | 194 | /* 195 | * The base URL for the API. Should include a version. 196 | */ 197 | protected $base_url; 198 | 199 | /* 200 | * Options for this client 201 | */ 202 | protected $options; 203 | 204 | /* 205 | * The schema loaded for this API 206 | */ 207 | protected $schemas; 208 | 209 | /* 210 | * An instance of a class that implements RequestInterface, to make REST requests. 211 | */ 212 | protected $requestor; 213 | 214 | /* 215 | * Create a new Client. 216 | * 217 | * @param string $base_url: The root URL for the API you want to connect to. This should include version. 218 | * @param mixed $access_key: The access key for your API user, or an anonymous function that returns it. 219 | * @param mixed $secret_key: The secret key for your API user, or an anonymous function that returns it. 220 | * @param array $options: A hash map of options to override the default options (see static::$defaults). 221 | * 222 | * @returns \GDAPI\Client 223 | */ 224 | public function __construct($base_url, $access_key, $secret_key, $options=array()) 225 | { $this->id = md5($base_url); 226 | $this->base_url = $base_url; 227 | $this->options = array_replace_recursive(static::$defaults, $options); 228 | 229 | $request_class = $this->options['request_class']; 230 | $this->requestor = new $request_class($this, $this->base_url); 231 | 232 | if ( $access_key && $secret_key ) 233 | { 234 | $this->requestor->setAuth(static::resolvePassword($access_key), static::resolvePassword($secret_key)); 235 | } 236 | 237 | $this->loadSchemas(); 238 | 239 | static::$clients[$this->id] = $this; 240 | } 241 | 242 | /* 243 | * Destruct 244 | */ 245 | public function __destruct() 246 | { 247 | unset(static::$clients[$this->id]); 248 | } 249 | 250 | /* 251 | * Set a single option 252 | * @param string $name Option name 253 | * @param string $value Option value 254 | */ 255 | public function setOption($name,$value) 256 | { 257 | $this->options[$name] = $value; 258 | } 259 | 260 | /* 261 | * Set multiple options 262 | * @param array $opts name => value map of options 263 | */ 264 | public function setOptions($opts) 265 | { 266 | foreach ( $opts as $k => $v ) 267 | { 268 | $this->options[$name] = $value; 269 | } 270 | } 271 | 272 | public function getOptions() 273 | { 274 | return $this->options; 275 | } 276 | 277 | /* 278 | * Returns a password from a (possible) function that returns one 279 | * 280 | * @param string $str_or_fn A password string, or a function that returns one 281 | * 282 | * @returns string Password 283 | */ 284 | static function resolvePassword($str_or_fn) 285 | { 286 | if ( is_string($str_or_fn) ) 287 | { 288 | return $str_or_fn; 289 | } 290 | else 291 | { 292 | return $str_or_fn(); 293 | } 294 | } 295 | 296 | /* 297 | * Get an instance of a client by id 298 | * 299 | * @param string $id: The client id 300 | * 301 | * @return object Client 302 | */ 303 | static function &get($id) 304 | { 305 | if ( isset(static::$clients[$id]) ) 306 | { 307 | return static::$clients[$id]; 308 | } 309 | 310 | return null; 311 | } 312 | 313 | /* 314 | * Set the caching implementation 315 | * 316 | * @param object $class The caching object to use 317 | */ 318 | static function setCache($class) 319 | { 320 | static::$cache = $class; 321 | } 322 | 323 | /* 324 | * Load a schema and create Type objects 325 | * 326 | * @param string $path The path for the schema, relative to base_url 327 | */ 328 | protected function loadSchemas($path='/') 329 | { 330 | $res = false; 331 | $ns = $this->options['cache_namespace']; 332 | $cache_key = 'restclient_'. ($ns ? $ns.'_' : '') . $this->id .'_'. $path; 333 | $cache = static::$cache; 334 | 335 | // Fake a schema response with a file, if given 336 | if ( $this->options['schema_file'] ) 337 | { 338 | $res = new Resource($this->id, json_decode(file_get_contents($this->options['schema_file']))); 339 | } 340 | 341 | // Try the cache 342 | if ( $cache && !$res ) 343 | { 344 | $res = $cache::get($cache_key); 345 | } 346 | 347 | // Make a request for it 348 | if ( !$res ) 349 | { 350 | $res = $this->requestor->request('GET', $path); 351 | } 352 | 353 | if ( !$res ) 354 | { 355 | return $this->error('Unable to load API schema', 'schema'); 356 | } 357 | 358 | if ( strtolower($res->getType()) == 'apiversion' && $res->getLink('schemas') ) 359 | { 360 | // The response was the root of a version, with a link to the schemas 361 | return $this->loadSchemas($res->getLink('schemas')); 362 | } 363 | elseif ( $res instanceof Collection && $res[0] && strtolower($res[0]->getType()) == 'apiversion' ) 364 | { 365 | // The response was an API root with no version 366 | return $this->error('The base URL "'. $this->base_url.$path .'" does not specify an API version to use', 'schema'); 367 | } 368 | else if ( $res instanceof Collection && $res[0] && $res[0]->getType() == 'schema' ) 369 | { 370 | // The response was a list of schemas 371 | 372 | $this->base_url = $res->getLink('root'); 373 | 374 | // Setup all the types 375 | $this->types = array(); 376 | for ( $i = 0 ; $i < count($res) ; $i++ ) 377 | { 378 | $schema = $res[$i]; 379 | $this->types[ $schema->getId() ] = new Type($this->id, $schema); 380 | } 381 | 382 | if ( $cache ) 383 | { 384 | $cache::set($cache_key, $res); 385 | } 386 | } 387 | else 388 | { 389 | // No idea what the response is 390 | return $this->error('The base URL "'. $this->base_url.$path .'" does not look like an API version','schema'); 391 | } 392 | } 393 | 394 | /* 395 | * Magic method to get a type 396 | * 397 | * @param string $name The type name 398 | * 399 | * @return object Type 400 | */ 401 | public function __get($name) 402 | { 403 | if ( !isset($this->types, $this->types[$name]) ) 404 | { 405 | return $this->error('There is no type for "'. $name . '" defined in the schema', 'type'); 406 | } 407 | 408 | return $this->types[$name]; 409 | } 410 | 411 | /* 412 | * Get all the types defined. 413 | * 414 | * @return array Types 415 | */ 416 | public function getTypes() 417 | { 418 | return $this->types; 419 | } 420 | 421 | /* 422 | * Perform a request 423 | * 424 | * @see RequestInterface 425 | */ 426 | public function request($method, $path, $qs=array(), $body=null, $content_type=false) 427 | { 428 | $requestor = $this->getRequestor(); 429 | return $requestor->request($method,$path,$qs,$body,$content_type); 430 | } 431 | 432 | /* 433 | * Gets the requestor object 434 | * 435 | * @return object Requestor 436 | */ 437 | public function getRequestor() 438 | { 439 | return $this->requestor; 440 | } 441 | 442 | /* 443 | * Convert responses into the appropriate classes 444 | * 445 | * @param mixed $data The response data to be converted 446 | * 447 | * @return object Type 448 | */ 449 | public function classify($data) 450 | { 451 | return $this->classifyRecursive($data,0,'auto'); 452 | } 453 | 454 | /* 455 | * Convert responses into the appropriate classes 456 | * 457 | * @param mixed $data The response data to be converted 458 | * @param int $depth The recursion depth; do not set directly. 459 | * @param string $depth What type to treat the data as; do not set directly. 460 | * 461 | * @return object Type 462 | */ 463 | protected function classifyRecursive($data, $depth=0, $as='auto') 464 | { 465 | if ( $as === 'auto' ) 466 | { 467 | if ( is_object($data) ) 468 | { 469 | $as = 'object'; 470 | } 471 | elseif ( is_array($data) ) 472 | { 473 | $as = 'array'; 474 | } 475 | else 476 | { 477 | $as = 'scalar'; 478 | } 479 | } 480 | 481 | if ( $as === 'object' ) 482 | { 483 | $type = null; 484 | $type_attr = $this->options['type_attr']; 485 | if ( isset($data->{$type_attr}) ) 486 | { 487 | $type = $data->{$type_attr}; 488 | if ( isset( $this->options['classmap'][$type] ) ) 489 | { 490 | $type = $this->options['classmap'][$type]; 491 | } 492 | else 493 | { 494 | $type = null; 495 | } 496 | } 497 | 498 | if ( $type === null ) 499 | { 500 | $type = $this->options['default_class']; 501 | } 502 | 503 | // Classify links, data, etc 504 | foreach ( $data as $k => &$v) 505 | { 506 | if ( is_array($v) ) 507 | { 508 | $v = $this->classify($v, $depth+1, 'array'); 509 | } 510 | elseif ( is_object($v) && isset($v->{$type_attr},$data->links) && array_key_exists($k,$data->links) ) 511 | { 512 | $v = $this->classify($v, $depth+1, 'object'); 513 | } 514 | } 515 | 516 | $out = new $type($this->id, $data); 517 | } 518 | elseif ( $as === 'array' ) 519 | { 520 | $out = array(); 521 | foreach ( $data as $k => &$v ) 522 | { 523 | $out[$k] = $this->classifyRecursive($v,$depth+1); 524 | } 525 | } 526 | else 527 | { 528 | $out = $data; 529 | } 530 | 531 | return $out; 532 | } 533 | 534 | /* 535 | * Throw errors 536 | * 537 | * @param string $message The error message 538 | * @param string $status The error code or HTTP status 539 | * @param array $body The response or error details 540 | * 541 | * @throws APIException 542 | */ 543 | public function error($message, $status, $body=array()) 544 | { 545 | if ( $this->options['throw_exceptions'] ) 546 | { 547 | $class = '\GDAPI\APIException'; 548 | 549 | if ( isset(APIException::$status_map[$status]) ) 550 | { 551 | $class = APIException::$status_map[$status]; 552 | } 553 | 554 | if ( !is_int($status) ) 555 | { 556 | $status = -1; 557 | } 558 | 559 | $err = new $class($message, $status, $body); 560 | throw $err; 561 | } 562 | else 563 | { 564 | return $body; 565 | } 566 | } 567 | 568 | /* 569 | * Get metadata about the last request & response 570 | * 571 | * @returns array 572 | */ 573 | public function getMeta() 574 | { 575 | return $this->requestor->getMeta(); 576 | } 577 | } 578 | 579 | ?> 580 | -------------------------------------------------------------------------------- /class/Collection.php: -------------------------------------------------------------------------------- 1 | data ) 34 | { 35 | $this->data = $body->data; 36 | unset($body->data); 37 | } 38 | 39 | parent::__construct($clientId,$body); 40 | } 41 | 42 | protected function schemaField($name) 43 | { 44 | $type_name = $this->getType(); 45 | $type = $this->getClient()->{$type_name}; 46 | 47 | if ( !$type ) 48 | { 49 | return null; 50 | } 51 | 52 | $field = $type->collectionField($name); 53 | return $field; 54 | } 55 | 56 | /* ArrayAccess */ 57 | public function offsetExists($offset) 58 | { 59 | return isset($this->data[$offset]); 60 | } 61 | 62 | public function offsetGet($offset) 63 | { 64 | if ( isset($this->data[$offset]) ) 65 | return $this->data[$offset]; 66 | 67 | return null; 68 | } 69 | 70 | public function offsetSet($offset, $value) 71 | { 72 | if ( is_null($offset) ) 73 | { 74 | $this->data[] = $value; 75 | } 76 | else 77 | { 78 | $this->data[$offset] = $value; 79 | } 80 | } 81 | 82 | public function offsetUnset($offset) 83 | { 84 | unset($this->data[$offset]); 85 | } 86 | /* End: ArrayAccess */ 87 | 88 | /* Iterator */ 89 | public function current() 90 | { 91 | return $this->data[$this->pos]; 92 | } 93 | 94 | public function key() 95 | { 96 | return $this->pos; 97 | } 98 | 99 | public function next() 100 | { 101 | $this->pos++; 102 | } 103 | 104 | public function rewind() 105 | { 106 | $this->pos = 0; 107 | } 108 | 109 | public function valid() 110 | { 111 | return isset($this->data[$this->pos]); 112 | } 113 | /* End: Iterator */ 114 | 115 | /* Countable */ 116 | public function count() 117 | { 118 | return count($this->data); 119 | } 120 | /* End: Countable */ 121 | 122 | /* Operations */ 123 | public function create($obj) 124 | { 125 | $data = ( $obj instanceof Resource ? $obj->getMeta() : $obj ); 126 | $url = $this->getLink('self'); 127 | $client = $this->getClient(); 128 | return $client->request('POST', $url, array(), $data, Client::MIME_TYPE_JSON); 129 | } 130 | 131 | public function remove($id_or_obj) 132 | { 133 | $id = ( $id_or_obj instanceof Resource ? $id_or_obj->getId() : $id_or_obj ); 134 | $url = $this->getLink('self').'/'. urlencode($id); 135 | $client = $this->getClient(); 136 | return $client->request('DELETE', $url); 137 | } 138 | /* End: Operations */ 139 | } 140 | -------------------------------------------------------------------------------- /class/CurlRequest.php: -------------------------------------------------------------------------------- 1 | client = $client; 42 | $this->curl = curl_init(); 43 | $this->base_url = $base_url; 44 | $this->applyOptions(); 45 | } 46 | 47 | protected function applyOptions() 48 | { 49 | $o = $this->client->getOptions(); 50 | 51 | $curl_opt = array( 52 | CURLOPT_USERAGENT => static::getUserAgent(), 53 | CURLOPT_SSL_VERIFYPEER => ($o['verify_ssl'] !== false ), 54 | CURLOPT_SSL_VERIFYHOST => ($o['verify_ssl'] !== false ), 55 | CURLOPT_FOLLOWLOCATION => ($o['follow_redirects'] !== false ), 56 | CURLOPT_MAXREDIRS => $o['max_redirects'], 57 | ); 58 | 59 | if ( $o['ca_cert'] ) 60 | { 61 | $curl_opt[CURLOPT_CAINFO] = $o['ca_cert']; 62 | } 63 | 64 | if ( $o['ca_path'] ) 65 | { 66 | $curl_opt[CURLOPT_CAPATH] = $o['ca_path']; 67 | } 68 | 69 | if ( static::supportsMSTimeouts() ) 70 | { 71 | $curl_opt[CURLOPT_TIMEOUT_MS] = ceil($o['response_timeout']*1000); 72 | $curl_opt[CURLOPT_CONNECTTIMEOUT_MS] = ceil($o['connect_timeout']*1000); 73 | } 74 | else 75 | { 76 | $curl_opt[CURLOPT_TIMEOUT] = ceil($o['response_timeout']); 77 | $curl_opt[CURLOPT_CONNECTTIMEOUT] = ceil($o['connect_timeout']); 78 | } 79 | 80 | if ( $o['compress'] !== false ) 81 | { 82 | // Empty string sends all supported encodings 83 | $curl_opt[CURLOPT_ENCODING] = ''; 84 | } 85 | 86 | if ( $o['interface'] ) 87 | { 88 | $curl_opt[CURLOPT_INTERFACE] = $o['interface']; 89 | } 90 | 91 | if ( $o['client_cert'] ) 92 | { 93 | $curl_opt[CURLOPT_SSLCERT] = $o['client_cert']; 94 | 95 | if ( $o['client_cert_key'] ) 96 | { 97 | $curl_opt[CURLOPT_SSLKEY] = $o['client_cert_key']; 98 | } 99 | 100 | if ( $o['client_cert_pass'] ) 101 | { 102 | $curl_opt[CURLOPT_SSLKEYPASSWD] = $o['client_cert_pass']; 103 | } 104 | } 105 | 106 | curl_setopt_array($this->curl, $curl_opt); 107 | } 108 | 109 | static function getUserAgent() 110 | { 111 | $ua = "GDAPI/". Client::VERSION . 112 | " PHP/". phpversion() . 113 | " cURL/". static::getPHPVersionID(); 114 | return $ua; 115 | } 116 | 117 | static function getCurlVersionString() 118 | { 119 | $curl_version = curl_version(); 120 | return $curl_version['version']; 121 | } 122 | 123 | static function getCurlVersionID() 124 | { 125 | $curl_version = curl_version(); 126 | return $curl_version['version_number']; 127 | } 128 | 129 | static function getPHPVersionID() 130 | { 131 | if ( defined('PHP_VERSION_ID') ) 132 | { 133 | $php_version_id = PHP_VERSION_ID; 134 | } 135 | else 136 | { 137 | $version = explode('.', PHP_VERSION); 138 | $php_version_id = $version[0] * 10000 + $version[1] * 100 + $version[2]; 139 | } 140 | 141 | return $php_version_id; 142 | } 143 | 144 | static function supportsMSTimeouts() 145 | { 146 | $php = static::getPHPVersionID(); 147 | $curl = static::getCurlVersionID(); 148 | 149 | // Supported in PHP 5.2.3+ and cURL 7.16.2+ 150 | if ( $php >= 50203 && $curl >= 0x71002 ) 151 | { 152 | return true; 153 | } 154 | 155 | return false; 156 | } 157 | 158 | public function setAuth($access_key, $secret_key) 159 | { 160 | curl_setopt_array($this->curl, array( 161 | CURLOPT_HTTPAUTH => CURLAUTH_BASIC, 162 | CURLOPT_USERPWD => $access_key .":". $secret_key, 163 | )); 164 | } 165 | 166 | public function request($method, $path, $qs=array(), $body=null, $content_type=false) 167 | { 168 | $o = &$this->client->getOptions(); 169 | $method = strtoupper($method); 170 | 171 | curl_setopt($this->curl, CURLOPT_HEADER, $o['stream_output'] === false ); 172 | curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, $o['stream_output'] === false ); 173 | 174 | $headers = $o['headers']; 175 | if ( !$headers ) 176 | { 177 | $headers = array(); 178 | } 179 | 180 | if ( $o['keep_alive'] ) 181 | { 182 | $headers['Connection'] = 'Keep-Alive'; 183 | $headers['Keep-Alive'] = $o['keep_alive']; 184 | } 185 | 186 | if ( stripos($path,'http') === 0 ) 187 | { 188 | $url = $path; 189 | } 190 | else 191 | { 192 | $url = $this->base_url . $path; 193 | } 194 | 195 | if ( isset($qs) && count($qs) ) 196 | { 197 | $url .= (( strpos($url,'?') === false ) ? '?' : '&') . http_build_query($qs); 198 | } 199 | curl_setopt($this->curl, CURLOPT_URL, $url); 200 | 201 | if ( $body !== null ) 202 | { 203 | // JSON encode objects that are passed in 204 | if ( $content_type && stripos($content_type,'json') !== false && !is_string($body) ) 205 | { 206 | $body = json_encode($body); 207 | } 208 | 209 | if ( $content_type ) 210 | { 211 | $headers['Content-Type'] = $content_type; 212 | } 213 | 214 | if ( is_array($body) ) 215 | { 216 | curl_setopt($this->curl, CURLOPT_POSTFIELDS, $body); 217 | } 218 | else 219 | { 220 | $headers['Content-Length'] = strlen($body); 221 | curl_setopt($this->curl, CURLOPT_POSTFIELDS, $body); 222 | } 223 | } 224 | else if ( $method == 'POST' ) 225 | { 226 | $headers['Content-Length'] = 0; 227 | 228 | // Must set this to null to clear out the body from a previous request 229 | curl_setopt($this->curl, CURLOPT_POSTFIELDS, null); 230 | } 231 | 232 | curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method); 233 | 234 | $flat_headers = array(); 235 | foreach ( $headers as $k => $v ) 236 | { 237 | unset($headers[$k]); 238 | $k = strtolower($k); 239 | $headers[$k] = $v; 240 | $flat_headers[] = "$k: $v"; 241 | } 242 | curl_setopt($this->curl, CURLOPT_HTTPHEADER, $flat_headers); 243 | 244 | $this->last = array( 245 | 'method' => $method, 246 | 'url' => $url, 247 | 'request_headers' => $headers, 248 | ); 249 | 250 | $response = curl_exec( $this->curl); 251 | $info = curl_getinfo($this->curl); 252 | $errno = curl_errno( $this->curl); 253 | $error = curl_error( $this->curl); 254 | 255 | $this->last['status'] = $response_code = $info['http_code']; 256 | $this->last['errno'] = $errno; 257 | $this->last['error'] = $error; 258 | $this->last['info'] = $info; 259 | $this->last['response'] = $response; 260 | 261 | if ( $errno ) 262 | { 263 | return $this->client->error($error,'request',$this->last); 264 | } 265 | else if ( $o['stream_output'] !== false ) 266 | { 267 | return true; 268 | } 269 | else 270 | { 271 | $body = $this->parseResponse($response,$info); 272 | 273 | if ( $response_code >= 200 && $response_code <= 299 ) 274 | { 275 | return $body; 276 | } 277 | else if ( $body ) 278 | { 279 | $message = ''; 280 | if ( $body->metaIsSet('message') ) 281 | { 282 | $message = $body->getMessage(); 283 | } 284 | 285 | return $this->client->error($message, $response_code, $body); 286 | } 287 | else 288 | { 289 | return $this->client->error('', $response_code, $response); 290 | } 291 | } 292 | } 293 | 294 | protected function parseResponse($res,$meta) 295 | { 296 | $o = $this->client->getOptions(); 297 | 298 | $raw_headers = substr($res,0,$meta['header_size']); 299 | $raw_body = substr($res,$meta['header_size']); 300 | 301 | $headers = array(); 302 | $lines = explode("\r\n",$raw_headers); 303 | 304 | if ( preg_match("/^HTTP\/([0-9.]+)\s+(\d+)\s+(.*)$/", $lines[0], $match) ) 305 | { 306 | $this->last['http_version'] = $match[1]; 307 | $this->last['status_msg'] = $match[3]; 308 | } 309 | else 310 | { 311 | return $this->client->error('Failed parsing HTTP response', 'parse'); 312 | } 313 | 314 | for ( $i = 1, $len = count($lines) ; $i < $len ; $i++ ) 315 | { 316 | $line = trim($lines[$i]); 317 | if ( !$line ) 318 | { 319 | continue; 320 | } 321 | 322 | $pos = strpos($line,":"); 323 | $k = strtolower(substr($line,0,$pos)); 324 | $v = substr($line,$pos+1); 325 | $headers[$k] = $v; 326 | } 327 | $this->last['response_headers'] = $headers; 328 | 329 | if ( isset($headers['content-type']) && strpos($headers['content-type'],'json') !== false ) 330 | { 331 | if ( PHP_VERSION_ID >= 50400 ) 332 | { 333 | $json = json_decode($raw_body, false, $o['json_depth_limit'], JSON_BIGINT_AS_STRING); 334 | } 335 | else 336 | { 337 | $json = json_decode($raw_body, false, $o['json_depth_limit']); 338 | } 339 | 340 | if ( $err = json_last_error() == JSON_ERROR_NONE ) 341 | { 342 | $json = $this->client->classify($json); 343 | return $json; 344 | } 345 | else 346 | { 347 | return $this->client->error("JSON Decode error: $err", 'parse'); 348 | } 349 | } 350 | } 351 | 352 | public function getMeta() 353 | { 354 | return $this->last; 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /class/CustomException.php: -------------------------------------------------------------------------------- 1 | message}' in {$this->file}({$this->line})\n" 61 | . "{$this->getTraceAsString()}"; 62 | } 63 | } 64 | 65 | ?> 66 | -------------------------------------------------------------------------------- /class/Error.php: -------------------------------------------------------------------------------- 1 | clientId = $clientId; 36 | 37 | if ( $body ) 38 | { 39 | foreach ( $body as $key => $value ) 40 | { 41 | if ( $key == 'links' ) 42 | { 43 | $this->links = $value; 44 | } 45 | else if ( $key == 'actions' ) 46 | { 47 | $this->actions = $value; 48 | } 49 | else if ( $key == 'body' ) 50 | { 51 | // This should only be in a collection... 52 | } 53 | else 54 | { 55 | $this->meta[$key] = $value; 56 | } 57 | } 58 | } 59 | } 60 | 61 | public function __call($callName, $args) 62 | { 63 | // Links 64 | if ( strpos($callName, 'fetch') === 0 ) 65 | { 66 | $name = lcfirst(substr($callName,5)); 67 | return $this->doFetch($name,$args); 68 | } 69 | // Field Getter 70 | elseif ( strpos($callName, 'get') === 0 ) 71 | { 72 | $name = lcfirst(substr($callName,3)); 73 | if ( array_key_exists($name, $this->meta) ) 74 | { 75 | // Return timestamps in seconds, as is tradition, instead of milliseconds. 76 | if ( $this->meta[$name] !== null && strtoupper(substr($name, -2)) == 'TS' ) 77 | { 78 | $short_name = substr($name,0,-2); 79 | $field = $this->schemaField($short_name); 80 | 81 | if ( isset($field, $field->type) && $field->type == 'date' ) 82 | { 83 | return $this->meta[$name]/1000; 84 | } 85 | } 86 | 87 | return $this->meta[$name]; 88 | } 89 | else 90 | { 91 | $field = $this->schemaField($name); 92 | 93 | if ( !isset($field) ) 94 | { 95 | trigger_error("Attempted to access unknown property '$name' on " . __CLASS__ . " object: " . print_r($this->meta,true), E_USER_WARNING); 96 | } 97 | 98 | return null; 99 | } 100 | } 101 | // Field Setter 102 | elseif ( strpos($callName, 'set') === 0 ) 103 | { 104 | $name = lcfirst(substr($callName,3)); 105 | $this->meta[$name] = $args[0]; 106 | return true; 107 | } 108 | else if ( strpos($callName, 'do') === 0 ) 109 | { 110 | $name = lcfirst(substr($callName,2)); 111 | return $this->doAction($name,$args); 112 | } 113 | else if ( strpos($callName, 'can') === 0 ) 114 | { 115 | $name = lcfirst(substr($callName,3)); 116 | return $this->canAction($name); 117 | } 118 | 119 | } 120 | 121 | protected function schemaField($name) 122 | { 123 | $type_name = $this->getType(); 124 | $type = $this->getClient()->{$type_name}; 125 | 126 | if ( !$type ) 127 | { 128 | return null; 129 | } 130 | 131 | $field = $type->resourceField($name); 132 | return $field; 133 | } 134 | 135 | public function metaIsSet($name) 136 | { 137 | return isset($this->meta[$name]); 138 | } 139 | 140 | /* 141 | public function __get($name) 142 | { 143 | if ( isset($this->meta[$name]) ) 144 | { 145 | return $this->meta[$name]; 146 | } 147 | } 148 | 149 | public function __set($name,$value) 150 | { 151 | if ( isset($this->meta[$name]) ) 152 | { 153 | return $this->meta[$name]; 154 | } 155 | } 156 | 157 | public function __isset($name) 158 | { 159 | return isset($this->meta[$name]); 160 | } 161 | 162 | public function __unset($name) 163 | { 164 | unset($this->meta[$name]); 165 | } 166 | */ 167 | 168 | protected function &getClient() 169 | { 170 | return Client::get($this->clientId); 171 | } 172 | 173 | protected function doFetch($name,$args) 174 | { 175 | $opt = array(); 176 | if ( isset($args,$args[0]) ) 177 | $opt['filters'] = $args[0]; 178 | 179 | if ( isset($args,$args[1]) ) 180 | $opt['sort'] = $args[1]; 181 | 182 | if ( isset($args,$args[2]) ) 183 | $opt['pagination'] = $args[2]; 184 | 185 | if ( isset($args,$args[3]) ) 186 | $opt['include'] = $args[3]; 187 | 188 | $link = Type::listHref($this->getLink($name), $opt); 189 | 190 | if ( !$link ) 191 | { 192 | return null; 193 | } 194 | 195 | $client = $this->getClient(); 196 | return $client->request('GET', $link); 197 | } 198 | 199 | public function getLinks() 200 | { 201 | return $this->links; 202 | } 203 | 204 | public function getLink($name) 205 | { 206 | if ( isset($this->links->{$name}) ) 207 | { 208 | return $this->links->{$name}; 209 | } 210 | 211 | return null; 212 | } 213 | 214 | public function canAction($name) 215 | { 216 | return isset($this->actions->{$name}); 217 | } 218 | 219 | public function doAction($name,$args) 220 | { 221 | $opt = null; 222 | if ( isset($args,$args[0]) ) 223 | { 224 | $opt = $args[0]; 225 | } 226 | 227 | if (!$this->canAction($name) ) 228 | { 229 | return null; 230 | } 231 | 232 | $link = $this->actions->{$name}; 233 | 234 | if ( !$link ) 235 | { 236 | return null; 237 | } 238 | 239 | $client = $this->getClient(); 240 | return $client->request('POST', $link, array(), $opt, Client::MIME_TYPE_JSON); 241 | } 242 | 243 | public function getMeta() 244 | { 245 | return $this->meta; 246 | } 247 | 248 | public function save() 249 | { 250 | $link = $this->getLink('self'); 251 | $client = $this->getClient(); 252 | return $client->request('PUT', $link, array(), $this->meta, Client::MIME_TYPE_JSON); 253 | } 254 | 255 | public function remove() 256 | { 257 | $link = $this->getLink('self'); 258 | $client = $this->getClient(); 259 | return $client->request('DELETE', $link); 260 | } 261 | } 262 | 263 | ?> 264 | -------------------------------------------------------------------------------- /class/Type.php: -------------------------------------------------------------------------------- 1 | clientId = $clientId; 33 | $this->schema = $schema; 34 | } 35 | 36 | public function getById($id) 37 | { 38 | $url = $this->getUrl($id); 39 | $client = $this->getClient(); 40 | return $client->request('GET', $url); 41 | } 42 | 43 | public function query($filters=array(), $sort=array(), $pagination=array(), $include=array()) 44 | { 45 | $options = array( 46 | 'filters' => $filters, 47 | 'sort' => $sort, 48 | 'pagination' => $pagination, 49 | 'include' => $include, 50 | ); 51 | 52 | $url = static::listHref($this->getUrl(), $options); 53 | $client = $this->getClient(); 54 | return $client->request('GET', $url); 55 | } 56 | 57 | public function create($obj) 58 | { 59 | $data = ( $obj instanceof Resource ? $obj->getMeta() : $obj ); 60 | $url = $this->getUrl(); 61 | $client = $this->getClient(); 62 | return $client->request('POST', $url, array(), $data, Client::MIME_TYPE_JSON); 63 | } 64 | 65 | public function remove($id_or_obj) 66 | { 67 | $id = ( $id_or_obj instanceof Resource ? $id_or_obj->getId() : $id_or_obj ); 68 | $url = $this->getUrl($id); 69 | $client = $this->getClient(); 70 | return $client->request('DELETE', $url); 71 | } 72 | 73 | public function schema() 74 | { 75 | return $this->schema; 76 | } 77 | 78 | public function resourceField($name) 79 | { 80 | if ( $this->schema->metaIsSet('resourceFields') ) 81 | $fields = $this->schema->getResourceFields(); 82 | else 83 | $fields = $this->schema->getFields(); 84 | 85 | if ( isset($fields->{$name}) ) 86 | { 87 | return $fields->{$name}; 88 | } 89 | 90 | return null; 91 | } 92 | 93 | public function collectionField($name) 94 | { 95 | $fields = $this->schema->getCollectionFields(); 96 | if ( isset($fields->{$name}) ) 97 | { 98 | return $fields->{$name}; 99 | } 100 | 101 | return null; 102 | } 103 | 104 | protected function getUrl($id=false) 105 | { 106 | return $this->schema->getLink('collection') . ($id === false ? '' : '/'.urlencode($id) ); 107 | } 108 | 109 | public static function listHref($url, $opt) 110 | { 111 | $opt = static::arrayify($opt); 112 | $qs = parse_url($url,PHP_URL_QUERY); 113 | 114 | # Filters 115 | if ( isset($opt['filters']) && count($opt['filters']) ) 116 | { 117 | // 'filters' is a hash of field names => filter or array(filters) 118 | // Each filter value can be: 119 | // - A simple literal like 'blah', ("name equals blah") 120 | // - A hash with modifier and/or value: array('modifier' => 'ne', value => 'blah') ("name is not equal to blah") 121 | // - An array of one or more of the above: array('blah', array('modifier' => 'notnull') ("name is equal to blah AND name is not null") 122 | 123 | // Loop over the hash of each field name 124 | foreach ( $opt['filters'] as $fieldName => $list ) 125 | { 126 | // Turn whatever the input was into an aray of individual filters to check 127 | if ( !is_array($list) ) 128 | { 129 | // Simple value 130 | $list = array($list); 131 | } 132 | else if ( isset($list['value']) || isset($list['modifier']) ) 133 | { 134 | // It's an "array", but really a hash like array('modifier' => 'blah', 'value' => blah') 135 | $list = array($list); 136 | } 137 | else 138 | { 139 | // Already an array of individual filters, do nothing 140 | } 141 | 142 | // Loop over each individual filter for this field 143 | foreach ( $list as $filter ) 144 | { 145 | // This is a filter like array('modifier' => 'blah', 'value' => blah') 146 | if ( is_array($filter) && ( isset($filter['value']) || isset($filter['modifier']) ) ) 147 | { 148 | $name = $fieldName; 149 | 150 | if ( isset($filter['modifier']) && $filter['modifier'] != '' && $filter['modifier'] != 'eq' ) 151 | $name .= '_' . $filter['modifier']; 152 | 153 | $value = null; 154 | if ( isset($filter['value']) ) 155 | { 156 | $value = $filter['value']; 157 | } 158 | } 159 | else 160 | { 161 | // This is a simple literal name=value literal 162 | $name = $fieldName; 163 | $value = $filter; 164 | } 165 | 166 | $qs .= '&' . urlencode($name); 167 | 168 | // Only add value if it's meaningful 169 | // (Note: A filter with value => null is invalid, use array('modifier' => 'null') to say that a field is null) 170 | if ( $value !== null ) 171 | $qs .= '='. urlencode($value); 172 | } 173 | } 174 | } 175 | 176 | # Sorting 177 | if ( isset($opt['sort']) && count($opt['sort']) ) 178 | { 179 | if ( is_array($opt['sort']) ) 180 | { 181 | $qs .= '&sort=' . urlencode($opt['sort']['name']); 182 | if ( isset($opt['sort']['order']) && strtolower($opt['sort']['order']) == 'desc' ) 183 | { 184 | $qs .= '&order=desc'; 185 | } 186 | } 187 | } 188 | 189 | # Pagination 190 | if ( isset($opt['pagination']) && count($opt['pagination']) ) 191 | { 192 | $qs .= '&limit=' . intval($opt['pagination']['limit']); 193 | 194 | if ( isset($opt['pagination']['marker']) ) 195 | { 196 | $qs .= '&marker=' . urlencode($opt['pagination']['marker']); 197 | } 198 | } 199 | 200 | # Include 201 | if ( isset($opt['include']) && count($opt['include']) ) 202 | { 203 | foreach ( $opt['include'] as $link ) 204 | { 205 | $qs .= '&include=' . urlencode($link); 206 | } 207 | } 208 | 209 | $base_url = preg_replace("/\?.*/","",$url); 210 | $out = $base_url; 211 | if ( $qs ) 212 | { 213 | // If the initial URL query string was empty, there will be an extra & at the beginning 214 | $out .= '?' . preg_replace("/^&/","",$qs); 215 | } 216 | 217 | return $out; 218 | } 219 | 220 | static function arrayify($obj) 221 | { 222 | if ( is_object($obj) ) 223 | $ary = get_object_vars($obj); 224 | else 225 | $ary = $obj; 226 | 227 | foreach ( $ary as $k => $v ) 228 | { 229 | if ( is_array($v) || is_object($v) ) 230 | { 231 | $v = static::arrayify($v); 232 | $ary[$k] = $v; 233 | } 234 | } 235 | 236 | return $ary; 237 | } 238 | 239 | protected function &getClient() 240 | { 241 | return Client::get($this->clientId); 242 | } 243 | } 244 | 245 | ?> 246 | -------------------------------------------------------------------------------- /class/VariableCache.php: -------------------------------------------------------------------------------- 1 | 67 | -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /tests/filters.php: -------------------------------------------------------------------------------- 1 | 42)); 7 | 8 | test('k_ne=42', array('k' => array('modifier' => 'ne', 'value' => 42))); 9 | 10 | test('k_ne=43&k_gt=44', array( 11 | 'k' => array( 12 | array('modifier' => 'ne', 'value' => 43), 13 | array('modifier' => 'gt', 'value' => 44) 14 | ) 15 | )); 16 | 17 | test('k_ne=43&k_gt=44&k=45&j=46',array( 18 | 'k' => array( 19 | array('modifier' => 'ne', 'value' => 43), 20 | array('modifier' => 'gt', 'value' => 44), 21 | 45 22 | ), 23 | 'j' => 46, 24 | ) 25 | ); 26 | 27 | function test($expect, $filters) 28 | { 29 | $base = 'http://a.com'; 30 | $got = GDAPI\Type::listHref($base,array('filters' => $filters)); 31 | 32 | $index = strpos($got,'?'); 33 | if ( $index === FALSE ) 34 | $query = ""; 35 | else 36 | $query = substr($got,$index+1); 37 | 38 | echo "Expect: $expect\nGot: $query\nResult: " . ($query == $expect ? "ok" : "FAIL") . "\n\n"; 39 | } 40 | 41 | ?> 42 | --------------------------------------------------------------------------------