├── .gitignore ├── LICENSE ├── README.md ├── class.InstantAPI.php ├── index.php └── settings.inc.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waldo Jaquith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instant API 2 | 3 | A PHP class that makes it trivial to pop up a RESTful, JSON-emitting API from a single JSON file. Intended for retrieving individual records from a JSON file that is comprised of many records. It indexes records by a specified field, which functions as the unique ID. 4 | 5 | It caches API data as a single serialized object, in APC, in Memcached, or as individual JSON files on the filesystem. 6 | 7 | If you'd prefer this in Python, @jbradforddillon [ported it and made some improvements](https://github.com/jbradforddillon/instant-api-py). 8 | 9 | ## Instructions 10 | 1. Install `index.php` and `class.InstantAPI.php` in a public directory on a web server. 11 | 1. If you want to cache data on the filesystem, create a directory called `cache`, and give the web server permission to write to it and read from it. 12 | 1. Edit the configuration options in `settings.inc.php`: 13 | * Specify the path to the JSON file as `JSON_FILE`. (You can just drop it in the same directory as `index.php` etc.) 14 | * Specify the name of the field in each JSON record that will function as the unique ID for each request as `INDEXED_FIELD`. 15 | * Specify the type of caching that you want to use. 16 | * If caching in Memcached, provide the server name (`MEMCACHED_SERVER`) and port (`MEMCACHED_PORT`). 17 | 18 | Requests must be in the format of `http://example.com/?id=[unique_id]`. Of course, the directory need not be named `instantapi`, and `mod_rewrite` can be used to eliminate `?id=` from the public URL, so the URL could read `http://example.com/person/jsmith.json`, or `http://example.com/zipcode/90210.json`. 19 | 20 | The first request will prime the cache and then deliver the requested result; subsequent requests will be served from the cache. To force a refresh of the filesystem cache, such as after updating the master JSON file, simply delete all of the files in `cache/`. 21 | 22 | ## Example 23 | 24 | To create an API for this JSON file, `committees.json`, with `CommitteeCode` as the unique ID: 25 | 26 | ```json 27 | { 28 | "0": { 29 | "AccountId": "a1f8792b-3e82-e111-9bed-984be103f032", 30 | "CommitteeCode": "PP-12-00458", 31 | "CommitteeName": "10th District Republican Congressional Committee" 32 | }, 33 | "1": { 34 | "AccountId": "92b38bad-2583-e111-9bed-984be103f032", 35 | "CommitteeCode": "PP-12-00366", 36 | "CommitteeName": "11th Congressional District Democratic Committee" 37 | }, 38 | "2": { 39 | "AccountId": "69376bae-3e82-e111-9bed-984be103f032", 40 | "CommitteeCode": "PP-12-00457", 41 | "CommitteeName": "11th Congressional District of VA Republican Committee" 42 | }, 43 | "3": { 44 | "AccountId": "341646c1-4082-e111-9bed-984be103f032", 45 | "CommitteeCode": "PP-12-00450", 46 | "CommitteeName": "1st District Republican Committee" 47 | }, 48 | "4": { 49 | "AccountId": "2b5f88f6-aa7d-e111-9bed-984be103f032", 50 | "CommitteeCode": "PAC-12-00377", 51 | "CommitteeName": "2007 Conservative Victory Committee" 52 | } 53 | } 54 | ``` 55 | 56 | Copy `committees.json` into the Instant API directory, set `JSON_FILE` to `committees.json`, and set `INDEXED_FIELD` to `CommitteeCode`. If `CACHE_TYPE` is kept at the default value of `json`, then loading `http://example.com/?id=PP-12-00458` will create five JSON files in `cache`, and then pass the contents of `/cache/PP-12-00458.json` directly to the browser. (The cache directory could be to say, `records`, and the URL `http://example.com/records/PP-12-00458.json` could be queried directly, loading that static file and eliminating the need to invoke Instant API at all.) 57 | 58 | 59 | ## Requirements 60 | * PHP v5.2 or later. 61 | -------------------------------------------------------------------------------- /class.InstantAPI.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright 2014 Waldo Jaquith 13 | * @license http://opensource.org/licenses/MIT 14 | * @link https://github.com/waldoj/instant-api/ 15 | * 16 | */ 17 | class InstantAPI 18 | { 19 | 20 | /** 21 | * Retreives data and caches it, immediately upon loading 22 | * 23 | * Every time we instantiate InstantAPI(), we want to retrieve data (and possibly cache it). 24 | * 25 | * @return boolean TRUE or FALSE, FALSE if data could be retrieved, parsed, or cached. 26 | */ 27 | function __construct() 28 | { 29 | 30 | /* 31 | * If the specified cache is Memcached, create a new Memcached instance. 32 | */ 33 | if (CACHE_TYPE == 'memcached') 34 | { 35 | $this->mc = new Memcached(); 36 | $this->mc->addServer(MEMCACHED_SERVER, MEMCACHED_PORT); 37 | } 38 | 39 | $this->id = filter_input(INPUT_GET, 'id'); 40 | $result = $this->retrieve_data(); 41 | 42 | if ($result === FALSE) 43 | { 44 | 45 | $result = $this->parse_json(); 46 | if ($result === FALSE) 47 | { 48 | die('Cannot parse JSON.'); 49 | } 50 | $result = $this->cache_data(); 51 | if ($result === FALSE) 52 | { 53 | die('Cannot cache data.'); 54 | } 55 | $result = $this->retrieve_data(); 56 | if ($result === FALSE) 57 | { 58 | die('Cannot retrieve data from cache.'); 59 | } 60 | 61 | } 62 | 63 | } 64 | 65 | /** 66 | * Parse data from a JSON file 67 | * 68 | * Extract all data from the JSON file and pivot it to be indexed by the specified field. 69 | * 70 | * @return boolean TRUE or FALSE, FALSE if JSON could not parsed. 71 | */ 72 | function parse_json() 73 | { 74 | 75 | /* 76 | * A field name to be indexed must be specified. 77 | */ 78 | if (defined('INDEXED_FIELD') === FALSE) 79 | { 80 | return FALSE; 81 | } 82 | 83 | /* 84 | * Load the file in as an object. 85 | */ 86 | $data = json_decode(file_get_contents(JSON_FILE)); 87 | 88 | if ($data == FALSE) 89 | { 90 | return FALSE; 91 | } 92 | 93 | /* 94 | * Duplicate the object, creating a new one that uses the committee ID as the key. 95 | */ 96 | $tmp = new stdClass(); 97 | foreach($data as &$record) 98 | { 99 | 100 | $tmp->{$record->{INDEXED_FIELD}} = $record; 101 | 102 | /* 103 | * Save memory. 104 | */ 105 | unset($record); 106 | 107 | } 108 | 109 | /* 110 | * Swap variables. 111 | */ 112 | unset($data); 113 | $this->data = $tmp; 114 | unset($tmp); 115 | 116 | return TRUE; 117 | 118 | } // end method parse_json() 119 | 120 | /** 121 | * Cache data 122 | * 123 | * Store the JSON file's constituent data within the configured cache mechanism. 124 | * 125 | * @return boolean TRUE or FALSE, FALSE if JSON could not cached. 126 | */ 127 | function cache_data() 128 | { 129 | 130 | if (CACHE_TYPE === FALSE) 131 | { 132 | return TRUE; 133 | } 134 | 135 | elseif (CACHE_TYPE == 'serialize') 136 | { 137 | $result = file_put_contents(CACHE_DIRECTORY . 'cache', serialize($this->data)); 138 | if ($result === FALSE) 139 | { 140 | return FALSE; 141 | } 142 | } 143 | 144 | elseif (CACHE_TYPE == 'apc') 145 | { 146 | 147 | /* 148 | * Iterate through all of the records and store each of them within APC. 149 | */ 150 | foreach ($this->data as $id => $record) 151 | { 152 | apc_store($id, $record); 153 | } 154 | 155 | } 156 | 157 | elseif (CACHE_TYPE == 'memcached') 158 | { 159 | 160 | /* 161 | * Iterate through all of the records and store each of them within APC. 162 | */ 163 | foreach ($this->data as $id => $record) 164 | { 165 | $this->mc->set($id, serialize($record)); 166 | } 167 | 168 | } 169 | 170 | elseif (CACHE_TYPE == 'json') 171 | { 172 | 173 | /* 174 | * Iterate through all of the records and store each of them within the filesystem. 175 | */ 176 | foreach ($this->data as $id => $record) 177 | { 178 | file_put_contents(CACHE_DIRECTORY . $id .'.json', json_encode($record)); 179 | } 180 | 181 | } 182 | 183 | return TRUE; 184 | 185 | } // end method cache_data 186 | 187 | 188 | /** 189 | * Retrieve cached data 190 | * 191 | * Get cached data from the configured cache mechanism. 192 | * 193 | * @return boolean TRUE or FALSE, FALSE if JSON could not cached. 194 | */ 195 | function retrieve_data() 196 | { 197 | 198 | if (CACHE_TYPE === FALSE) 199 | { 200 | $this->parse_json(); 201 | return TRUE; 202 | } 203 | 204 | elseif (CACHE_TYPE == 'serialize') 205 | { 206 | 207 | if (file_exists(CACHE_DIRECTORY . 'cache') === FALSE) 208 | { 209 | return FALSE; 210 | } 211 | 212 | $result = file_get_contents(CACHE_DIRECTORY . 'cache'); 213 | if ($result === FALSE) 214 | { 215 | return FALSE; 216 | } 217 | 218 | $this->data = unserialize($result); 219 | return TRUE; 220 | 221 | } 222 | 223 | elseif (CACHE_TYPE == 'apc') 224 | { 225 | 226 | apc_fetch($this->id, $result); 227 | if ($result === FALSE) 228 | { 229 | return FALSE; 230 | } 231 | $this->record = $record; 232 | unset($record); 233 | return TRUE; 234 | 235 | } 236 | 237 | elseif (CACHE_TYPE == 'memcached') 238 | { 239 | 240 | $this->mc->get($this->id, $result); 241 | if ($result === FALSE) 242 | { 243 | return FALSE; 244 | } 245 | $this->record = unserialize($record); 246 | unset($record); 247 | return TRUE; 248 | 249 | } 250 | 251 | elseif (CACHE_TYPE == 'json') 252 | { 253 | 254 | if (file_exists(CACHE_DIRECTORY . $this->id .'.json') === FALSE) 255 | { 256 | return FALSE; 257 | } 258 | 259 | readfile(CACHE_DIRECTORY . $this->id .'.json'); 260 | exit; 261 | 262 | } 263 | 264 | } // end method retrieve_data 265 | 266 | } // end class Server 267 | 268 | /** 269 | * Format an error as JSON 270 | * 271 | * Return a JSON-formatted error message, to permit parsing and rendering by the client. 272 | * 273 | * @return string JSON-formatted data 274 | */ 275 | function json_error($message = 'A fatal error occurred.', $http_code = '400 Bad Request') 276 | { 277 | header('HTTP/1.0 ' . $http_code); 278 | echo json_encode( array( 'error' => $message ) ); 279 | } 280 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright 2014 Waldo Jaquith 12 | * @license http://opensource.org/licenses/MIT 13 | * @link https://github.com/waldoj/instant-api/ 14 | * 15 | */ 16 | 17 | /* 18 | * Include the settings. 19 | */ 20 | require('settings.inc.php'); 21 | 22 | /* 23 | * Include the Instant API library. 24 | */ 25 | require('class.InstantAPI.php'); 26 | 27 | /* 28 | * All output will be as JSON. 29 | */ 30 | header('Content-Type: application/json'); 31 | 32 | /* 33 | * Require that an ID be present in the request. 34 | */ 35 | if (!isset($_GET['id'])) 36 | { 37 | json_error('No ID provided.'); 38 | die(); 39 | } 40 | 41 | /* 42 | * Create a new instance of Instant API. 43 | */ 44 | $server = new InstantAPI(); 45 | 46 | /* 47 | * If the requested ID doesn't exist, return a 404. 48 | */ 49 | if (!isset($server->data->{$server->id})) 50 | { 51 | json_error('ID not found.', '404 Not Found'); 52 | die(); 53 | } 54 | 55 | /* 56 | * Return the record to the client. 57 | */ 58 | echo json_encode($server->data->{$server->id}); 59 | -------------------------------------------------------------------------------- /settings.inc.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014 Waldo Jaquith 10 | * @license http://opensource.org/licenses/MIT 11 | * @link https://github.com/waldoj/instant-api/ 12 | * 13 | */ 14 | 15 | /* 16 | * What is the JSON file that's to be the source of this API's data? 17 | */ 18 | define('JSON_FILE', ''); 19 | 20 | /* 21 | * What is the name of the field that will be queried via the API? That is, what is the unique ID? 22 | */ 23 | define('INDEXED_FIELD', ''); 24 | 25 | /* 26 | * What type of caching should be used? Valid options are: "false" (a literal, boolean FALSE) to 27 | * indicate that no caching should be used; "serialize" to indicate that the entire object should be 28 | * serialized and stored as a file; "apc" to store each object property in APC; "memcached" to store 29 | * each object property in Memcached, and "json" to store each object property as an individual JSON file. 30 | */ 31 | define('CACHE_TYPE', 'json'); 32 | 33 | /* 34 | * If cached within the filesystem, in what directory should cached material be stored? 35 | */ 36 | define('CACHE_DIRECTORY', 'cache/'); 37 | 38 | /* 39 | * If using Memcached-based caching, what is the server and port for the Memcached server? 40 | */ 41 | define('MEMCACHED_SERVER', '127.0.0.1'); 42 | define('MEMCACHED_PORT', '11211'); 43 | --------------------------------------------------------------------------------