├── test ├── dynamodb-local-data │ └── .gitkeep └── docker │ ├── sleep.sh │ └── Dockerfile ├── .gitignore ├── docker-compose.yml ├── composer.json ├── LICENSE.txt ├── README.md ├── bin └── kettle-skeleton.php └── src └── kettle.php /test/dynamodb-local-data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | vendor 3 | composer.phar 4 | composer.lock 5 | -------------------------------------------------------------------------------- /test/docker/sleep.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | sleep infinity 5 | -------------------------------------------------------------------------------- /test/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-cli 2 | 3 | WORKDIR /opt/project 4 | 5 | COPY ./sleep.sh /sleep.sh 6 | 7 | CMD ["/bin/bash", "/sleep.sh"] 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | php74: 4 | build: ${PWD}/test/docker 5 | image: php74-cli 6 | container_name: php74-kettle 7 | tty: true 8 | volumes: 9 | - ${PWD}:/opt/project 10 | 11 | dynamodb-local: 12 | container_name: dynamodb-local 13 | image: amazon/dynamodb-local:latest 14 | user: root 15 | command: -jar DynamoDBLocal.jar -sharedDb -dbPath /data 16 | volumes: 17 | - ${PWD}/test/dynamodb-local-data:/data 18 | ports: 19 | - "8000:8000" 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kettle/dynamodb-orm", 3 | "type": "library", 4 | "description": "A lightweight object-dynamodb mapper for PHP", 5 | "keywords": ["kettle", "orm", "dynamodb", "aws"], 6 | "homepage": "http://github.com/inouet/", 7 | "support": { 8 | "issues": "https://github.com/inouet/kettle/issues", 9 | "source": "https://github.com/inouet/kettle" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Taiji Inoue", 14 | "email": "inudog@gmail.com", 15 | "role": "Maintainer" 16 | } 17 | ], 18 | "license": [ 19 | "MIT" 20 | ], 21 | "require": { 22 | "php": ">=5.4.0", 23 | "aws/aws-sdk-php": "3.*" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "3.7.*" 27 | }, 28 | "autoload": { 29 | "classmap": ["src/kettle.php"] 30 | }, 31 | "bin": ["bin/kettle-skeleton.php"] 32 | } 33 | 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014, Taiji Inoue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kettle 2 | ====== 3 | 4 | Kettle is a lightweight object-dynamodb mapper for PHP. 5 | Kettle provides a simple interface to Amazon DynamoDB. 6 | 7 | See Some Code 8 | ------------------- 9 | 10 | ```php 11 | findOne(10); 15 | $user->name = 'John'; 16 | $user->save(); 17 | 18 | 19 | $tweets = ORM::factory('Tweet')->where('user_id', 10) 20 | ->findMany(); 21 | 22 | foreach ($tweets as $tweet) { 23 | echo $tweet->text . PHP_EOL; 24 | } 25 | 26 | ``` 27 | 28 | 1. Installation 29 | ------------------- 30 | 31 | Package is available on Packagist, you can install it using Composer. 32 | 33 | ``` 34 | $ cat < composer.json 35 | { 36 | "require": { 37 | "kettle/dynamodb-orm": "0.2.*" 38 | } 39 | } 40 | EOF 41 | 42 | $ composer install 43 | ``` 44 | 45 | 46 | 2. Configuration 47 | ------------------- 48 | 49 | ```php 50 | 'N', // user_id is number 87 | 'name' => 'S', // name is string 88 | 'age' => 'N', 89 | 'country' => 'S', 90 | ); 91 | } 92 | 93 | 94 | ``` 95 | 96 | If you use a generator, you can also create an class as follows. 97 | 98 | ``` 99 | $ php bin/kettle-skeleton.php --table-name user --region ap-northeast-1 > User.php 100 | ``` 101 | 102 | Table must have been created in advance. 103 | Because this generator generates a class based on the information and data collected by the "describeTable" and "scan" operation. 104 | 105 | 4. Create 106 | ------------------- 107 | 108 | ```php 109 | create(); 112 | $user->user_id = 1; 113 | $user->name = 'John'; 114 | $user->age = 20; 115 | $user->save(); 116 | 117 | ``` 118 | 119 | 5. Retrieve 120 | ------------------- 121 | 122 | ```php 123 | findOne(1); 126 | echo $user->name. PHP_EOL; 127 | 128 | print_r($user->asArray()); 129 | 130 | ``` 131 | 132 | 6. Update 133 | ------------------- 134 | 135 | ```php 136 | findOne(1); 139 | $user->age = 21; 140 | $user->save(); 141 | 142 | ``` 143 | 144 | 7. Delete 145 | ------------------- 146 | 147 | ```php 148 | findOne(1); 151 | $user->delete(); 152 | 153 | ``` 154 | 155 | 156 | 8. Find 157 | ------------------- 158 | 159 | ```php 160 | where('user_id', 1) 164 | ->where('timestamp', '>', 1397264554) 165 | ->findMany(); 166 | 167 | foreach ($tweets as $tweet) { 168 | echo $tweet->text . PHP_EOL; 169 | } 170 | 171 | ``` 172 | 173 | 9. Find first record 174 | ------------------- 175 | 176 | ```php 177 | where('user_id', 1) 181 | ->where('timestamp', '>', 1397264554) 182 | ->findFirst(); 183 | 184 | echo $tweet->text . PHP_EOL; 185 | 186 | ``` 187 | 188 | 189 | 10. Find by Global Secondary Index 190 | ------------------- 191 | 192 | ```php 193 | where('country', 'Japan') 197 | ->where('age', '>=', 20) 198 | ->index('country-age-index') // specify index name 199 | ->findMany(); 200 | 201 | ``` 202 | 203 | 204 | 11. Query Filtering 205 | ------------------- 206 | 207 | ```php 208 | where('user_id', 1) 212 | ->filter('is_deleted', 0) // using filter 213 | ->findMany(); 214 | 215 | ``` 216 | -------------------------------------------------------------------------------- /bin/kettle-skeleton.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | TableName.php 9 | * 10 | */ 11 | 12 | $files = [ 13 | __DIR__ . '/../vendor/autoload.php', 14 | __DIR__ . '/../../../autoload.php', 15 | ]; 16 | 17 | foreach ($files as $file) { 18 | if (file_exists($file)) { 19 | require $file; 20 | break; 21 | } 22 | } 23 | 24 | use Aws\DynamoDb\DynamoDbClient; 25 | 26 | /** 27 | * main 28 | * 29 | * @void 30 | */ 31 | function main() 32 | { 33 | $args = get_args(); 34 | 35 | if (!isset($args['table-name'])) { 36 | echo "ERROR: argument 'table-name' is required.\n\n"; 37 | print_usage(); 38 | die(); 39 | } 40 | $region = 'us-west-2'; 41 | if (isset($args['region'])) { 42 | $region = $args['region']; 43 | } 44 | 45 | $table_name = $args['table-name']; 46 | 47 | $params = [ 48 | 'version' => '2012-08-10', 49 | 'region' => $region, 50 | ]; 51 | 52 | $client = new DynamoDbClient($params); 53 | 54 | // Table Descriptions 55 | try { 56 | $result = $client->describeTable(['TableName' => $table_name]); 57 | } catch (\Exception $e) { 58 | echo "DynamoDB ERROR: " . $e->getMessage() . "\n\n"; 59 | print_usage(); 60 | die(); 61 | } 62 | 63 | // AttributeDefinitions 64 | $attribute_definitions = $result->search('Table.AttributeDefinitions[]'); 65 | 66 | // KeySchema 67 | $key_schema = $result->search('Table.KeySchema[]'); 68 | 69 | // GSI 70 | $global_secondary_indexes = $result->search('Table.GlobalSecondaryIndexes[]'); 71 | 72 | // LSI 73 | $local_secondary_indexes = $result->search('Table.LocalSecondaryIndexes[]'); 74 | 75 | // Items 76 | $result_items = $client->scan(['TableName' => $table_name, 'Limit' => 10]); 77 | $items = $result_items->search("Items[]"); 78 | 79 | $template = <<<'EOT' 80 | $type_value) { 148 | foreach ($type_value as $type => $value) { 149 | $schema[$attribute_name] = $type; 150 | } 151 | } 152 | } 153 | 154 | // From attribute definitions 155 | foreach ($attribute_definitions as $row) { 156 | $attribute_name = $row['AttributeName']; 157 | $attribute_type = $row['AttributeType']; 158 | $schema[$attribute_name] = $attribute_type; 159 | } 160 | 161 | // max length 162 | $max_attribute_len = 0; 163 | foreach ($schema as $k => $v) { 164 | if (strlen($k) > $max_attribute_len) { 165 | $max_attribute_len = strlen($k); 166 | } 167 | } 168 | ksort($schema); 169 | 170 | $schema_code = "[\n"; 171 | foreach ($schema as $key => $val) { 172 | $key = str_pad("'{$key}'", $max_attribute_len + 2, " ", STR_PAD_RIGHT); 173 | $schema_code .= " {$key} => '{$val}',\n"; 174 | } 175 | $schema_code .= " ]"; 176 | 177 | $replace['%SCHEMA%'] = $schema_code; 178 | 179 | //--------------------------------------- 180 | // GSI 181 | //--------------------------------------- 182 | 183 | $code = build_index_code($global_secondary_indexes); 184 | if ($global_secondary_indexes) { 185 | $replace['//protected $_global_secondary_indexes'] = 'protected $_global_secondary_indexes'; 186 | } 187 | $replace['%GLOBAL_SECONDARY_INDEXES%'] = $code; 188 | 189 | //--------------------------------------- 190 | // LSI 191 | //--------------------------------------- 192 | 193 | $code = build_index_code($local_secondary_indexes); 194 | if ($local_secondary_indexes) { 195 | $replace['//protected $_local_secondary_indexes'] = 'protected $_local_secondary_indexes'; 196 | } 197 | $replace['%LOCAL_SECONDARY_INDEXES%'] = $code; 198 | 199 | //--------------------------------------- 200 | // @property 201 | //--------------------------------------- 202 | 203 | $properties = []; 204 | $types = [ 205 | 'S' => 'string', 206 | 'N' => 'int', 207 | 'B' => 'string' 208 | ]; 209 | foreach ($schema as $key => $val) { 210 | $type = isset($types[$val]) ? $types[$val] : 'string'; 211 | $properties[] = sprintf("@property %s \$%s\n", str_pad($type, 6, ' ', STR_PAD_RIGHT), $key); 212 | } 213 | 214 | $property_code = join(' * ', $properties); 215 | $replace['%PROPERTY%'] = rtrim($property_code); 216 | 217 | $result = build_skeleton_code($template, $replace); 218 | echo $result; 219 | } 220 | 221 | /** 222 | * Convert table_name to ClassName 223 | * 224 | * @param string $table_name 225 | * 226 | * @return string 227 | */ 228 | function table_name_to_class_name($table_name) 229 | { 230 | $table_name = ucwords(str_replace('_', ' ', $table_name)); 231 | $table_name = str_replace(' ', '', $table_name); 232 | return $table_name; 233 | } 234 | 235 | /** 236 | * Build index code 237 | * 238 | * @param array|null $indexes 239 | * 240 | * @return string 241 | */ 242 | function build_index_code($indexes) 243 | { 244 | if (!$indexes) { 245 | return '[]'; 246 | } 247 | 248 | $code = "[\n"; 249 | foreach ($indexes as $index) { 250 | $code .= " '{$index['IndexName']}' => ["; 251 | $_keys = []; 252 | foreach ($index['KeySchema'] as $row) { 253 | if ($row['KeyType'] == 'HASH') { 254 | $_keys[0] = $row['AttributeName']; 255 | } 256 | if ($row['KeyType'] == 'RANGE') { 257 | $_keys[1] = $row['AttributeName']; 258 | } 259 | } 260 | $code .= "'" . join("','", $_keys) . "'"; 261 | $code .= "],\n"; 262 | } 263 | $code .= " ]"; 264 | 265 | return $code; 266 | } 267 | 268 | /** 269 | * Build skeleton code 270 | * 271 | * @param string $template 272 | * @param array $replace 273 | * 274 | * @return string 275 | */ 276 | function build_skeleton_code($template, array $replace) 277 | { 278 | foreach ($replace as $key => $value) { 279 | $template = str_replace($key, $value, $template); 280 | } 281 | return $template; 282 | } 283 | 284 | 285 | function get_args() 286 | { 287 | global $argv; 288 | 289 | $args = []; 290 | $current_key = null; 291 | for ($i = 1, $total = count($argv); $i < $total; $i++) { 292 | if ($i % 2) { 293 | if (substr($argv[$i], 0, 2) == '--') { 294 | $current_key = str_replace('--', '', $argv[$i]); 295 | } else { 296 | $current_key = trim($argv[$i]); 297 | } 298 | } else { 299 | $args[$current_key] = $argv[$i]; 300 | $current_key = null; 301 | } 302 | } 303 | return $args; 304 | } 305 | 306 | function print_usage() 307 | { 308 | $usage = <<<'EOT' 309 | Usage: 310 | bin/kettle-skeleton.php --table-name TABLE_NAME --region ap-northeast-1 311 | 312 | EOT; 313 | echo $usage; 314 | } 315 | 316 | main(); 317 | -------------------------------------------------------------------------------- /src/kettle.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | 17 | namespace Kettle; 18 | 19 | use Aws\Credentials\Credentials; 20 | use Aws\DynamoDb\DynamoDbClient; 21 | 22 | class ORM 23 | { 24 | 25 | const DEFAULT_CONNECTION = 'default'; 26 | 27 | // -------------------------- 28 | 29 | /** 30 | * Class configuration 31 | * 32 | * @var array 33 | * - key 34 | * - secret 35 | * - region 36 | * - endpoint/base_url (for DynamoDB local) 37 | * - version 38 | * - scheme 39 | * - profile (AWS Credential Name) 40 | */ 41 | protected static $_config_default = array( 42 | 'key' => null, 43 | 'secret' => null, 44 | 'region' => null, 45 | 'base_url' => null, 46 | 'endpoint' => null, 47 | 'version' => '2012-08-10', 48 | 'scheme' => 'https', 49 | 'profile' => null, 50 | ); 51 | 52 | protected static $_config = array(); 53 | 54 | /** 55 | * @var \Aws\DynamoDb\DynamoDbClient[] 56 | */ 57 | protected static $_client = array(); 58 | 59 | // Log of all queries run, mapped by connection key, only populated if logging is enabled 60 | protected static $_query_log = array(); 61 | 62 | // -------------------------- 63 | 64 | // DynamoDB TableName 65 | protected $_table_name; 66 | 67 | // HashKey 68 | protected $_hash_key; 69 | 70 | // RangeKey 71 | protected $_range_key; 72 | 73 | // ConnectionName 74 | protected $_connection_name = self::DEFAULT_CONNECTION; 75 | 76 | // GlobalSecondaryIndex 77 | protected $_global_secondary_indexes = []; 78 | 79 | // LocalSecondaryIndex 80 | protected $_local_secondary_indexes = []; 81 | 82 | /** 83 | * data schema 84 | * 85 | * @var array 86 | * 87 | * ex) 88 | * $_schema = array( 89 | * 'field_name_1' => 'S', 90 | * 'field_name_2' => 'N', 91 | * ); 92 | */ 93 | protected $_schema = array(); 94 | 95 | /** 96 | * DynamoDB record data is stored here as an associative array 97 | * 98 | * @var array 99 | */ 100 | protected $_data = array(); 101 | 102 | protected $_data_original = array(); 103 | 104 | // LIMIT (QueryParameter) 105 | protected $_limit = null; 106 | 107 | // ExclusiveStartKey (QueryParameter) 108 | protected $_exclusive_start_key = null; 109 | 110 | // IndexName (QueryParameter) 111 | protected $_query_index_name = null; 112 | 113 | // ConsistentRead (QueryParameter) 114 | protected $_consistent_read = false; 115 | 116 | // LastEvaluatedKey (QueryResponse) 117 | protected $_last_evaluated_key = null; 118 | 119 | // Count (QueryResponse) 120 | public $_result_count = null; 121 | 122 | /** 123 | * Array of WHERE clauses (QueryParameter) 124 | * 125 | * $_where_conditions = array( 126 | * 0 => array('name', 'EQ', 'John'), 127 | * 1 => array('age', 'GT', 20), 128 | * ); 129 | */ 130 | protected $_where_conditions = array(); 131 | 132 | /** 133 | * Array of Filter clauses (QueryParameter) 134 | * 135 | * $_filter_conditions = array( 136 | * 0 => array('country', 'IN', array('Japan', 'Korea')) 137 | * 1 => array('age', 'GT', 20), 138 | * ); 139 | */ 140 | protected $_filter_conditions = array(); 141 | 142 | // Is this a new object (has create() been called)? 143 | protected $_is_new = false; 144 | 145 | //----------------------------------------------- 146 | // PUBLIC METHODS 147 | //----------------------------------------------- 148 | public static function configure($key, $value, $connection_name = self::DEFAULT_CONNECTION) 149 | { 150 | if (!isset(self::$_config[$connection_name])) { 151 | self::$_config[$connection_name] = self::$_config_default; 152 | } 153 | self::$_config[$connection_name][$key] = $value; 154 | } 155 | 156 | /** 157 | * Retrieve configuration options by key, or as whole array. 158 | * 159 | * @param string $key 160 | * 161 | * @return string|array 162 | */ 163 | public static function getConfig($key = null, $connection_name = self::DEFAULT_CONNECTION) 164 | { 165 | if ($key) { 166 | return isset(self::$_config[$connection_name][$key]) ? self::$_config[$connection_name][$key] : null; 167 | } else { 168 | return self::$_config[$connection_name]; 169 | } 170 | } 171 | 172 | /** 173 | * Get an array containing all the queries and response 174 | * Only works if the 'logging' config option is 175 | * set to true. Otherwise, returned array will be empty. 176 | * 177 | * @return array 178 | * @deprecated 179 | */ 180 | public static function getQueryLog() 181 | { 182 | if (isset(self::$_query_log)) { 183 | return self::$_query_log; 184 | } 185 | return array(); 186 | } 187 | 188 | /** 189 | * Get last query 190 | * 191 | * @return array 192 | * @deprecated 193 | */ 194 | public static function getLastQuery() 195 | { 196 | if (!isset(self::$_query_log)) { 197 | return array(); 198 | } 199 | return end(self::$_query_log); 200 | } 201 | 202 | /** 203 | * Get connection name 204 | * 205 | * @return string 206 | */ 207 | public function getConnectionName() 208 | { 209 | return $this->_connection_name; 210 | } 211 | 212 | public function setConnectionName($connection_name) 213 | { 214 | $this->_connection_name = $connection_name; 215 | } 216 | 217 | /** 218 | * Get number of records that matched the query 219 | * 220 | * @return int 221 | */ 222 | public function getCount() 223 | { 224 | return $this->_result_count; 225 | } 226 | 227 | /** 228 | * Retrieve single result using hash_key and range_key 229 | * 230 | * @param string $hash_key_value 231 | * @param string $range_key_value 232 | * @param array $options 233 | * 234 | * @return $this instance of the ORM sub class 235 | * @throws \Exception 236 | */ 237 | public function findOne($hash_key_value, $range_key_value = null, array $options = array()) 238 | { 239 | $conditions = array( 240 | $this->_hash_key => $hash_key_value, 241 | ); 242 | 243 | if ($range_key_value) { 244 | if (!$this->_range_key) { 245 | throw new \Exception("Range key is not defined."); 246 | } 247 | $conditions[$this->_range_key] = $range_key_value; 248 | } 249 | 250 | $key = $this->_formatAttributes($conditions); 251 | $args = array( 252 | 'TableName' => $this->_table_name, 253 | 'Key' => $key, 254 | 'ConsistentRead' => $this->_consistent_read, 255 | 'ReturnConsumedCapacity' => 'TOTAL', 256 | // 'AttributesToGet' 257 | ); 258 | 259 | 260 | // Merge $options to $args 261 | $option_names = array('AttributesToGet', 'ReturnConsumedCapacity'); 262 | foreach ($option_names as $option_name) { 263 | if (isset($options[$option_name])) { 264 | $args[$option_name] = $options[$option_name]; 265 | } 266 | } 267 | 268 | $_client = $this->getClient(); 269 | $item = $_client->getItem($args); 270 | 271 | if (!is_array($item['Item'])) { 272 | return null; 273 | } 274 | 275 | $result = $this->_formatResult($item['Item']); 276 | 277 | $class_name = get_called_class(); 278 | $instance = self::factory($class_name, $this->_connection_name); 279 | $instance->hydrate($result); 280 | return $instance; 281 | } 282 | 283 | /** 284 | * Retrieve multiple results using query 285 | * 286 | * @param array $options 287 | * 288 | * @return $this[] 289 | */ 290 | public function findMany(array $options = array()) 291 | { 292 | $conditions = $this->_buildConditions($this->_where_conditions); 293 | if ($this->_filter_conditions) { 294 | $filter_conditions = $this->_buildConditions($this->_filter_conditions); 295 | $options['QueryFilter'] = $filter_conditions; 296 | } 297 | $result = $this->query($conditions, $options); 298 | 299 | // scan($tableName, $filter, $limit = null) 300 | $array = array(); 301 | $class_name = get_called_class(); 302 | foreach ($result as $row) { 303 | $instance = self::factory($class_name, $this->_connection_name); 304 | $instance->hydrate($row); 305 | $array[] = $instance; 306 | } 307 | return $array; 308 | } 309 | 310 | /** 311 | * Retrieve first result using query 312 | * 313 | * @param array $options 314 | * 315 | * @return $this|null 316 | */ 317 | public function findFirst(array $options = array()) 318 | { 319 | // $this->_limit = 1; # FIX: bug at using filter 320 | $array = $this->findMany($options); 321 | if (is_array($array) && sizeof($array) > 0) { 322 | return $array[0]; 323 | } 324 | return null; 325 | } 326 | 327 | /** 328 | * Save data to the DynamoDB 329 | * 330 | * @param array $options 331 | * 332 | * $options = array( 333 | * 'ReturnValues' => 'NONE', // NONE|ALL_OLD|UPDATED_OLD|ALL_NEW|UPDATED_NEW 334 | * 'ReturnConsumedCapacity' => 'NONE', // INDEXES|TOTAL|NONE 335 | * 'ReturnItemCollectionMetrics' => 'NONE', // SIZE|NONE 336 | * 337 | * 'ForceUpdate' => false, // If true No ConditionalCheck 338 | * ); 339 | * 340 | * @return \Aws\Result 341 | */ 342 | public function save(array $options = array()) 343 | { 344 | $values = $this->_compact($this->_data); 345 | $expected = array(); 346 | 347 | if ($this->_is_new) { // insert 348 | if (!isset($options['ForceUpdate']) || !$options['ForceUpdate']) { 349 | // Expect duplicate error if already exists. 350 | $exists = array(); 351 | foreach ($this->_schema as $key => $value) { 352 | $exists[$key] = false; 353 | } 354 | $options['Exists'] = $exists; 355 | } 356 | $result = $this->putItem($values, $options, $expected); 357 | $this->_is_new = false; 358 | } else { // update 359 | if (!isset($options['ForceUpdate']) || !$options['ForceUpdate']) { 360 | // If data is modified by different instance or process, 361 | // throw Aws\DynamoDb\Exception\ConditionalCheckFailedException 362 | $expected = $this->_data_original; 363 | } 364 | $result = $this->putItem($values, $options, $expected); 365 | //$result = $this->updateItem($values, $options, $expected); 366 | } 367 | 368 | return $result; 369 | } 370 | 371 | /** 372 | * Delete record 373 | * 374 | * @return mixed 375 | * @link http://docs.aws.amazon.com/aws-sdk-php/latest/class-Aws.DynamoDb.DynamoDbClient.html#_deleteItem 376 | */ 377 | public function delete() 378 | { 379 | $conditions = $this->_getKeyConditions(); 380 | $args = array( 381 | 'TableName' => $this->_table_name, 382 | 'Key' => $conditions, 383 | 'ReturnValues' => 'ALL_OLD', 384 | ); 385 | 386 | $_client = $this->getClient(); 387 | $result = $_client->deleteItem($args); 388 | return $result; 389 | } 390 | 391 | /** 392 | * Add a LIMIT to the query 393 | * 394 | * @param int $limit 395 | * 396 | * @return $this 397 | */ 398 | public function limit($limit) 399 | { 400 | $this->_limit = $limit; 401 | return $this; 402 | } 403 | 404 | /** 405 | * Set IndexName (Query Parameter) 406 | * 407 | * @param string $index_name 408 | * 409 | * @return $this 410 | */ 411 | public function index($index_name) 412 | { 413 | $this->_query_index_name = $index_name; 414 | return $this; 415 | } 416 | 417 | /** 418 | * Set ConsistentRead Option to the query 419 | * 420 | * @param bool $consistent_read 421 | * 422 | * @return $this 423 | */ 424 | public function consistent($consistent_read = true) 425 | { 426 | $this->_consistent_read = $consistent_read; 427 | return $this; 428 | } 429 | 430 | /** 431 | * The LastEvaluatedKey is only provided if the results exceed 1 MB, or if you have used Limit. 432 | * 433 | * @return mixed array|null 434 | */ 435 | public function getLastEvaluatedKey() 436 | { 437 | return $this->_last_evaluated_key; 438 | } 439 | 440 | /** 441 | * Add a WHERE column = value clause 442 | * 443 | * @param string $key 444 | * @param string $value or $operator 445 | * @param mixed $value 446 | * 447 | * Usage: 448 | * $user->where('name', 'John'); 449 | * $user->where('age', '>', 20); 450 | * 451 | * @return $this 452 | */ 453 | public function where() 454 | { 455 | $args = func_get_args(); 456 | $key = $args[0]; 457 | if (func_num_args() == 2) { 458 | $value = $args[1]; 459 | $operator = 'EQ'; 460 | } else { 461 | $value = $args[2]; 462 | $operator = $this->_convertOperator($args[1]); 463 | } 464 | 465 | $this->_where_conditions[] = array($key, $operator, $value); 466 | return $this; 467 | } 468 | 469 | /** 470 | * Add a Filter column = value clause 471 | * 472 | * @param string $key 473 | * @param string $value or $operator 474 | * @param mixed $value 475 | * 476 | * Usage: 477 | * $user->filter('name', 'John'); 478 | * $user->filter('age', '>', 20); 479 | * 480 | * @return $this 481 | */ 482 | public function filter() 483 | { 484 | $args = func_get_args(); 485 | $key = $args[0]; 486 | if (func_num_args() == 2) { 487 | $value = $args[1]; 488 | $operator = 'EQ'; 489 | } else { 490 | $value = $args[2]; 491 | $operator = $this->_convertOperator($args[1]); 492 | } 493 | 494 | $this->_filter_conditions[] = array($key, $operator, $value); 495 | return $this; 496 | } 497 | 498 | /** 499 | * Set a property to a particular value on this object. 500 | * 501 | * @param string $key 502 | * @param mixed $value 503 | */ 504 | public function set($key, $value) 505 | { 506 | if (array_key_exists($key, $this->_schema)) { 507 | $type = $this->_getDataType($key); 508 | if ($type == 'S' || $type == 'N') { 509 | $value = strval($value); 510 | } 511 | $this->_data[$key] = $value; 512 | } 513 | } 514 | 515 | /** 516 | * Return the value of a property of this object (dynamodb row) or null if not present. 517 | * 518 | * @param string $key 519 | * 520 | * @return mixed 521 | */ 522 | public function get($key) 523 | { 524 | return isset($this->_data[$key]) ? $this->_data[$key] : null; 525 | } 526 | 527 | /** 528 | * Remove value from set type field (String Set, Number Set, Binary Set) 529 | * 530 | * @param $key 531 | * @param $value 532 | */ 533 | public function setRemove($key, $value) 534 | { 535 | $type = $this->_getDataType($key); 536 | if ($type == 'SS' || $type == 'NS' || $type == 'BS') { 537 | $array = $this->get($key); 538 | $index = array_search($value, $array); 539 | if (!is_null($index)) { 540 | unset($array[$index]); 541 | } 542 | $array = array_values($array); 543 | $this->set($key, $array); 544 | } 545 | } 546 | 547 | /** 548 | * Add value to set type field (String Set, Number Set, Binary Set) 549 | * 550 | * @param $key 551 | * @param $value 552 | */ 553 | public function setAdd($key, $value) 554 | { 555 | $type = $this->_getDataType($key); 556 | if ($type == 'SS' || $type == 'NS') { 557 | $this->_data[$key][] = strval($value); 558 | } elseif ($type == 'BS') { 559 | $this->_data[$key][] = $value; 560 | } 561 | } 562 | 563 | /** 564 | * @param array $data 565 | * 566 | * @return $this 567 | */ 568 | public function create(array $data = array()) 569 | { 570 | $this->_is_new = true; 571 | return $this->hydrate($data); 572 | } 573 | 574 | /** 575 | * @param array $data 576 | * 577 | * @return $this 578 | */ 579 | public function hydrate(array $data = array()) 580 | { 581 | foreach ($data as $key => $value) { 582 | $this->set($key, $value); 583 | } 584 | $this->_data_original = $this->_data; 585 | return $this; 586 | } 587 | 588 | /** 589 | * Return the raw data wrapped by this ORM instance as an associative array. 590 | * 591 | * @return array 592 | */ 593 | public function asArray() 594 | { 595 | return $this->_data; 596 | } 597 | 598 | /** 599 | * Return DynamoDbClient instance 600 | * 601 | * @return \Aws\DynamoDb\DynamoDbClient 602 | */ 603 | public function getClient() 604 | { 605 | $client = self::$_client[$this->_connection_name]; 606 | return $client; 607 | } 608 | 609 | /** 610 | * query 611 | * 612 | * @param array $conditions 613 | * @param array $options 614 | * 615 | * @return array 616 | * 617 | * @link http://docs.aws.amazon.com/aws-sdk-php/latest/class-Aws.DynamoDb.DynamoDbClient.html#_query 618 | */ 619 | public function query(array $conditions, array $options = array()) 620 | { 621 | $args = array( 622 | 'TableName' => $this->_table_name, 623 | 'KeyConditions' => $conditions, 624 | 'ScanIndexForward' => true, 625 | // Select: ALL_ATTRIBUTES|ALL_PROJECTED_ATTRIBUTES|SPECIFIC_ATTRIBUTES|COUNT 626 | 'Select' => 'ALL_ATTRIBUTES', 627 | 'ReturnConsumedCapacity' => 'TOTAL', 628 | 'ConsistentRead' => $this->_consistent_read, 629 | //'AttributesToGet' 630 | //'ExclusiveStartKey' 631 | //'IndexName' 632 | ); 633 | 634 | // Merge $options to $args 635 | $option_names = array('ScanIndexForward', 'QueryFilter'); 636 | foreach ($option_names as $option_name) { 637 | if (isset($options[$option_name])) { 638 | $args[$option_name] = $options[$option_name]; 639 | } 640 | } 641 | 642 | // if IndexName is specified 643 | if ($this->_query_index_name) { 644 | $args['IndexName'] = $this->_query_index_name; 645 | } 646 | 647 | if (intval($this->_limit) > 0) { // Has limit 648 | // if ExclusiveStartKey is set 649 | if ($this->_exclusive_start_key) { 650 | $exclusive_start_key = $this->_formatAttributes($this->_exclusive_start_key); 651 | $args['ExclusiveStartKey'] = $exclusive_start_key; 652 | } 653 | 654 | $args['Limit'] = intval($this->_limit); 655 | 656 | $_client = $this->getClient(); 657 | $result = $_client->query($args); 658 | 659 | // $result is "Guzzle\Service\Resource\Model" 660 | // and $result has next keys 661 | // - Count 662 | // - Items 663 | // - ScannedCount 664 | // - LastEvaluatedKey 665 | $items = $result['Items']; 666 | 667 | // Set LastEvaluatedKey 668 | $last_evaluated_key = null; 669 | if (isset($result['LastEvaluatedKey'])) { 670 | $last_evaluated_key = $this->_formatResult($result['LastEvaluatedKey']); 671 | } 672 | $this->_last_evaluated_key = $last_evaluated_key; 673 | 674 | // Set Count 675 | $result_count = null; 676 | if (isset($result['Count'])) { 677 | $result_count = $result['Count']; 678 | } 679 | $this->_result_count = $result_count; 680 | 681 | } else { // No limit (Use Iterator) 682 | $_client = $this->getClient(); 683 | $iterator = $_client->getIterator('Query', $args); 684 | 685 | // $iterator is "Aws\Common\Iterator\AwsResourceIterator" 686 | $items = array(); 687 | foreach ($iterator as $item) { 688 | $items[] = $item; 689 | } 690 | 691 | // Set Count 692 | $this->_result_count = count($items); 693 | 694 | } 695 | 696 | return $this->_formatResults($items); 697 | } 698 | 699 | /** 700 | * Retrieve all records using scan 701 | * 702 | * @param array $options 703 | * 704 | * @return $this[] 705 | */ 706 | public function findAll(array $options = array()) 707 | { 708 | if ($this->_filter_conditions) { 709 | $filter_conditions = $this->_buildConditions($this->_filter_conditions); 710 | $options['ScanFilter'] = $filter_conditions; 711 | } 712 | $result = $this->scan($options); 713 | $array = array(); 714 | $class_name = get_called_class(); 715 | foreach ($result as $row) { 716 | $instance = self::factory($class_name, $this->_connection_name); 717 | $instance->hydrate($row); 718 | $array[] = $instance; 719 | } 720 | return $array; 721 | } 722 | 723 | /** 724 | * scan 725 | * 726 | * @param array $options 727 | * 728 | * @return array 729 | * 730 | */ 731 | public function scan(array $options = array()) 732 | { 733 | $options['TableName'] = $this->_table_name; 734 | $_client = $this->getClient(); 735 | 736 | $iterator = $_client->getIterator('Scan', $options); 737 | $items = array(); 738 | foreach ($iterator as $item) { 739 | $items[] = $item; 740 | } 741 | return $this->_formatResults($items); 742 | } 743 | 744 | /** 745 | * putItem 746 | * 747 | * @param array $values 748 | * @param array $options 749 | * @param array $expected 750 | * 751 | * @return \Aws\Result 752 | * 753 | * @link http://docs.aws.amazon.com/aws-sdk-php/latest/class-Aws.DynamoDb.DynamoDbClient.html#_putItem 754 | */ 755 | public function putItem(array $values, array $options = array(), array $expected = array()) 756 | { 757 | $args = array( 758 | 'TableName' => $this->_table_name, 759 | 'Item' => $this->_formatAttributes($values), 760 | //'ReturnValues' => 'ALL_NEW', 761 | 'ReturnConsumedCapacity' => 'TOTAL', 762 | 'ReturnItemCollectionMetrics' => 'SIZE', 763 | ); 764 | 765 | // Set Expected if exists 766 | if ($expected || isset($options['Exists'])) { 767 | $exists = isset($options['Exists']) ? $options['Exists'] : array(); 768 | $args['Expected'] = $this->_formatAttributeExpected($expected, $exists); 769 | } 770 | 771 | // Merge $options to $args 772 | $option_names = array('ReturnValues', 'ReturnConsumedCapacity', 'ReturnItemCollectionMetrics'); 773 | foreach ($option_names as $option_name) { 774 | if (isset($options[$option_name])) { 775 | $args[$option_name] = $options[$option_name]; 776 | } 777 | } 778 | 779 | $_client = $this->getClient(); 780 | $item = $_client->putItem($args); 781 | 782 | return $item; 783 | } 784 | 785 | /** 786 | * updateItem 787 | * 788 | * @param array $values associative array 789 | * 790 | * $values = array( 791 | * 'name' => 'John', 792 | * 'age' => 30, 793 | * ); 794 | * 795 | * @param array $options 796 | * 797 | * $options = array( 798 | * 'ReturnValues' => 'string', 799 | * 'ReturnConsumedCapacity' => 'string', 800 | * 'ReturnItemCollectionMetrics' => 'string', 801 | * 'Action' => array('age' => 'ADD'), 802 | * 'Exists' => array('age' => true), 803 | * ); 804 | * 805 | * @param array $expected 806 | * 807 | * @return \AWS\Result 808 | * 809 | * @link http://docs.aws.amazon.com/aws-sdk-php/latest/class-Aws.DynamoDb.DynamoDbClient.html#_updateItem 810 | */ 811 | public function updateItem(array $values, array $options = array(), array $expected = array()) 812 | { 813 | $conditions = $this->_getKeyConditions(); 814 | 815 | $action = array(); // Update Action (ADD|PUT|DELETE) 816 | if (isset($options['Action'])) { 817 | $action = $options['Action']; 818 | } 819 | 820 | $attributes = $this->_formatAttributeUpdates($values, $action); 821 | 822 | foreach ($conditions as $key => $value) { 823 | if (isset($attributes[$key])) { 824 | unset($attributes[$key]); 825 | } 826 | } 827 | $args = array( 828 | 'TableName' => $this->_table_name, 829 | 'Key' => $conditions, 830 | 'AttributeUpdates' => $attributes, 831 | 'ReturnValues' => 'ALL_NEW', 832 | 'ReturnConsumedCapacity' => 'TOTAL', 833 | 'ReturnItemCollectionMetrics' => 'SIZE', 834 | ); 835 | 836 | // Set Expected if exists 837 | if ($expected || isset($options['Exists'])) { 838 | $exists = isset($options['Exists']) ? $options['Exists'] : array(); 839 | $args['Expected'] = $this->_formatAttributeExpected($expected, $exists); 840 | } 841 | 842 | // Merge $options to $args 843 | $option_names = array('ReturnValues', 'ReturnConsumedCapacity', 'ReturnItemCollectionMetrics'); 844 | foreach ($option_names as $option_name) { 845 | if (isset($options[$option_name])) { 846 | $args[$option_name] = $options[$option_name]; 847 | } 848 | } 849 | 850 | $_client = $this->getClient(); 851 | $item = $_client->updateItem($args); 852 | 853 | return $item; 854 | } 855 | 856 | /** 857 | * Set ExclusiveStartKey for query parameter 858 | * 859 | * @param array $exclusive_start_key 860 | * 861 | * $exclusive_start_key = array( 862 | * 'key_name1' => 'value1', 863 | * 'key_name2' => 'value2', 864 | * ); 865 | * 866 | * @return $this 867 | */ 868 | public function setExclusiveStartKey(array $exclusive_start_key) 869 | { 870 | $this->_exclusive_start_key = $exclusive_start_key; 871 | return $this; 872 | } 873 | 874 | /** 875 | * Reset Where Conditions and Limit .. 876 | * 877 | * @return $this 878 | */ 879 | public function resetConditions() 880 | { 881 | $this->_limit = null; 882 | $this->_where_conditions = array(); 883 | $this->_filter_conditions = array(); 884 | $this->_exclusive_start_key = null; 885 | $this->_query_index_name = null; 886 | $this->_consistent_read = false; 887 | return $this; 888 | } 889 | 890 | /** 891 | * Get Hash Key 892 | * 893 | * @return string 894 | */ 895 | public function getHashKey() 896 | { 897 | return $this->_hash_key; 898 | } 899 | 900 | /** 901 | * Get Range Key 902 | * 903 | * @return string 904 | */ 905 | public function getRangeKey() 906 | { 907 | return $this->_range_key; 908 | } 909 | 910 | /** 911 | * Get Table Name 912 | * 913 | * @return string 914 | */ 915 | public function getTableName() 916 | { 917 | return $this->_table_name; 918 | } 919 | 920 | /** 921 | * Convert to DynamoDB Import/Export Format. 922 | * 923 | * @link http://docs.aws.amazon.com/datapipeline/latest/DeveloperGuide/dp-importexport-ddb-pipelinejson-verifydata2.html 924 | * 925 | * @return string 926 | */ 927 | public function toImportFormat() 928 | { 929 | $text = ""; 930 | $etx = chr(3); // ETX 931 | $stx = chr(2); // STX 932 | 933 | foreach ($this->_schema as $column => $type) { 934 | $value = $this->get($column); 935 | if (strlen($value) == 0) { 936 | continue; 937 | } 938 | // column_name≤ETX>{type:value} 939 | $data = array(strtolower($type) => $value); 940 | $json = json_encode($data); 941 | if (!$json) { 942 | continue; 943 | } 944 | $text .= $column . $etx . $json . $stx; 945 | } 946 | $text = rtrim($text, $stx); // remove last STX 947 | $text .= "\n"; 948 | return $text; 949 | } 950 | 951 | /** 952 | * Retrieve items in batches of up to 100 953 | * 954 | * @param array $key_values 955 | * 956 | * HashKey: [hash_key_value1, hash_key_value2 ..] 957 | * HashKey + RangeKey: [[hash_key_value1, range_key_value1] ...] 958 | * 959 | * @return $this[] 960 | */ 961 | public function batchGetItems(array $key_values) 962 | { 963 | $keys = array(); 964 | foreach ($key_values as $key_value) { 965 | if ($this->_range_key) { 966 | $conditions = array( 967 | $this->_hash_key => $key_value[0], 968 | $this->_range_key => $key_value[1] 969 | ); 970 | } else { 971 | $_id = is_array($key_value) ? $key_value[0] : $key_value; 972 | $conditions = array( 973 | $this->_hash_key => $_id 974 | ); 975 | } 976 | $keys[] = $this->_formatAttributes($conditions); 977 | } 978 | $_client = $this->getClient(); 979 | $result = $_client->batchGetItem( 980 | array( 981 | 'RequestItems' => array( 982 | $this->_table_name => array( 983 | 'Keys' => $keys, 984 | 'ConsistentRead' => true 985 | ) 986 | ) 987 | ) 988 | ); 989 | $items = $result->getPath("Responses/{$this->_table_name}"); 990 | $class_name = get_called_class(); 991 | $formatted_result = $this->_formatResults($items); 992 | 993 | $array = array(); 994 | foreach ($formatted_result as $row) { 995 | $instance = self::factory($class_name, $this->_connection_name); 996 | $instance->hydrate($row); 997 | $array[] = $instance; 998 | } 999 | return $array; 1000 | } 1001 | 1002 | /** 1003 | * Retrieve findMany result as array 1004 | * 1005 | * @param array $options 1006 | * 1007 | * @return array 1008 | */ 1009 | public function findArray(array $options = array()) 1010 | { 1011 | $entity_list = $this->findMany($options); 1012 | $result = array(); 1013 | foreach ($entity_list as $entity) { 1014 | $result[] = $entity->asArray(); 1015 | } 1016 | return $result; 1017 | } 1018 | 1019 | //----------------------------------------------- 1020 | // MAGIC METHODS 1021 | //----------------------------------------------- 1022 | 1023 | public function __get($key) 1024 | { 1025 | return $this->get($key); 1026 | } 1027 | 1028 | public function __set($key, $value) 1029 | { 1030 | $this->set($key, $value); 1031 | } 1032 | 1033 | public function __unset($key) 1034 | { 1035 | unset($this->_data[$key]); 1036 | } 1037 | 1038 | public function __isset($key) 1039 | { 1040 | return isset($this->_data[$key]); 1041 | } 1042 | 1043 | //----------------------------------------------- 1044 | // PUBLIC METHODS (STATIC) 1045 | //----------------------------------------------- 1046 | 1047 | /** 1048 | * @param string $class_name 1049 | * @param string $connection_name 1050 | * 1051 | * @return $this instance of the ORM sub class 1052 | */ 1053 | public static function factory($class_name, $connection_name = self::DEFAULT_CONNECTION) 1054 | { 1055 | self::_setupClient($connection_name); 1056 | 1057 | /** @var self $object */ 1058 | $object = new $class_name(); 1059 | $object->setConnectionName($connection_name); 1060 | return $object; 1061 | } 1062 | 1063 | //----------------------------------------------- 1064 | // PRIVATE METHODS 1065 | //----------------------------------------------- 1066 | protected function __construct() 1067 | { 1068 | 1069 | } 1070 | 1071 | /** 1072 | * Return primary key condition 1073 | * 1074 | * @return array $condition 1075 | * $condition = array( 1076 | * 'id' => array('S' => '10001'), 1077 | * 'time' => array('N' => '1397604993'), 1078 | * ); 1079 | */ 1080 | protected function _getKeyConditions() 1081 | { 1082 | $condition = array( 1083 | $this->_hash_key => $this->get($this->_hash_key) 1084 | ); 1085 | if ($this->_range_key) { 1086 | if ($this->get($this->_range_key)) { 1087 | $condition[$this->_range_key] = $this->get($this->_range_key); 1088 | } 1089 | } 1090 | $condition = $this->_formatAttributes($condition); 1091 | return $condition; 1092 | } 1093 | 1094 | /** 1095 | * _formatAttributes 1096 | * 1097 | * @param array $array 1098 | * 1099 | * $array = array( 1100 | * 'name' => 'John', 1101 | * 'age' => 20, 1102 | * ); 1103 | * 1104 | * @return array $result 1105 | * 1106 | * $result = array( 1107 | * 'name' => array('S' => 'John'), 1108 | * 'age' => array('N' => 20), 1109 | * ); 1110 | */ 1111 | protected function _formatAttributes($array) 1112 | { 1113 | $result = array(); 1114 | foreach ($array as $key => $value) { 1115 | $type = $this->_getDataType($key); 1116 | if ($type == 'S' || $type == 'N') { 1117 | $value = strval($value); 1118 | } 1119 | $result[$key] = array($type => $value); 1120 | } 1121 | return $result; 1122 | } 1123 | 1124 | /** 1125 | * Format attribute for Update 1126 | * 1127 | * @param array $array 1128 | * 1129 | * $array = array( 1130 | * 'name' => 'John', 1131 | * 'age' => 1, 1132 | * ); 1133 | * 1134 | * @param array $actions 1135 | * 1136 | * $actions = array( 1137 | * 'count' => 'ADD', // field_name => action_name 1138 | * ); 1139 | * 1140 | * @return array $result 1141 | * 1142 | * $result = array( 1143 | * 'name' => array( 1144 | * 'Action' => 'PUT', 1145 | * 'Value' => array('S' => 'John') 1146 | * ), 1147 | * 'count' => array( 1148 | * 'Action' => 'ADD', 1149 | * 'Value' => array('N' => 1) 1150 | * ), 1151 | * ); 1152 | */ 1153 | protected function _formatAttributeUpdates(array $array, array $actions = array()) 1154 | { 1155 | $result = array(); 1156 | foreach ($array as $key => $value) { 1157 | $type = $this->_getDataType($key); 1158 | $action = 'PUT'; // default 1159 | if (isset($actions[$key])) { 1160 | $action = $actions[$key]; // overwrite if $actions is set 1161 | } 1162 | $result[$key] = array( 1163 | 'Action' => $action, 1164 | 'Value' => array($type => $value), 1165 | ); 1166 | } 1167 | return $result; 1168 | } 1169 | 1170 | /** 1171 | * Format attribute for Expected 1172 | * 1173 | * @param array $array 1174 | * 1175 | * $array = array( 1176 | * 'name' => 'John', 1177 | * 'age' => 30, 1178 | * ); 1179 | * 1180 | * @param array $exists 1181 | * 1182 | * $exists = array( 1183 | * 'age' => true, // field_name => bool 1184 | * ); 1185 | * 1186 | * @return array 1187 | * 1188 | * $result = array( 1189 | * 'name' => array( 1190 | * 'Value' => array('S' => 'John') 1191 | * ), 1192 | * 'age' => array( 1193 | * 'Value' => array('N' => 30), 1194 | * 'Exists' => true 1195 | * ) 1196 | * ); 1197 | * 1198 | */ 1199 | protected function _formatAttributeExpected(array $array, array $exists = array()) 1200 | { 1201 | $result = array(); 1202 | foreach ($array as $key => $value) { 1203 | $type = $this->_getDataType($key); 1204 | $result[$key] = array( 1205 | 'Value' => array($type => $value) 1206 | ); 1207 | } 1208 | foreach ($exists as $key => $value) { 1209 | $result[$key]['Exists'] = $value; // set if $exists is set 1210 | } 1211 | return $result; 1212 | } 1213 | 1214 | 1215 | /** 1216 | * Convert result array to simple associative array 1217 | * 1218 | * @param array $items 1219 | * 1220 | * @return array 1221 | * @see ORM#_formatResult 1222 | */ 1223 | protected function _formatResults(array $items) 1224 | { 1225 | $result = array(); 1226 | foreach ($items as $item) { 1227 | $result[] = $this->_formatResult($item); 1228 | } 1229 | return $result; 1230 | } 1231 | 1232 | 1233 | /** 1234 | * Convert result array to simple associative array 1235 | * 1236 | * @param array $item 1237 | * 1238 | * $item = array( 1239 | * 'name' => array('S' => 'John'), 1240 | * 'age' => array('N' => 30) 1241 | * ); 1242 | * 1243 | * @return array $hash 1244 | * 1245 | * $hash = array( 1246 | * 'name' => 'John', 1247 | * 'age' => 30, 1248 | * ); 1249 | */ 1250 | protected function _formatResult(array $item) 1251 | { 1252 | $hash = array(); 1253 | foreach ($item as $key => $value) { 1254 | $values = array_values($value); 1255 | $hash[$key] = $values[0]; 1256 | } 1257 | return $hash; 1258 | } 1259 | 1260 | /** 1261 | * Build where conditions 1262 | * 1263 | * @param array $conditions 1264 | * 1265 | * $_where_conditions = array( 1266 | * 0 => array('name', 'EQ', 'John'), 1267 | * 1 => array('age', 'GT', 20), 1268 | * 2 => array('country', 'IN', array('Japan', 'Korea')) 1269 | * ); 1270 | * 1271 | * @return array $result 1272 | * 1273 | * $result = array( 1274 | * 'name' => array( 1275 | * 'ComparisonOperator' => 'EQ' 1276 | * 'AttributeValueList' => array( 1277 | * 0 => array( 1278 | * 'S' => 'John' 1279 | * ) 1280 | * ) 1281 | * ), 1282 | * : 1283 | * : 1284 | * ); 1285 | */ 1286 | public function _buildConditions($conditions) 1287 | { 1288 | $result = array(); 1289 | foreach ($conditions as $i => $condition) { 1290 | $key = $condition[0]; 1291 | $operator = $condition[1]; 1292 | $value = $condition[2]; 1293 | 1294 | if (!is_array($value)) { 1295 | $value = array((string)$value); 1296 | } 1297 | 1298 | $attributes = array(); 1299 | foreach ($value as $v) { 1300 | $attributes[] = array($this->_getDataType($key) => (string)$v); 1301 | } 1302 | $result[$key] = array( 1303 | 'ComparisonOperator' => $operator, 1304 | 'AttributeValueList' => $attributes, 1305 | ); 1306 | } 1307 | return $result; 1308 | } 1309 | 1310 | /** 1311 | * Convert operator by alias 1312 | * 1313 | * @param string $operator 1314 | * 1315 | * @return string $operator 1316 | */ 1317 | protected function _convertOperator($operator) 1318 | { 1319 | $alias = array( 1320 | '=' => 'EQ', 1321 | '!=' => 'NE', 1322 | '>' => 'GT', 1323 | '>=' => 'GE', 1324 | '<' => 'LT', 1325 | '<=' => 'LE', 1326 | '~' => 'BETWEEN', 1327 | '^' => 'BEGINS_WITH', 1328 | 'NOT_NULL' => 'NOT_NULL', 1329 | 'NULL' => 'NULL', 1330 | 'CONTAINS' => 'CONTAINS', 1331 | 'NOT_CONTAINS' => 'NOT_CONTAINS', 1332 | 'IN' => 'IN', 1333 | ); 1334 | if (isset($alias[$operator])) { 1335 | return $alias[$operator]; 1336 | } 1337 | if (in_array($operator, array_values($alias))) { 1338 | return $operator; 1339 | } 1340 | return 'EQ'; // default 1341 | } 1342 | 1343 | 1344 | /** 1345 | * Return data type using $_schema 1346 | * 1347 | * @param string $key 1348 | * 1349 | * @return string $type 1350 | * 1351 | * S: String 1352 | * N: Number 1353 | * B: Binary 1354 | * SS: A set of strings 1355 | * NS: A set of numbers 1356 | * BS: A set of binary 1357 | */ 1358 | protected function _getDataType($key) 1359 | { 1360 | $type = 'S'; 1361 | if (isset($this->_schema[$key])) { 1362 | $type = $this->_schema[$key]; 1363 | } 1364 | return $type; 1365 | } 1366 | 1367 | /** 1368 | * Removing all empty elements from a hash 1369 | * 1370 | * @param array $array 1371 | * 1372 | * @return array 1373 | */ 1374 | protected function _compact(array $array) 1375 | { 1376 | foreach ($array as $key => $value) { 1377 | if (is_null($value) || $value === '') { 1378 | unset($array[$key]); 1379 | } 1380 | } 1381 | return $array; 1382 | } 1383 | 1384 | //----------------------------------------------- 1385 | // PRIVATE METHODS (STATIC) 1386 | //----------------------------------------------- 1387 | 1388 | // called from static factory method. 1389 | protected static function _setupClient($connection_name = self::DEFAULT_CONNECTION) 1390 | { 1391 | if (!isset(self::$_client[$connection_name])) { 1392 | $params = self::getConfig(null, $connection_name); 1393 | 1394 | if (self::getConfig('key', $connection_name) && self::getConfig('secret', $connection_name)) { 1395 | $params['credentials'] = new Credentials( 1396 | self::getConfig('key', $connection_name), self::getConfig('secret', $connection_name) 1397 | ); 1398 | } 1399 | if (self::getConfig('base_url', $connection_name)) { 1400 | $params['endpoint'] = self::getConfig('base_url', $connection_name); 1401 | } 1402 | 1403 | $client = new DynamoDbClient($params); 1404 | self::$_client[$connection_name] = $client; 1405 | } 1406 | } 1407 | 1408 | /** 1409 | * @deprecated 1410 | */ 1411 | protected static function _logQuery($query, array $args, $response) 1412 | { 1413 | } 1414 | } 1415 | 1416 | class KettleException extends \Exception 1417 | { 1418 | } 1419 | --------------------------------------------------------------------------------