├── DynamoDBWrapper.php ├── LICENSE ├── README.md └── composer.json /DynamoDBWrapper.php: -------------------------------------------------------------------------------- 1 | client = DynamoDbClient::factory($args); 13 | } 14 | 15 | public function get($tableName, $key, $options = array()) 16 | { 17 | $args = array( 18 | 'TableName' => $tableName, 19 | 'Key' => $this->convertAttributes($key), 20 | ); 21 | if (isset($options['ConsistentRead'])) { 22 | $args['ConsistentRead'] = $options['ConsistentRead']; 23 | } 24 | $item = $this->client->getItem($args); 25 | return $this->convertItem($item['Item']); 26 | } 27 | 28 | public function batchGet($tableName, $keys, $options = array()) 29 | { 30 | $results = array(); 31 | 32 | $ddbKeys = array(); 33 | foreach ($keys as $key) { 34 | $ddbKeys[] = $this->convertAttributes($key); 35 | } 36 | 37 | while (count($ddbKeys) > 0) { 38 | $targetKeys = array_splice($ddbKeys, 0, 100); 39 | 40 | $result = $this->client->batchGetItem(array( 41 | 'RequestItems' => array( 42 | $tableName => array( 43 | 'Keys' => $targetKeys, 44 | ), 45 | ), 46 | )); 47 | $items = $result->getPath("Responses/{$tableName}"); 48 | $results = array_merge($results, $this->convertItems($items)); 49 | 50 | // if some keys not processed, try again as next request 51 | $unprocessedKeys = $result->getPath("UnprocessedKeys/{$tableName}"); 52 | if (count($unprocessedKeys) > 0) { 53 | $ddbKeys = array_merge($ddbKeys, $unprocessedKeys); 54 | } 55 | } 56 | 57 | if (isset($options['Order'])) { 58 | if ( ! isset($options['Order']['Key'])) { 59 | throw new Exception("Order option needs 'Key'."); 60 | } 61 | $key = $options['Order']['Key']; 62 | 63 | if (isset($options['Order']['Forward']) && !$options['Order']['Forward']) { 64 | $vals = array('b', 'a'); 65 | } else { 66 | $vals = array('a', 'b'); 67 | } 68 | 69 | $f = 'return ($'.$vals[0].'[\''.$key.'\'] - $'.$vals[1].'[\''.$key.'\']);'; 70 | usort($results, create_function('$a,$b',$f)); 71 | } 72 | 73 | return $results; 74 | } 75 | 76 | public function query($tableName, $keyConditions, $options = array()) 77 | { 78 | $args = array( 79 | 'TableName' => $tableName, 80 | 'KeyConditions' => $this->convertConditions($keyConditions), 81 | 'ScanIndexForward' => true, 82 | 'Limit' => 100, 83 | ); 84 | if (isset($options['ScanIndexForward'])) { 85 | $args['ScanIndexForward'] = $options['ScanIndexForward']; 86 | } 87 | if (isset($options['IndexName'])) { 88 | $args['IndexName'] = $options['IndexName']; 89 | } 90 | if (isset($options['Limit'])) { 91 | $args['Limit'] = $options['Limit']+0; 92 | } 93 | if (isset($options['ConsistentRead'])) { 94 | $args['ConsistentRead'] = $options['ConsistentRead']; 95 | } 96 | if (isset($options['ExclusiveStartKey'])) { 97 | $args['ExclusiveStartKey'] = $this->convertAttributes($options['ExclusiveStartKey']); 98 | } 99 | $result = $this->client->query($args); 100 | return $this->convertItems($result['Items']); 101 | } 102 | 103 | public function count($tableName, $keyConditions, $options = array()) 104 | { 105 | $args = array( 106 | 'TableName' => $tableName, 107 | 'KeyConditions' => $this->convertConditions($keyConditions), 108 | 'Select' => 'COUNT', 109 | ); 110 | if (isset($options['IndexName'])) { 111 | $args['IndexName'] = $options['IndexName']; 112 | } 113 | $result = $this->client->query($args); 114 | return $result['Count']; 115 | } 116 | 117 | public function scan($tableName, $filter, $limit = null) 118 | { 119 | if (empty($filter)) { 120 | $scanFilter = null; 121 | } else { 122 | $scanFilter = $this->convertConditions($filter); 123 | } 124 | $items = $this->client->getIterator('Scan', array( 125 | 'TableName' => $tableName, 126 | 'ScanFilter' => $scanFilter, 127 | )); 128 | return $this->convertItems($items); 129 | } 130 | 131 | public function put($tableName, $item, $expected = array()) 132 | { 133 | $args = array( 134 | 'TableName' => $tableName, 135 | 'Item' => $this->convertAttributes($item), 136 | ); 137 | if (!empty($expected)) { 138 | $item['Expected'] = $expected; 139 | } 140 | // Put and catch exception when ConditionalCheckFailed 141 | try { 142 | $item = $this->client->putItem($args); 143 | } 144 | catch (ConditionalCheckFailedException $e) { 145 | return false; 146 | } 147 | return true; 148 | } 149 | 150 | public function batchPut($tableName, $items) 151 | { 152 | return $this->batchWrite('PutRequest', $tableName, $items); 153 | } 154 | 155 | public function update($tableName, $key, $update, $expected = array()) 156 | { 157 | $args = array( 158 | 'TableName' => $tableName, 159 | 'Key' => $this->convertAttributes($key), 160 | 'AttributeUpdates' => $this->convertUpdateAttributes($update), 161 | 'ReturnValues' => 'UPDATED_NEW', 162 | ); 163 | if (!empty($expected)) { 164 | $item['Expected'] = $expected; 165 | } 166 | // Put and catch exception when ConditionalCheckFailed 167 | try { 168 | $item = $this->client->updateItem($args); 169 | } 170 | catch (ConditionalCheckFailed $e) { 171 | return null; 172 | } 173 | return $this->convertItem($item['Attributes']); 174 | } 175 | 176 | public function delete($tableName, $key) 177 | { 178 | $args = array( 179 | 'TableName' => $tableName, 180 | 'Key' => $this->convertAttributes($key), 181 | 'ReturnValues' => 'ALL_OLD', 182 | ); 183 | $result = $this->client->deleteItem($args); 184 | return $this->convertItem($result['Attributes']); 185 | } 186 | 187 | public function batchDelete($tableName, $keys) 188 | { 189 | return $this->batchWrite('DeleteRequest', $tableName, $keys); 190 | } 191 | 192 | protected function batchWrite($requestType, $tableName, $items) 193 | { 194 | $entityKeyName = ($requestType === 'PutRequest' ? 'Item' : 'Key'); 195 | 196 | $requests = array(); 197 | foreach ($items as $item) { 198 | $requests[] = array( 199 | $requestType => array( 200 | $entityKeyName => $this->convertAttributes($item) 201 | ) 202 | ); 203 | } 204 | 205 | while (count($requests) > 0) { 206 | $targetRequests = array_splice($requests, 0, 25); 207 | 208 | $result = $this->client->batchWriteItem(array( 209 | 'RequestItems' => array( 210 | $tableName => $targetRequests 211 | ), 212 | )); 213 | 214 | // if some items not processed, try again as next request 215 | $unprocessedRequests = $result->getPath("UnprocessedItems/{$tableName}"); 216 | if (count($unprocessedRequests) > 0) { 217 | $requests = array_merge($requests, $unprocessedRequests); 218 | } 219 | } 220 | 221 | return true; 222 | } 223 | 224 | public function createTable($tableName, $hashKey, $rangeKey = null, $options = null) { 225 | 226 | $attributeDefinitions = array(); 227 | $keySchema = array(); 228 | 229 | // HashKey 230 | $hashKeyComponents = $this->convertComponents($hashKey); 231 | $hashKeyName = $hashKeyComponents[0]; 232 | $hashKeyType = $hashKeyComponents[1]; 233 | $attributeDefinitions []= array('AttributeName' => $hashKeyName, 'AttributeType' => $hashKeyType); 234 | $keySchema[] = array('AttributeName' => $hashKeyName, 'KeyType' => 'HASH'); 235 | 236 | // RangeKey 237 | if (isset($rangeKey)) { 238 | $rangeKeyComponents = $this->convertComponents($rangeKey); 239 | $rangeKeyName = $rangeKeyComponents[0]; 240 | $rangeKeyType = $rangeKeyComponents[1]; 241 | $attributeDefinitions[] = array('AttributeName' => $rangeKeyName, 'AttributeType' => $rangeKeyType); 242 | $keySchema[] = array('AttributeName' => $rangeKeyName, 'KeyType' => 'RANGE'); 243 | } 244 | 245 | // Generate Args 246 | $args = array( 247 | 'TableName' => $tableName, 248 | 'AttributeDefinitions' => $attributeDefinitions, 249 | 'KeySchema' => $keySchema, 250 | 'ProvisionedThroughput' => array( 251 | 'ReadCapacityUnits' => 1, 252 | 'WriteCapacityUnits' => 1 253 | ) 254 | ); 255 | 256 | // Set Local Secondary Index if needed 257 | if (isset($options['LocalSecondaryIndexes'])) { 258 | $LSI = array(); 259 | foreach ($options['LocalSecondaryIndexes'] as $i) { 260 | $LSI []= array( 261 | 'IndexName' => $i['name'].'Index', 262 | 'KeySchema' => array( 263 | array('AttributeName' => $hashKeyName, 'KeyType' => 'HASH'), 264 | array('AttributeName' => $i['name'], 'KeyType' => 'RANGE') 265 | ), 266 | 'Projection' => array( 267 | 'ProjectionType' => $i['projection_type'] 268 | ), 269 | ); 270 | $attributeDefinitions []= array('AttributeName' => $i['name'], 'AttributeType' => $i['type']); 271 | } 272 | $args['LocalSecondaryIndexes'] = $LSI; 273 | $args['AttributeDefinitions'] = $attributeDefinitions; 274 | } 275 | 276 | $this->client->createTable($args); 277 | $this->client->waitUntilTableExists(array('TableName' => $tableName)); 278 | } 279 | 280 | public function deleteTable($tableName) 281 | { 282 | $this->client->deleteTable(array('TableName' => $tableName)); 283 | $this->client->waitUntilTableNotExists(array('TableName' => $tableName)); 284 | } 285 | 286 | public function emptyTable($table) { 287 | // Get table info 288 | $result = $this->client->describeTable(array('TableName' => $table)); 289 | $keySchema = $result['Table']['KeySchema']; 290 | foreach ($keySchema as $schema) { 291 | if ($schema['KeyType'] === 'HASH') { 292 | $hashKeyName = $schema['AttributeName']; 293 | } 294 | else if ($schema['KeyType'] === 'RANGE') { 295 | $rangeKeyName = $schema['AttributeName']; 296 | } 297 | } 298 | 299 | // Delete items in the table 300 | $scan = $this->client->getIterator('Scan', array('TableName' => $table)); 301 | foreach ($scan as $item) { 302 | // set hash key 303 | $hashKeyType = array_key_exists('S', $item[$hashKeyName]) ? 'S' : 'N'; 304 | $key = array( 305 | $hashKeyName => array($hashKeyType => $item[$hashKeyName][$hashKeyType]), 306 | ); 307 | // set range key if defined 308 | if (isset($rangeKeyName)) { 309 | $rangeKeyType = array_key_exists('S', $item[$rangeKeyName]) ? 'S' : 'N'; 310 | $key[$rangeKeyName] = array($rangeKeyType => $item[$rangeKeyName][$rangeKeyType]); 311 | } 312 | $this->client->deleteItem(array( 313 | 'TableName' => $table, 314 | 'Key' => $key 315 | )); 316 | } 317 | } 318 | 319 | protected function asString($value) 320 | { 321 | if (is_array($value)) { 322 | $newValue = array(); 323 | foreach ($value as $v) { 324 | $newValue[] = (string)$v; 325 | } 326 | } else { 327 | $newValue = (string)$value; 328 | } 329 | return $newValue; 330 | } 331 | 332 | protected function convertAttributes($targets) 333 | { 334 | $newTargets = array(); 335 | foreach ($targets as $k => $v) { 336 | $attrComponents = $this->convertComponents($k); 337 | $newTargets[$attrComponents[0]] = array($attrComponents[1] => $this->asString($v)); 338 | } 339 | return $newTargets; 340 | } 341 | 342 | protected function convertUpdateAttributes($targets) 343 | { 344 | $newTargets = array(); 345 | foreach ($targets as $k => $v) { 346 | $attrComponents = $this->convertComponents($k); 347 | $newTargets[$attrComponents[0]] = array( 348 | 'Action' => $v[0], 349 | 'Value' => array($attrComponents[1] => $this->asString($v[1])), 350 | ); 351 | } 352 | return $newTargets; 353 | } 354 | 355 | protected function convertConditions($conditions) 356 | { 357 | $ddbConditions = array(); 358 | foreach ($conditions as $k => $v) { 359 | // Get attr name and type 360 | $attrComponents = $this->convertComponents($k); 361 | $attrName = $attrComponents[0]; 362 | $attrType = $attrComponents[1]; 363 | 364 | // Get ComparisonOperator and value 365 | if ( ! is_array($v)) { 366 | $v = array('EQ', $this->asString($v)); 367 | } 368 | $comparisonOperator = $v[0]; 369 | $value = count($v) > 1 ? $v[1] : null; 370 | 371 | // Get AttributeValueList 372 | if ($v[0] === 'BETWEEN') { 373 | if (count($value) !== 2) { 374 | throw new Exception("Require 2 values as array for BETWEEN"); 375 | } 376 | $attributeValueList = array( 377 | array($attrType => $this->asString($value[0])), 378 | array($attrType => $this->asString($value[1])) 379 | ); 380 | } else if ($v[0] === 'IN') { 381 | $attributeValueList = array(); 382 | foreach ($value as $v) { 383 | $attributeValueList[] = array($attrType => $this->asString($v)); 384 | } 385 | } else if ($v[0] === 'NOT_NULL' || $v[0] === 'NULL') { 386 | $attributeValueList = null; 387 | } else { 388 | $attributeValueList = array( 389 | array($attrType => $this->asString($value)), 390 | ); 391 | } 392 | 393 | // Constract key condition for DynamoDB 394 | $ddbConditions[$attrName] = array( 395 | 'AttributeValueList' => $attributeValueList, 396 | 'ComparisonOperator' => $comparisonOperator 397 | ); 398 | } 399 | 400 | return $ddbConditions; 401 | } 402 | 403 | protected function convertItem($item) 404 | { 405 | if (empty($item)) return null; 406 | 407 | $converted = array(); 408 | foreach ($item as $k => $v) { 409 | if (isset($v['S'])) { 410 | $converted[$k] = $v['S']; 411 | } 412 | else if (isset($v['SS'])) { 413 | $converted[$k] = $v['SS']; 414 | } 415 | else if (isset($v['N'])) { 416 | $converted[$k] = $v['N']; 417 | } 418 | else if (isset($v['NS'])) { 419 | $converted[$k] = $v['NS']; 420 | } 421 | else if (isset($v['B'])) { 422 | $converted[$k] = $v['B']; 423 | } 424 | else if (isset($v['BS'])) { 425 | $converted[$k] = $v['BS']; 426 | } 427 | else { 428 | throw new Exception('Not implemented type'); 429 | } 430 | } 431 | return $converted; 432 | } 433 | 434 | protected function convertItems($items) 435 | { 436 | $converted = array(); 437 | foreach ($items as $item) { 438 | $converted []= $this->convertItem($item); 439 | } 440 | return $converted; 441 | } 442 | 443 | /** 444 | * convert string attribute paramter to array components. 445 | * 446 | * @param string $attribute double colon separated string "::" 447 | * @return array parsed parameter. [0]=, [1]= 448 | */ 449 | protected function convertComponents($attribute){ 450 | $components = explode('::', $attribute); 451 | if (count($components) < 2) { 452 | $components[1] = 'S'; 453 | } 454 | return $components; 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Masayuki Tanaka 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 | dynamodb-php-wrapper 2 | ==================== 3 | 4 | Access AWS DynamoDB through simpler interface. 5 | 6 | This module is simpler because: 7 | * The results of each API are mapped to Array object automatically. 8 | * The request exceeding the limit can be sent by dividing into small ones automatically. 9 | * The response exceeding the limit can be fetched by requesting continuously. 10 | 11 | If you get interested in this module, please see [the wiki](https://github.com/masayuki0812/dynamodb-php-wrapper/wiki). 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws/dynamodb-php-wrapper", 3 | "require": { 4 | "aws/aws-sdk-php": "2.*" 5 | }, 6 | "description": "Access AWS DynamoDB through simpler interface.", 7 | "license": "MIT", 8 | "keywords": ["aws","dynamodb"], 9 | "authors": [ 10 | { 11 | "name": "Masayuki Tanaka", 12 | "email": "masayuki0812@mac.com", 13 | "homepage": "https://github.com/masayuki0812", 14 | "role": "Developer" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-0": { 19 | "": "." 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------