├── .codecov.yml ├── .editorconfig ├── .gitattributes ├── .github ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .styleci.yml ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── config └── dynamodb.php ├── phpcs.xml ├── phpunit.xml ├── schema.js └── src ├── ComparisonOperator.php ├── Concerns └── HasParsers.php ├── ConditionAnalyzer ├── Analyzer.php └── Index.php ├── DynamoDb ├── DynamoDbManager.php ├── ExecutableQuery.php └── QueryBuilder.php ├── DynamoDbClientInterface.php ├── DynamoDbClientService.php ├── DynamoDbCollection.php ├── DynamoDbModel.php ├── DynamoDbQueryBuilder.php ├── DynamoDbServiceProvider.php ├── EmptyAttributeFilter.php ├── Facades └── DynamoDb.php ├── InvalidQuery.php ├── ModelObserver.php ├── ModelTrait.php ├── NotSupportedException.php ├── Parsers ├── ConditionExpression.php ├── ExpressionAttributeNames.php ├── ExpressionAttributeValues.php ├── FilterExpression.php ├── KeyConditionExpression.php ├── Placeholder.php ├── ProjectionExpression.php └── UpdateExpression.php └── RawDynamoDbQuery.php /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | project: yes 12 | patch: yes 13 | changes: no 14 | 15 | comment: 16 | layout: "reach, diff, flags, files, footer" 17 | behavior: default 18 | require_changes: no 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.{yml,yaml}] 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | local export-ignore 2 | local_init.db export-ignore 3 | local_schema.js export-ignore 4 | local_test.db export-ignore 5 | tests export-ignore 6 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 29 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 1 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: false 13 | # Comment to post when closing a stale issue. Set to `false` to disable 14 | closeComment: > 15 | This issue has been automatically closed because it has not had any recent activity. 😨 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | php: 20 | - '7.3' 21 | - '7.4' 22 | - '8.0' 23 | laravel: 24 | - 7.* 25 | - 8.* 26 | prefer: 27 | - 'prefer-lowest' 28 | - 'prefer-stable' 29 | include: 30 | - laravel: '7.*' 31 | testbench: '5.*' 32 | - laravel: '8.*' 33 | testbench: '6.*' 34 | 35 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} --${{ matrix.prefer }} 36 | 37 | steps: 38 | - uses: actions/checkout@v1 39 | 40 | - name: Setup PHP 41 | uses: shivammathur/setup-php@v2 42 | with: 43 | php-version: ${{ matrix.php }} 44 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv 45 | coverage: pcov 46 | 47 | - name: Start DynamoDB 48 | run: | 49 | java -Djava.library.path=./DynamoDBLocal_lib -jar local/DynamoDBLocal.jar --port 3000 & 50 | 51 | - uses: actions/cache@v1 52 | name: Cache dependencies 53 | with: 54 | path: ~/.composer/cache/files 55 | key: composer-php-${{ matrix.php }}-${{ matrix.laravel }}-${{ matrix.prefer }}-${{ hashFiles('composer.json') }} 56 | 57 | - name: Install dependencies 58 | run: | 59 | composer require "illuminate/support:${{ matrix.laravel }}" "illuminate/database:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update 60 | composer update --${{ matrix.prefer }} --prefer-dist --no-interaction --no-suggest 61 | 62 | - name: Run tests 63 | run: | 64 | vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml 65 | 66 | - uses: codecov/codecov-action@v1 67 | with: 68 | fail_ci_if_error: false 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .DS_Store 3 | composer.phar 4 | composer.lock 5 | local_test.db 6 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | php: 6 | - 7.2 7 | - 7.3 8 | - 7.4 9 | 10 | env: 11 | matrix: 12 | - COMPOSER_FLAGS="" 13 | 14 | cache: 15 | directories: 16 | - $HOME/.composer/cache 17 | 18 | before_script: 19 | - java -Djava.library.path=./DynamoDBLocal_lib -jar local/DynamoDBLocal.jar --port 3000 & 20 | - sleep 2 21 | - travis_retry composer self-update 22 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source 23 | 24 | script: 25 | - phpunit --coverage-text --coverage-clover=coverage.xml 26 | 27 | after_success: 28 | - bash <(curl -s https://codecov.io/bash) 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Renoki Co. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/renoki-co/dynamodb/workflows/CI/badge.svg?branch=master) 2 | [![Latest Stable Version](https://poser.pugx.org/rennokki/dynamodb/v/stable)](https://packagist.org/packages/rennokki/dynamodb) 3 | [![Total Downloads](https://poser.pugx.org/rennokki/dynamodb/downloads)](https://packagist.org/packages/rennokki/dynamodb) 4 | [![Monthly Downloads](https://poser.pugx.org/rennokki/dynamodb/d/monthly)](https://packagist.org/packages/rennokki/dynamodb) 5 | [![codecov](https://codecov.io/gh/renoki-co/dynamodb/branch/master/graph/badge.svg)](https://codecov.io/gh/renoki-co/dynamodb/branch/master) 6 | [![StyleCI](https://github.styleci.io/repos/223236785/shield?branch=master)](https://github.styleci.io/repos/223236785) 7 | 8 | Laravel DynamoDB 9 | ================ 10 | 11 | This package is a fork from [the original package by Bao Pham](https://github.com/baopham/laravel-dynamodb). 12 | 13 | > For advanced users only. If you're not familiar with Laravel, [Laravel Eloquent](https://laravel.com/docs/eloquent) and [DynamoDB](https://aws.amazon.com/dynamodb/), then I suggest that you get familiar with those first. 14 | 15 | - [Laravel DynamoDB](#laravel-dynamodb) 16 | - [Install](#install) 17 | - [Install (for Lumen)](#install-for-lumen) 18 | - [Usage](#usage) 19 | - [Extend your Model](#extend-your-model) 20 | - [Add Trait to Model (to Sync)](#add-trait-to-model-to-sync) 21 | - [Query Builder](#query-builder) 22 | - [AWS SDK](#aws-sdk) 23 | - [Supported features](#supported-features) 24 | - [find() and delete()](#find-and-delete) 25 | - [Conditions](#conditions) 26 | - [whereNull() and whereNotNull()](#wherenull-and-wherenotnull) 27 | - [all() and first()](#all-and-first) 28 | - [Pagination](#pagination) 29 | - [update()](#update) 30 | - [updateAsync()](#updateasync) 31 | - [save()](#save) 32 | - [saveAsync()](#saveasync) 33 | - [delete()](#delete) 34 | - [deleteAsync()](#deleteasync) 35 | - [chunk()](#chunk) 36 | - [limit() and take()](#limit-and-take) 37 | - [firstOrFail()](#firstorfail) 38 | - [findOrFail()](#findorfail) 39 | - [refresh()](#refresh) 40 | - [getItemsCount()](#getitemscount) 41 | - [Query Scope](#query-scope) 42 | - [REMOVE — Deleting Attributes From An Item](#remove--deleting-attributes-from-an-item) 43 | - [toSql()](#tosql) 44 | - [Decorate Query](#decorate-query) 45 | - [Indexes](#indexes) 46 | - [Composite Keys](#composite-keys) 47 | - [Query Builder](#query-builder-1) 48 | - [FAQ](#faq) 49 | - [Security](#security) 50 | - [Credits](#credits) 51 | - [License](#license) 52 | 53 | # Install 54 | 55 | Install the package using Composer: 56 | 57 | ```bash 58 | $ composer require rennokki/dynamodb 59 | ``` 60 | 61 | If your Laravel package does not support auto-discovery, add this to your `config/app.php` file: 62 | 63 | ```php 64 | 'providers' => [ 65 | ... 66 | Rennokki\DynamoDb\DynamoDbServiceProvider::class, 67 | ...**** 68 | ]; 69 | ``` 70 | 71 | Publish the config files. 72 | 73 | ```php 74 | php artisan vendor:publish 75 | ``` 76 | 77 | # Install (for Lumen) 78 | 79 | For Lumen, try [this](https://github.com/laravelista/lumen-vendor-publish) to install the `vendor:publish` command and load configuration file and enable Eloquent support in `bootstrap/app.php`: 80 | 81 | ```php 82 | $app = new Laravel\Lumen\Application( 83 | realpath(__DIR__.'/../') 84 | ); 85 | 86 | // Load dynamodb config file 87 | $app->configure('dynamodb'); 88 | 89 | // Enable Eloquent support 90 | $app->withEloquent(); 91 | ``` 92 | 93 | # Usage 94 | 95 | ## Extend your Model 96 | Extend your model with `Rennokki\DynamoDb\DynamoDbModel`, then you can use Eloquent methods that are supported. 97 | The idea here is that you can switch back to Eloquent without changing your queries. 98 | 99 | ```php 100 | use Rennokki\DynamoDb\DynamoDbModel; 101 | 102 | class MyModel extends DynamoDbModel 103 | { 104 | // 105 | } 106 | ``` 107 | 108 | ## Add Trait to Model (to Sync) 109 | 110 | To sync your DB table with a DynamoDb table, use trait `Rennokki\DynamoDb\ModelTrait`. 111 | This trait will call a `PutItem` after the model is saved, update or deleted. 112 | 113 | ```php 114 | use Rennokki\DynamoDb\ModelTrait as DynamoDbable; 115 | 116 | class MyModel extends Model 117 | { 118 | use DynamoDbable; 119 | } 120 | ``` 121 | 122 | # Query Builder 123 | 124 | You can use the [query builder](#query-builder) facade to build more complex queries. 125 | 126 | # AWS SDK 127 | 128 | AWS SDK v3 for PHP uses guzzlehttp promises to allow for asynchronous workflows. Using this package you can run eloquent queries like [delete](#deleteasync), [update](#updateasync), [save](#saveasync) asynchronously on DynamoDb. 129 | 130 | # Supported features 131 | 132 | ## find() and delete() 133 | 134 | ```php 135 | $model->find($id, array $columns = []); 136 | $model->findMany($ids, array $columns = []); 137 | 138 | $model->delete(); 139 | $model->deleteAsync()->wait(); 140 | ``` 141 | 142 | ## Conditions 143 | 144 | ```php 145 | // Using getIterator() 146 | // If 'key' is the primary key or a global/local index and it is a supported Query condition, 147 | // will use 'Query', otherwise 'Scan'. 148 | $model->where('key', 'key value')->get(); 149 | 150 | $model->where(['key' => 'key value']); 151 | 152 | // Chainable for 'AND'. 153 | $model->where('foo', 'bar') 154 | ->where('foo2', '!=' 'bar2') 155 | ->get(); 156 | 157 | // Chainable for 'OR'. 158 | $model->where('foo', 'bar') 159 | ->orWhere('foo2', '!=' 'bar2') 160 | ->get(); 161 | 162 | // Other types of conditions 163 | $model->where('count', '>', 0)->get(); 164 | $model->where('count', '>=', 0)->get(); 165 | $model->where('count', '<', 0)->get(); 166 | $model->where('count', '<=', 0)->get(); 167 | $model->whereIn('count', [0, 100])->get(); 168 | $model->whereNotIn('count', [0, 100])->get(); 169 | $model->where('count', 'between', [0, 100])->get(); 170 | $model->where('description', 'begins_with', 'foo')->get(); 171 | $model->where('description', 'contains', 'foo')->get(); 172 | $model->where('description', 'not_contains', 'foo')->get(); 173 | 174 | // Nested conditions 175 | $model 176 | ->where('name', 'foo') 177 | ->where(function ($query) { 178 | return $query 179 | ->where('count', 10) 180 | ->orWhere('count', 20); 181 | })->get(); 182 | 183 | // Nested attributes 184 | $model->where('nestedMap.foo', 'bar')->where('list[0]', 'baz')->get(); 185 | ``` 186 | 187 | ### whereNull() and whereNotNull() 188 | 189 | **NULL and NOT_NULL only check for the attribute presence not its value being null** 190 | Please see: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html 191 | 192 | ```php 193 | $model->whereNull('name'); 194 | $model->whereNotNull('name'); 195 | ``` 196 | 197 | ## all() and first() 198 | ```php 199 | // Using scan operator, not too reliable since DynamoDb will only give 1MB total of data. 200 | $model->all(); 201 | 202 | // Basically a scan but with limit of 1 item. 203 | $model->first(); 204 | ``` 205 | 206 | ## Pagination 207 | 208 | Unfortunately, offset of how many records to skip does not make sense for DynamoDb. 209 | Instead, provide the last result of the previous query as the starting point for the next query. 210 | 211 | **Examples:** 212 | 213 | For query such as the following: 214 | ```php 215 | $query = $model->where('count', 10)->limit(2); 216 | $items = $query->all(); 217 | $last = $items->last(); 218 | ``` 219 | 220 | Take the last item of this query result as the next "offset": 221 | 222 | ```php 223 | $nextPage = $query->after($last)->limit(2)->all(); 224 | 225 | // or 226 | $nextPage = $query->afterKey($items->lastKey())->limit(2)->all(); 227 | 228 | // or (for query without index condition only) 229 | $nextPage = $query->afterKey($last->getKeys())->limit(2)->all(); 230 | ``` 231 | 232 | ## update() 233 | 234 | ```php 235 | $model->update($attributes); 236 | ``` 237 | 238 | ## updateAsync() 239 | 240 | ```php 241 | // update asynchronously and wait on the promise for completion. 242 | $model->updateAsync($attributes)->wait(); 243 | ``` 244 | 245 | ## save() 246 | 247 | ```php 248 | $model = new Model(); 249 | // Define fillable attributes in your Model class. 250 | $model->fillableAttr1 = 'foo'; 251 | $model->fillableAttr2 = 'foo'; 252 | 253 | // DynamoDb doesn't support incremented Id, so you need to use UUID for the primary key. 254 | $model->id = 'de305d54-75b4-431b-adb2-eb6b9e546014'; 255 | $model->save(); 256 | ``` 257 | 258 | ## saveAsync() 259 | 260 | Saving single model asynchronously and waiting on the promise for completion. 261 | 262 | ```php 263 | $model = new Model; 264 | 265 | // Define fillable attributes in your Model class. 266 | $model->fillableAttr1 = 'foo'; 267 | $model->fillableAttr2 = 'bar'; 268 | 269 | // DynamoDb doesn't support incremented Id, so you need to use UUID for the primary key. 270 | $model->id = 'de305d54-75b4-431b-adb2-eb6b9e546014'; 271 | $model->saveAsync()->wait(); 272 | ``` 273 | 274 | Saving multiple models asynchronously and waiting on all of them simultaneously. 275 | 276 | ```php 277 | for ($i = 0; $i < 10; $i++) { 278 | $model = new Model; 279 | 280 | // Define fillable attributes in your Model class. 281 | $model->fillableAttr1 = 'foo'; 282 | $model->fillableAttr2 = 'bar'; 283 | 284 | // DynamoDb doesn't support incremented Id, so you need to use UUID for the primary key. 285 | $model->id = uniqid(); 286 | 287 | // Returns a promise which you can wait on later. 288 | $promises[] = $model->saveAsync(); 289 | } 290 | 291 | \GuzzleHttp\Promise\all($promises)->wait(); 292 | ``` 293 | 294 | ## delete() 295 | 296 | ```php 297 | $model->delete(); 298 | ``` 299 | 300 | ## deleteAsync() 301 | 302 | ```php 303 | $model->deleteAsync()->wait(); 304 | ``` 305 | 306 | ## chunk() 307 | 308 | ```php 309 | $model->chunk(10, function ($records) { 310 | foreach ($records as $record) { 311 | // 312 | } 313 | }); 314 | ``` 315 | 316 | ## limit() and take() 317 | 318 | ```php 319 | // Use this with caution unless your limit is small. 320 | // DynamoDB has a limit of 1MB so if your limit is very big, the results will not be expected. 321 | $model->where('name', 'foo')->take(3)->get(); 322 | ``` 323 | 324 | ## firstOrFail() 325 | 326 | ```php 327 | $model->where('name', 'foo')->firstOrFail(); 328 | 329 | // for composite key 330 | $model->where('id', 'foo')->where('id2', 'bar')->firstOrFail(); 331 | ``` 332 | 333 | ## findOrFail() 334 | 335 | ```php 336 | $model->findOrFail('foo'); 337 | 338 | // for composite key 339 | $model->findOrFail(['id' => 'foo', 'id2' => 'bar']); 340 | ``` 341 | 342 | ## refresh() 343 | 344 | ```php 345 | $model = Model::first(); 346 | $model->refresh(); 347 | ``` 348 | 349 | ## getItemsCount() 350 | 351 | ```php 352 | // returns the approximate total count of the table items 353 | $total = Model::getItemsCount(); // ex: 5 354 | ``` 355 | 356 | ## Query Scope 357 | 358 | ```php 359 | class Foo extends DynamoDbModel 360 | { 361 | protected static function boot() 362 | { 363 | parent::boot(); 364 | 365 | static::addGlobalScope('count', function (DynamoDbQueryBuilder $builder) { 366 | $builder->where('count', '>', 6); 367 | }); 368 | } 369 | 370 | public function scopeCountUnderFour($builder) 371 | { 372 | return $builder->where('count', '<', 4); 373 | } 374 | 375 | public function scopeCountUnder($builder, $count) 376 | { 377 | return $builder->where('count', '<', $count); 378 | } 379 | } 380 | 381 | $foo = new Foo(); 382 | 383 | // Global scope will be applied 384 | $foo->all(); 385 | 386 | // Local scope 387 | $foo->withoutGlobalScopes()->countUnderFour()->get(); 388 | 389 | // Dynamic local scope 390 | $foo->withoutGlobalScopes()->countUnder(6)->get(); 391 | ``` 392 | 393 | ## REMOVE — Deleting Attributes From An Item 394 | 395 | Please see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.REMOVE 396 | 397 | ```php 398 | $model = new Model(); 399 | $model->where('id', 'foo')->removeAttribute('name', 'description', 'nested.foo', 'nestedArray[0]'); 400 | 401 | // Equivalent of: 402 | Model::find('foo')->removeAttribute('name', 'description', 'nested.foo', 'nestedArray[0]'); 403 | ``` 404 | 405 | ## toSql() 406 | 407 | For debugging purposes, you can choose to convert to the actual DynamoDb query 408 | ```php 409 | $raw = $model->where('count', '>', 10)->toDynamoDbQuery(); 410 | 411 | // $op is either "Scan" or "Query" 412 | $op = $raw->op; 413 | 414 | // The query body being sent to AWS 415 | $query = $raw->query; 416 | ``` 417 | 418 | The `$raw` variable is an instance of [RawDynamoDbQuery](./src/RawDynamoDbQuery.php) 419 | 420 | ## Decorate Query 421 | 422 | Use `decorate` when you want to enhance the query. 423 | 424 | To set the order of the sort key: 425 | 426 | ```php 427 | $items = $model 428 | ->where('hash', 'hash-value') 429 | ->where('range', '>', 10) 430 | ->decorate(function (RawDynamoDbQuery $raw) { 431 | // desc order 432 | $raw->query['ScanIndexForward'] = false; 433 | })->get(); 434 | ``` 435 | 436 | To force to use "Query" instead of "Scan" if the library fails to detect the correct operation: 437 | 438 | ```php 439 | $items = $model 440 | ->where('hash', 'hash-value') 441 | ->decorate(function (RawDynamoDbQuery $raw) { 442 | $raw->op = 'Query'; 443 | })->get(); 444 | ``` 445 | 446 | # Indexes 447 | 448 | If your table has indexes, make sure to declare them in your model class like so 449 | 450 | ```php 451 | /** 452 | * The DynamoDb indexes. 453 | * [ 454 | * '' => [ 455 | * 'hash' => '' 456 | * ], 457 | * '' => [ 458 | * 'hash' => '', 459 | * 'range' => '' 460 | * ], 461 | * ] 462 | * 463 | * @var array 464 | */ 465 | protected $dynamoDbIndexKeys = [ 466 | 'count_index' => [ 467 | 'hash' => 'count' 468 | ], 469 | ]; 470 | ``` 471 | 472 | Note that order of index matters when a key exists in multiple indexes. 473 | 474 | For example, for the following query, `count_index` will be used: 475 | 476 | ```php 477 | $model->where('user_id', 123)->where('count', '>', 10)->get(); 478 | ``` 479 | 480 | ```php 481 | protected $dynamoDbIndexKeys = [ 482 | 'count_index' => [ 483 | 'hash' => 'user_id', 484 | 'range' => 'count' 485 | ], 486 | 'user_index' => [ 487 | 'hash' => 'user_id', 488 | ], 489 | ]; 490 | ``` 491 | 492 | 493 | Most of the time, you should not have to do anything but if you need to use a specific index, you can specify it like so 494 | 495 | ```php 496 | $model 497 | ->where('user_id', 123) 498 | ->where('count', '>', 10) 499 | ->withIndex('count_index') 500 | ->get(); 501 | ``` 502 | 503 | # Composite Keys 504 | 505 | To use composite keys with your model: 506 | 507 | Set `$compositeKey` to an array of the attributes names comprising the key, e.g. 508 | 509 | ```php 510 | protected $primaryKey = 'customer_id'; 511 | protected $compositeKey = ['customer_id', 'agent_id']; 512 | ``` 513 | 514 | To find a record with a composite key: 515 | 516 | ```php 517 | $model->find(['customer_id' => 'value1', 'agent_id' => 'value2']); 518 | ``` 519 | 520 | # Query Builder 521 | 522 | Use `DynamoDb` facade to build raw queries. 523 | 524 | ```php 525 | use Rennokki\DynamoDb\Facades\DynamoDb; 526 | 527 | DynamoDb::table('articles') 528 | // call set to build the query body to be sent to AWS 529 | ->setFilterExpression('#name = :name') 530 | ->setExpressionAttributeNames(['#name' => 'author_name']) 531 | ->setExpressionAttributeValues([':name' => DynamoDb::marshalValue('Bao')]) 532 | ->prepare() 533 | // the query body will be sent upon calling this. 534 | ->scan(); // supports any DynamoDbClient methods (e.g. batchWriteItem, batchGetItem, etc.) 535 | 536 | DynamoDb::table('articles') 537 | ->setIndex('author_name') 538 | ->setKeyConditionExpression('#name = :name') 539 | ->setProjectionExpression('id, author_name') 540 | // Can set the attribute mapping one by one instead 541 | ->setExpressionAttributeName('#name', 'author_name') 542 | ->setExpressionAttributeValue(':name', DynamoDb::marshalValue('Bao')) 543 | ->prepare() 544 | ->query(); 545 | 546 | DynamoDb::table('articles') 547 | ->setKey(DynamoDb::marshalItem(['id' => 'ae025ed8'])) 548 | ->setUpdateExpression('REMOVE #c, #t') 549 | ->setExpressionAttributeName('#c', 'comments') 550 | ->setExpressionAttributeName('#t', 'tags') 551 | ->prepare() 552 | ->updateItem(); 553 | 554 | DynamoDb::table('articles') 555 | ->setKey(DynamoDb::marshalItem(['id' => 'ae025ed8'])) 556 | ->prepare() 557 | ->deleteItem(); 558 | 559 | DynamoDb::table('articles') 560 | ->setItem( 561 | DynamoDb::marshalItem(['id' => 'ae025ed8', 'author_name' => 'New Name']) 562 | ) 563 | ->prepare() 564 | ->putItem(); 565 | 566 | // Or, instead of ::table() 567 | DynamoDb::newQuery() 568 | ->setTableName('articles') 569 | 570 | // Or access the DynamoDbClient instance directly 571 | DynamoDb::client(); 572 | 573 | // pass in the connection name to get a different client instance other than the default. 574 | DynamoDb::client('test'); 575 | ``` 576 | 577 | The query builder methods are in the form of `set`, where `` is the key name of the query body to be sent. 578 | 579 | For example, to build an [`UpdateTable`](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-dynamodb-2012-08-10.html#updatetable) query: 580 | 581 | ```php 582 | [ 583 | 'AttributeDefinitions' => ..., 584 | 'GlobalSecondaryIndexUpdates' => ..., 585 | 'TableName' => ... 586 | ] 587 | ``` 588 | 589 | Do: 590 | ```php 591 | $query = DynamoDb::table('articles') 592 | ->setAttributeDefinitions(...) 593 | ->setGlobalSecondaryIndexUpdates(...); 594 | ``` 595 | 596 | And when ready: 597 | ```php 598 | $query->prepare()->updateTable(); 599 | ``` 600 | 601 | # FAQ 602 | 603 | Q: Cannot assign `id` property if its not in the fillable array 604 | A: Try [this](https://github.com/baopham/laravel-dynamodb/issues/10)? 605 | 606 | Q: How to create migration? 607 | A: Please see [this issue](https://github.com/baopham/laravel-dynamodb/issues/90) 608 | 609 | Q: How to use with factory? 610 | A: Please see [this issue](https://github.com/baopham/laravel-dynamodb/issues/111) 611 | 612 | Q: How do I use with Job? Getting a SerializesModels error 613 | A: You can either [write your own restoreModel](https://github.com/baopham/laravel-dynamodb/issues/132) or remove the `SerializesModels` trait from your Job. 614 | 615 | ## Security 616 | 617 | If you discover any security related issues, please email alex@renoki.org instead of using the issue tracker. 618 | 619 | ## Credits 620 | 621 | - [Bao Pham](https://github.com/baopham/laravel-dynamodb) 622 | - [warrick-loyaltycorp](https://github.com/warrick-loyaltycorp) 623 | - [Alexander Ward](https://github.com/cthos) 624 | - [Quang Ngo](https://github.com/vanquang9387) 625 | - [David Higgins](https://github.com/zoul0813) 626 | - [Damon Williams](https://github.com/footballencarta) 627 | - [Alex Renoki](https://github.com/rennokki) 628 | - [All Contributors](../../contributors) 629 | 630 | ## License 631 | 632 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 633 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rennokki/dynamodb", 3 | "description": "AWS DynamoDB Eloquent ORM for Laravel 7+", 4 | "keywords": ["laravel", "dynamodb", "aws", "amazon", "query"], 5 | "require": { 6 | "aws/aws-sdk-php": "^3.142", 7 | "guzzlehttp/guzzle": "^6.5|^7.0", 8 | "illuminate/support": "^7.30|^8.23", 9 | "illuminate/database": "^7.30|^8.23" 10 | }, 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Bao Pham", 15 | "email": "gbaopham@gmail.com" 16 | }, 17 | { 18 | "name": "Alex Renoki", 19 | "email": "alex@renoki.org", 20 | "homepage": "https://github.com/rennokki", 21 | "role": "Developer" 22 | } 23 | ], 24 | "autoload": { 25 | "psr-4": { 26 | "Rennokki\\DynamoDb\\": "src/" 27 | } 28 | }, 29 | "require-dev": { 30 | "laravel/legacy-factories": "^1.1", 31 | "mockery/mockery": "^1.4", 32 | "orchestra/testbench": "^5.0|^6.0", 33 | "orchestra/database": "^5.0|^6.0" 34 | }, 35 | "scripts": { 36 | "test": "phpunit", 37 | "local": "java -Djava.library.path=./DynamoDBLocal_lib -jar local/DynamoDBLocal.jar --port 3000" 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Rennokki\\DynamoDb\\Tests\\": "tests/" 42 | } 43 | }, 44 | "config": { 45 | "sort-packages": true 46 | }, 47 | "minimum-stability": "dev", 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "Rennokki\\DynamoDb\\DynamoDbServiceProvider" 52 | ] 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /config/dynamodb.php: -------------------------------------------------------------------------------- 1 | env('DYNAMODB_CONNECTION', 'aws'), 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | DynamoDb Connections 18 | |-------------------------------------------------------------------------- 19 | | 20 | | Here are each of the DynamoDb connections setup for your application. 21 | | 22 | | Most of the connection's config will be fed directly to AwsClient 23 | | constructor http://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.AwsClient.html#___construct 24 | */ 25 | 'connections' => [ 26 | 'aws' => [ 27 | 'credentials' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID', ''), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY', ''), 30 | 31 | // If using as an assumed IAM role, you can also use the `token` parameter 32 | 'token' => env('AWS_DYNAMODB_SESSION_TOKEN', ''), 33 | ], 34 | 35 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 36 | 37 | // If true, it will use Laravel Log. 38 | // For advanced options, see http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/configuration.html 39 | 'debug' => env('AWS_DYNAMODB_DEBUG', false), 40 | ], 41 | 42 | 'aws_iam_role' => [ 43 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 44 | 45 | // If true, it will use Laravel Log. 46 | // For advanced options, see http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/configuration.html 47 | 'debug' => env('AWS_DYNAMODB_DEBUG', false), 48 | ], 49 | 50 | 'local' => [ 51 | 'credentials' => [ 52 | 'key' => env('AWS_ACCESS_KEY_ID', 'local'), 53 | 'secret' => env('AWS_SECRET_ACCESS_KEY', 'secret'), 54 | ], 55 | 56 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 57 | 58 | // see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html 59 | 'endpoint' => env('AWS_DYNAMODB_LOCAL_ENDPOINT'), 60 | 61 | // If true, it will use Laravel Log. 62 | // For advanced options, see http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/configuration.html 63 | 'debug' => true, 64 | ], 65 | 66 | 'test' => [ 67 | 'credentials' => [ 68 | 'key' => env('AWS_ACCESS_KEY_ID', 'local'), 69 | 'secret' => env('AWS_SECRET_ACCESS_KEY', 'secret'), 70 | ], 71 | 72 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 73 | 74 | // see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html 75 | 'endpoint' => env('AWS_DYNAMODB_LOCAL_ENDPOINT'), 76 | 77 | // If true, it will use Laravel Log. 78 | // For advanced options, see http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/configuration.html 79 | 'debug' => true, 80 | ], 81 | ], 82 | ]; 83 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | vendor/* 17 | 18 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /schema.js: -------------------------------------------------------------------------------- 1 | var params = { 2 | TableName: 'test_model', 3 | KeySchema: [ // The type of of schema. Must start with a HASH type, with an optional second RANGE. 4 | { // Required HASH type attribute 5 | AttributeName: 'id', 6 | KeyType: 'HASH', 7 | } 8 | ], 9 | AttributeDefinitions: [ // The names and types of all primary and index key attributes only 10 | { 11 | AttributeName: 'id', 12 | AttributeType: 'S', // (S | N | B) for string, number, binary 13 | }, 14 | { 15 | AttributeName: 'count', 16 | AttributeType: 'N', // (S | N | B) for string, number, binary 17 | } 18 | ], 19 | ProvisionedThroughput: { // required provisioned throughput for the table 20 | ReadCapacityUnits: 1, 21 | WriteCapacityUnits: 1, 22 | }, 23 | GlobalSecondaryIndexes: [ // optional (list of GlobalSecondaryIndex) 24 | { 25 | IndexName: 'count_index', 26 | KeySchema: [ 27 | { // Required HASH type attribute 28 | AttributeName: 'count', 29 | KeyType: 'HASH', 30 | } 31 | ], 32 | Projection: { // attributes to project into the index 33 | ProjectionType: 'ALL', // (ALL | KEYS_ONLY | INCLUDE) 34 | }, 35 | ProvisionedThroughput: { // throughput to provision to the index 36 | ReadCapacityUnits: 1, 37 | WriteCapacityUnits: 1, 38 | }, 39 | }, 40 | // ... more global secondary indexes ... 41 | ] 42 | }; 43 | dynamodb.createTable(params, function(err, data) { 44 | if (err) print(err); // an error occurred 45 | else print(data); // successful response 46 | }); 47 | 48 | // Table with composite keys: 49 | 50 | params = { 51 | TableName: 'composite_test_model', 52 | KeySchema: [ // The type of of schema. Must start with a HASH type, with an optional second RANGE. 53 | { // Required HASH type attribute 54 | AttributeName: 'id', 55 | KeyType: 'HASH', 56 | }, 57 | { 58 | AttributeName: 'id2', 59 | KeyType: 'RANGE', 60 | } 61 | ], 62 | AttributeDefinitions: [ // The names and types of all primary and index key attributes only 63 | { 64 | AttributeName: 'id', 65 | AttributeType: 'S', // (S | N | B) for string, number, binary 66 | }, 67 | { 68 | AttributeName: 'id2', 69 | AttributeType: 'S', // (S | N | B) for string, number, binary 70 | }, 71 | { 72 | AttributeName: 'count', 73 | AttributeType: 'N', // (S | N | B) for string, number, binary 74 | } 75 | ], 76 | ProvisionedThroughput: { // required provisioned throughput for the table 77 | ReadCapacityUnits: 1, 78 | WriteCapacityUnits: 1, 79 | }, 80 | GlobalSecondaryIndexes: [ // optional (list of GlobalSecondaryIndex) 81 | { 82 | IndexName: 'count_index', 83 | KeySchema: [ 84 | { // Required HASH type attribute 85 | AttributeName: 'count', 86 | KeyType: 'HASH', 87 | } 88 | ], 89 | Projection: { // attributes to project into the index 90 | ProjectionType: 'ALL', // (ALL | KEYS_ONLY | INCLUDE) 91 | }, 92 | ProvisionedThroughput: { // throughput to provision to the index 93 | ReadCapacityUnits: 1, 94 | WriteCapacityUnits: 1, 95 | }, 96 | }, 97 | { 98 | IndexName: 'id_count_index', 99 | KeySchema: [ 100 | { // Required HASH type attribute 101 | AttributeName: 'id', 102 | KeyType: 'HASH', 103 | }, 104 | { 105 | AttributeName: 'count', 106 | KeyType: 'RANGE', 107 | } 108 | ], 109 | Projection: { // attributes to project into the index 110 | ProjectionType: 'ALL', // (ALL | KEYS_ONLY | INCLUDE) 111 | }, 112 | ProvisionedThroughput: { // throughput to provision to the index 113 | ReadCapacityUnits: 1, 114 | WriteCapacityUnits: 1, 115 | }, 116 | }, 117 | // ... more global secondary indexes ... 118 | ] 119 | }; 120 | 121 | dynamodb.createTable(params, function(err, data) { 122 | if (err) { 123 | print(err); 124 | } else { 125 | print(data); 126 | } 127 | }); 128 | -------------------------------------------------------------------------------- /src/ComparisonOperator.php: -------------------------------------------------------------------------------- 1 | static::EQ, 30 | '>' => static::GT, 31 | '>=' => static::GE, 32 | '<' => static::LT, 33 | '<=' => static::LE, 34 | 'in' => static::IN, 35 | '!=' => static::NE, 36 | 'begins_with' => static::BEGINS_WITH, 37 | 'between' => static::BETWEEN, 38 | 'not_contains' => static::NOT_CONTAINS, 39 | 'contains' => static::CONTAINS, 40 | 'null' => static::NULL, 41 | 'not_null' => static::NOT_NULL, 42 | ]; 43 | } 44 | 45 | /** 46 | * Get the list of supported operators (by DynamoDb names). 47 | * 48 | * @return array 49 | */ 50 | public static function getSupportedOperators(): array 51 | { 52 | return array_keys(static::getOperatorMapping()); 53 | } 54 | 55 | /** 56 | * Check if the operator is valid. 57 | * 58 | * @return bool 59 | */ 60 | public static function isValidOperator($operator): bool 61 | { 62 | $operator = strtolower($operator); 63 | $mapping = static::getOperatorMapping(); 64 | 65 | return isset($mapping[$operator]); 66 | } 67 | 68 | /** 69 | * Get the operator for the DynamoDb operator. 70 | * 71 | * @param string $operator 72 | * @return string 73 | */ 74 | public static function getDynamoDbOperator($operator): string 75 | { 76 | $mapping = static::getOperatorMapping(); 77 | $operator = strtolower($operator); 78 | 79 | return $mapping[$operator]; 80 | } 81 | 82 | /** 83 | * Get a list of query supported operators 84 | * wether is a range key or not. 85 | * 86 | * @param bool $isRangeKey 87 | * @return array 88 | */ 89 | public static function getQuerySupportedOperators($isRangeKey = false): array 90 | { 91 | if ($isRangeKey) { 92 | return [ 93 | static::EQ, 94 | static::LE, 95 | static::LT, 96 | static::GE, 97 | static::GT, 98 | static::BEGINS_WITH, 99 | static::BETWEEN, 100 | ]; 101 | } 102 | 103 | return [static::EQ]; 104 | } 105 | 106 | /** 107 | * Check if the operator is valid for query. 108 | * 109 | * @param string $operator 110 | * @param bool $isRangeKey 111 | * @return bool 112 | */ 113 | public static function isValidQueryOperator($operator, $isRangeKey = false): bool 114 | { 115 | $dynamoDbOperator = static::getDynamoDbOperator($operator); 116 | 117 | return static::isValidQueryDynamoDbOperator($dynamoDbOperator, $isRangeKey); 118 | } 119 | 120 | /** 121 | * Check if the operator is valid for DynamoDb query. 122 | * 123 | * @param string $dynamoDbOperator 124 | * @param bool $isRangeKey 125 | * @return bool 126 | */ 127 | public static function isValidQueryDynamoDbOperator($dynamoDbOperator, $isRangeKey = false): bool 128 | { 129 | return in_array($dynamoDbOperator, static::getQuerySupportedOperators($isRangeKey)); 130 | } 131 | 132 | /** 133 | * Check if the operator is a DynamoDb operator. 134 | * 135 | * @param string $op 136 | * @param string $dynamoDbOperator 137 | * @return bool 138 | */ 139 | public static function is($op, $dynamoDbOperator): bool 140 | { 141 | $mapping = static::getOperatorMapping(); 142 | 143 | return $mapping[strtolower($op)] === $dynamoDbOperator; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Concerns/HasParsers.php: -------------------------------------------------------------------------------- 1 | placeholder = new Placeholder; 72 | $this->expressionAttributeNames = new ExpressionAttributeNames; 73 | $this->expressionAttributeValues = new ExpressionAttributeValues; 74 | 75 | $this->keyConditionExpression = new KeyConditionExpression( 76 | $this->placeholder, 77 | $this->expressionAttributeValues, 78 | $this->expressionAttributeNames 79 | ); 80 | 81 | $this->filterExpression = new FilterExpression( 82 | $this->placeholder, 83 | $this->expressionAttributeValues, 84 | $this->expressionAttributeNames 85 | ); 86 | 87 | $this->projectionExpression = new ProjectionExpression($this->expressionAttributeNames); 88 | $this->updateExpression = new UpdateExpression($this->expressionAttributeNames); 89 | } 90 | 91 | /** 92 | * Reset the expressions. 93 | * 94 | * @return void 95 | */ 96 | public function resetExpressions(): void 97 | { 98 | $this->filterExpression->reset(); 99 | $this->keyConditionExpression->reset(); 100 | $this->updateExpression->reset(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/ConditionAnalyzer/Analyzer.php: -------------------------------------------------------------------------------- 1 | model = $model; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Set the index name. 47 | * 48 | * @param string|null $index 49 | * @return \Rennokki\DynamoDb\ConditionAnalyzer\Analyzer 50 | */ 51 | public function withIndex($index) 52 | { 53 | $this->indexName = $index; 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * Set the conditions. 60 | * 61 | * @param array $conditions 62 | * @return \Rennokki\DynamoDb\ConditionAnalyzer\Analyzer 63 | */ 64 | public function analyze($conditions) 65 | { 66 | $this->conditions = $conditions; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Check if the search is exact. 73 | * 74 | * @return bool 75 | */ 76 | public function isExactSearch(): bool 77 | { 78 | if (empty($this->conditions)) { 79 | return false; 80 | } 81 | 82 | if (empty($this->identifierConditions())) { 83 | return false; 84 | } 85 | 86 | foreach ($this->conditions as $condition) { 87 | if (Arr::get($condition, 'type') !== ComparisonOperator::EQ) { 88 | return false; 89 | } 90 | } 91 | 92 | return true; 93 | } 94 | 95 | /** 96 | * Get the index. 97 | * 98 | * @return Index|null 99 | */ 100 | public function index() 101 | { 102 | return $this->getIndex(); 103 | } 104 | 105 | /** 106 | * Get the conditions for the keys. 107 | * 108 | * @return array|null 109 | */ 110 | public function keyConditions() 111 | { 112 | $index = $this->getIndex(); 113 | 114 | if ($index) { 115 | return $this->getConditions($index->columns()); 116 | } 117 | 118 | return $this->identifierConditions(); 119 | } 120 | 121 | /** 122 | * Filter the conditions. 123 | * 124 | * @return array 125 | */ 126 | public function filterConditions(): array 127 | { 128 | $keyConditions = $this->keyConditions() ?: []; 129 | 130 | return array_filter($this->conditions, function ($condition) use ($keyConditions) { 131 | return array_search($condition, $keyConditions) === false; 132 | }); 133 | } 134 | 135 | public function identifierConditions() 136 | { 137 | $keyNames = $this->model->getKeyNames(); 138 | 139 | $conditions = $this->getConditions($keyNames); 140 | 141 | if (! $this->hasValidQueryOperator(...$keyNames)) { 142 | return; 143 | } 144 | 145 | return $conditions; 146 | } 147 | 148 | public function identifierConditionValues() 149 | { 150 | $idConditions = $this->identifierConditions(); 151 | 152 | if (! $idConditions) { 153 | return []; 154 | } 155 | 156 | $values = []; 157 | 158 | foreach ($idConditions as $condition) { 159 | $values[$condition['column']] = $condition['value']; 160 | } 161 | 162 | return $values; 163 | } 164 | 165 | /** 166 | * @param $column 167 | * 168 | * @return array|null 169 | */ 170 | private function getCondition($column) 171 | { 172 | return Arr::first($this->conditions, function ($condition) use ($column) { 173 | return $condition['column'] === $column; 174 | }); 175 | } 176 | 177 | /** 178 | * @param $columns 179 | * 180 | * @return array 181 | */ 182 | private function getConditions($columns): array 183 | { 184 | return array_filter($this->conditions, function ($condition) use ($columns) { 185 | return in_array($condition['column'], $columns); 186 | }); 187 | } 188 | 189 | /** 190 | * Get the index. 191 | * 192 | * @return Index|null 193 | */ 194 | private function getIndex() 195 | { 196 | if (empty($this->conditions)) { 197 | return; 198 | } 199 | 200 | $index = null; 201 | 202 | foreach ($this->model->getDynamoDbIndexKeys() as $name => $keysInfo) { 203 | $conditionKeys = Arr::pluck($this->conditions, 'column'); 204 | $keys = array_values($keysInfo); 205 | 206 | if (count(array_intersect($conditionKeys, $keys)) === count($keys)) { 207 | if (! isset($this->indexName) || $this->indexName === $name) { 208 | $index = new Index( 209 | $name, 210 | Arr::get($keysInfo, 'hash'), 211 | Arr::get($keysInfo, 'range') 212 | ); 213 | 214 | break; 215 | } 216 | } 217 | } 218 | 219 | if ($index && ! $this->hasValidQueryOperator($index->hash, $index->range)) { 220 | $index = null; 221 | } 222 | 223 | return $index; 224 | } 225 | 226 | /** 227 | * Check if the query is valid. 228 | * 229 | * @param string $hash 230 | * @param string|null $range 231 | * @return bool 232 | */ 233 | private function hasValidQueryOperator($hash, $range = null): bool 234 | { 235 | $hashCondition = $this->getCondition($hash); 236 | 237 | $validQueryOp = ComparisonOperator::isValidQueryDynamoDbOperator($hashCondition['type'] ?? null); 238 | 239 | if ($validQueryOp && $range) { 240 | $rangeCondition = $this->getCondition($range); 241 | 242 | $validQueryOp = ComparisonOperator::isValidQueryDynamoDbOperator( 243 | $rangeCondition['type'] ?? null, 244 | true 245 | ); 246 | } 247 | 248 | return $validQueryOp; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/ConditionAnalyzer/Index.php: -------------------------------------------------------------------------------- 1 | name = $name; 39 | $this->hash = $hash; 40 | $this->range = $range; 41 | } 42 | 43 | /** 44 | * Check if the index is composite. 45 | * 46 | * @return bool 47 | */ 48 | public function isComposite(): bool 49 | { 50 | return isset($this->hash) && isset($this->range); 51 | } 52 | 53 | /** 54 | * Build the columns. 55 | * 56 | * @return array 57 | */ 58 | public function columns(): array 59 | { 60 | $columns = []; 61 | 62 | if ($this->hash) { 63 | $columns[] = $this->hash; 64 | } 65 | 66 | if ($this->range) { 67 | $columns[] = $this->range; 68 | } 69 | 70 | return $columns; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/DynamoDb/DynamoDbManager.php: -------------------------------------------------------------------------------- 1 | service = $service; 32 | $this->marshaler = $service->getMarshaler(); 33 | } 34 | 35 | /** 36 | * Marshal the item. 37 | * 38 | * @param array $item 39 | * @return array 40 | */ 41 | public function marshalItem($item): array 42 | { 43 | return $this->marshaler->marshalItem($item); 44 | } 45 | 46 | /** 47 | * Marshal the value. 48 | * 49 | * @param mixed $value 50 | * @return array 51 | */ 52 | public function marshalValue($value): array 53 | { 54 | return $this->marshaler->marshalValue($value); 55 | } 56 | 57 | /** 58 | * Unmarshal an item. 59 | * 60 | * @param array $item 61 | * @return array 62 | */ 63 | public function unmarshalItem($item): array 64 | { 65 | return $this->marshaler->unmarshalItem($item); 66 | } 67 | 68 | /** 69 | * Unmarshal a value. 70 | * 71 | * @param mixed $value 72 | * @return array|string 73 | */ 74 | public function unmarshalValue($value) 75 | { 76 | return $this->marshaler->unmarshalValue($value); 77 | } 78 | 79 | /** 80 | * Get the client. 81 | * 82 | * @param string|null $connection 83 | * @return \Aws\DynamoDb\DynamoDbClient 84 | */ 85 | public function client($connection = null) 86 | { 87 | return $this->service->getClient($connection); 88 | } 89 | 90 | /** 91 | * Get the instance of a new query builder. 92 | * 93 | * @return QueryBuilder 94 | */ 95 | public function newQuery() 96 | { 97 | return new QueryBuilder($this->service); 98 | } 99 | 100 | /** 101 | * Set the table name. 102 | * 103 | * @param string $table 104 | * @return QueryBuilder 105 | */ 106 | public function table($table) 107 | { 108 | return $this->newQuery()->setTableName($table); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/DynamoDb/ExecutableQuery.php: -------------------------------------------------------------------------------- 1 | client = $client; 33 | $this->query = $query; 34 | } 35 | 36 | /** 37 | * Intecept the call class. 38 | * 39 | * @param string $method 40 | * @param array $parameters 41 | * @return mixed 42 | */ 43 | public function __call($method, $parameters) 44 | { 45 | return $this->client->{$method}($this->query); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/DynamoDb/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | service = $service; 36 | } 37 | 38 | /** 39 | * Hydrate the query. 40 | * 41 | * @param array $query 42 | * @return \Rennokki\DynamoDb\DynamoDb\QueryBuilder 43 | */ 44 | public function hydrate(array $query) 45 | { 46 | $this->query = $query; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Set a new attribute name. 53 | * 54 | * @param string $placeholder 55 | * @param string $name 56 | * @return \Rennokki\DynamoDb\DynamoDb\QueryBuilder 57 | */ 58 | public function setExpressionAttributeName($placeholder, $name) 59 | { 60 | $this->query['ExpressionAttributeNames'][$placeholder] = $name; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Set a new attribute value. 67 | * 68 | * @param string $placeholder 69 | * @param string $value 70 | * @return \Rennokki\DynamoDb\DynamoDb\QueryBuilder 71 | */ 72 | public function setExpressionAttributeValue($placeholder, $value) 73 | { 74 | $this->query['ExpressionAttributeValues'][$placeholder] = $value; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Prepare the query. 81 | * 82 | * @param DynamoDbClient|null $client 83 | * @return ExecutableQuery 84 | */ 85 | public function prepare(DynamoDbClient $client = null) 86 | { 87 | $raw = new RawDynamoDbQuery(null, $this->query); 88 | 89 | return new ExecutableQuery($client ?: $this->service->getClient(), $raw->finalize()->query); 90 | } 91 | 92 | /** 93 | * Call the method. 94 | * 95 | * @param string $method 96 | * @param array $parameters 97 | * @return mixed 98 | */ 99 | public function __call($method, $parameters) 100 | { 101 | if (Str::startsWith($method, 'set')) { 102 | $key = array_reverse(explode('set', $method, 2))[0]; 103 | $this->query[$key] = current($parameters); 104 | 105 | return $this; 106 | } 107 | 108 | throw new BadMethodCallException(sprintf( 109 | 'Method %s::%s does not exist.', 110 | static::class, 111 | $method 112 | )); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/DynamoDbClientInterface.php: -------------------------------------------------------------------------------- 1 | marshaler = $marshaler; 43 | $this->attributeFilter = $filter; 44 | $this->clients = []; 45 | } 46 | 47 | /** 48 | * Get the DynamoDb client configuration. 49 | * 50 | * @param string|null $connection 51 | * @return \Aws\DynamoDb\DynamoDbClient 52 | */ 53 | public function getClient($connection = null) 54 | { 55 | $connection = $connection ?: config('dynamodb.default'); 56 | 57 | if (isset($this->clients[$connection])) { 58 | return $this->clients[$connection]; 59 | } 60 | 61 | $config = config("dynamodb.connections.$connection", []); 62 | $config['version'] = '2012-08-10'; 63 | $config['debug'] = $this->getDebugOptions(Arr::get($config, 'debug')); 64 | 65 | $client = new DynamoDbClient($config); 66 | 67 | $this->clients[$connection] = $client; 68 | 69 | return $client; 70 | } 71 | 72 | /** 73 | * Get the marshaler for DynamoDb. 74 | * 75 | * @return \Aws\DynamoDb\Marshaler 76 | */ 77 | public function getMarshaler() 78 | { 79 | return $this->marshaler; 80 | } 81 | 82 | /** 83 | * Get the attribute filter. 84 | * 85 | * @return \Rennokki\DynamoDb\EmptyAttributeFilter 86 | */ 87 | public function getAttributeFilter() 88 | { 89 | return $this->attributeFilter; 90 | } 91 | 92 | /** 93 | * Trigger the log if debug is enabled. 94 | * 95 | * @param bool $debug 96 | * @return bool|array 97 | */ 98 | protected function getDebugOptions($debug = false) 99 | { 100 | if ($debug === true) { 101 | $logfn = function ($msg) { 102 | Log::info($msg); 103 | }; 104 | 105 | return ['logfn' => $logfn]; 106 | } 107 | 108 | return $debug; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/DynamoDbCollection.php: -------------------------------------------------------------------------------- 1 | conditionIndex = $conditionIndex; 29 | } 30 | 31 | /** 32 | * Get the last key. Used for limit/offset queries. 33 | * 34 | * @return null|string 35 | */ 36 | public function lastKey() 37 | { 38 | $after = $this->last(); 39 | 40 | if (empty($after)) { 41 | return; 42 | } 43 | 44 | $afterKey = $after->getKeys(); 45 | 46 | $attributes = $this->conditionIndex ? $this->conditionIndex->columns() : []; 47 | 48 | foreach ($attributes as $attribute) { 49 | $afterKey[$attribute] = $after->getAttribute($attribute); 50 | } 51 | 52 | return $afterKey; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/DynamoDbModel.php: -------------------------------------------------------------------------------- 1 | ' => [ 45 | * 'hash' => '' 46 | * ], 47 | * '' => [ 48 | * 'hash' => '', 49 | * 'range' => '' 50 | * ], 51 | * ]. 52 | * 53 | * @var array 54 | */ 55 | protected $dynamoDbIndexKeys = []; 56 | 57 | /** 58 | * Array of your composite key. 59 | * 60 | * ['', '']. 61 | * 62 | * @var array 63 | */ 64 | protected $compositeKey = []; 65 | 66 | /** 67 | * Default Date format (ISO 8601 Compliant) 68 | * https://www.php.net/manual/en/class.datetimeinterface.php#datetime.constants.atom. 69 | * 70 | * @var string 71 | */ 72 | protected $dateFormat = DateTime::ATOM; 73 | 74 | /** 75 | * Initialize the class. 76 | * 77 | * @param array $attributes 78 | * @return void 79 | */ 80 | public function __construct(array $attributes = []) 81 | { 82 | $this->bootIfNotBooted(); 83 | 84 | $this->syncOriginal(); 85 | 86 | $this->fill($attributes); 87 | 88 | $this->setupDynamoDb(); 89 | } 90 | 91 | /** 92 | * Get the DynamoDbClient service that is being used by the models. 93 | * 94 | * @return DynamoDbClientInterface 95 | */ 96 | public static function getDynamoDbClientService() 97 | { 98 | return static::$dynamoDb; 99 | } 100 | 101 | /** 102 | * Set the DynamoDbClient used by models. 103 | * 104 | * @param DynamoDbClientInterface $dynamoDb 105 | * @return void 106 | */ 107 | public static function setDynamoDbClientService(DynamoDbClientInterface $dynamoDb) 108 | { 109 | static::$dynamoDb = $dynamoDb; 110 | } 111 | 112 | /** 113 | * Unset the DynamoDbClient service for models. 114 | * 115 | * @return void 116 | */ 117 | public static function unsetDynamoDbClientService() 118 | { 119 | static::$dynamoDb = null; 120 | } 121 | 122 | /** 123 | * Set up the DynamoDb marshaler and attribute filters. 124 | * 125 | * @return void 126 | */ 127 | protected function setupDynamoDb() 128 | { 129 | $this->marshaler = static::$dynamoDb->getMarshaler(); 130 | $this->attributeFilter = static::$dynamoDb->getAttributeFilter(); 131 | } 132 | 133 | /** 134 | * Create a new DynamoDb Collection instance. 135 | * 136 | * @param array $models 137 | * @param \Rennokki\DynamoDb\ConditionAnalyzer\Index|null $index 138 | * @return DynamoDbCollection 139 | */ 140 | public function newCollection(array $models = [], $index = null) 141 | { 142 | return new DynamoDbCollection($models, $index); 143 | } 144 | 145 | /** 146 | * Trigger the save action. 147 | * 148 | * @param array $options 149 | * @return bool 150 | */ 151 | public function save(array $options = []) 152 | { 153 | $create = ! $this->exists; 154 | 155 | if ($this->fireModelEvent('saving') === false) { 156 | return false; 157 | } 158 | 159 | if ($create && $this->fireModelEvent('creating') === false) { 160 | return false; 161 | } 162 | 163 | if (! $create && $this->fireModelEvent('updating') === false) { 164 | return false; 165 | } 166 | 167 | if ($this->usesTimestamps()) { 168 | $this->updateTimestamps(); 169 | } 170 | 171 | $saved = $this->newQuery()->save(); 172 | 173 | if (! $saved) { 174 | return $saved; 175 | } 176 | 177 | $this->exists = true; 178 | $this->wasRecentlyCreated = $create; 179 | 180 | $this->fireModelEvent($create ? 'created' : 'updated', false); 181 | 182 | $this->finishSave($options); 183 | 184 | return $saved; 185 | } 186 | 187 | /** 188 | * Saves the model to DynamoDb asynchronously and returns a promise. 189 | * 190 | * @param array $options 191 | * @return bool|\GuzzleHttp\Promise\Promise 192 | */ 193 | public function saveAsync(array $options = []) 194 | { 195 | $create = ! $this->exists; 196 | 197 | if ($this->fireModelEvent('saving') === false) { 198 | return false; 199 | } 200 | 201 | if ($create && $this->fireModelEvent('creating') === false) { 202 | return false; 203 | } 204 | 205 | if (! $create && $this->fireModelEvent('updating') === false) { 206 | return false; 207 | } 208 | 209 | if ($this->usesTimestamps()) { 210 | $this->updateTimestamps(); 211 | } 212 | 213 | $savePromise = $this->newQuery()->saveAsync(); 214 | 215 | $savePromise->then(function ($result) use ($create, $options) { 216 | if (Arr::get($result, '@metadata.statusCode') === 200) { 217 | $this->exists = true; 218 | $this->wasRecentlyCreated = $create; 219 | 220 | $this->fireModelEvent($create ? 'created' : 'updated', false); 221 | $this->finishSave($options); 222 | } 223 | }); 224 | 225 | return $savePromise; 226 | } 227 | 228 | /** 229 | * Trigger the update action. 230 | * 231 | * @param array $attributes 232 | * @param array $options 233 | * @return bool 234 | */ 235 | public function update(array $attributes = [], array $options = []) 236 | { 237 | return $this->fill($attributes)->save(); 238 | } 239 | 240 | /** 241 | * Trigger the update actions and return a promise. 242 | * 243 | * @param array $attributes 244 | * @param array $options 245 | * @return bool|\GuzzleHttp\Promise\Promise 246 | */ 247 | public function updateAsync(array $attributes = [], array $options = []) 248 | { 249 | return $this->fill($attributes)->saveAsync($options); 250 | } 251 | 252 | /** 253 | * Create a new record in the table. 254 | * 255 | * @param array $attributes 256 | * @return \Rennokki\DynamoDb\DynamoDbModel 257 | */ 258 | public static function create(array $attributes = []) 259 | { 260 | $model = new static; 261 | 262 | $model->fill($attributes)->save(); 263 | 264 | return $model; 265 | } 266 | 267 | /** 268 | * Trigger the delete action. 269 | * 270 | * @return bool|null 271 | */ 272 | public function delete() 273 | { 274 | if (is_null($this->getKeyName())) { 275 | throw new Exception('No primary key defined on model.'); 276 | } 277 | 278 | if ($this->exists) { 279 | if ($this->fireModelEvent('deleting') === false) { 280 | return false; 281 | } 282 | 283 | $this->exists = false; 284 | 285 | $success = $this->newQuery()->delete(); 286 | 287 | if ($success) { 288 | $this->fireModelEvent('deleted', false); 289 | } 290 | 291 | return $success; 292 | } 293 | } 294 | 295 | /** 296 | * Trigger the delete action and return a promise. 297 | * 298 | * @return bool|\GuzzleHttp\Promise\Promise 299 | */ 300 | public function deleteAsync() 301 | { 302 | if (is_null($this->getKeyName())) { 303 | throw new Exception('No primary key defined on model.'); 304 | } 305 | 306 | if ($this->exists) { 307 | if ($this->fireModelEvent('deleting') === false) { 308 | return false; 309 | } 310 | 311 | $this->exists = false; 312 | 313 | $deletePromise = $this->newQuery()->deleteAsync(); 314 | 315 | $deletePromise->then(function () { 316 | $this->fireModelEvent('deleted', false); 317 | }); 318 | 319 | return $deletePromise; 320 | } 321 | } 322 | 323 | /** 324 | * Get a list of all of the records. 325 | * 326 | * @param array $columns 327 | * @return \Rennokki\DynamoDb\DynamoDbCollection 328 | */ 329 | public static function all($columns = []) 330 | { 331 | $instance = new static; 332 | 333 | return $instance->newQuery()->get($columns); 334 | } 335 | 336 | /** 337 | * Refresh the model. 338 | * 339 | * @return \Rennokki\DynamoDb\DynamoDbModel 340 | */ 341 | public function refresh() 342 | { 343 | if (! $this->exists) { 344 | return $this; 345 | } 346 | 347 | $query = $this->newQuery(); 348 | 349 | $refreshed = $query->find($this->getKeys()); 350 | 351 | $this->setRawAttributes($refreshed->toArray()); 352 | 353 | return $this; 354 | } 355 | 356 | /** 357 | * Generate a new query. 358 | * 359 | * @return DynamoDbQueryBuilder 360 | */ 361 | public function newQuery() 362 | { 363 | $builder = new DynamoDbQueryBuilder($this); 364 | 365 | foreach ($this->getGlobalScopes() as $identifier => $scope) { 366 | $builder->withGlobalScope($identifier, $scope); 367 | } 368 | 369 | return $builder; 370 | } 371 | 372 | /** 373 | * Check if the model has a composite key. 374 | * 375 | * @return bool 376 | */ 377 | public function hasCompositeKey(): bool 378 | { 379 | return ! empty($this->compositeKey); 380 | } 381 | 382 | /** 383 | * Marshal an item. 384 | * 385 | * @param array $item 386 | * @return array 387 | */ 388 | public function marshalItem($item) 389 | { 390 | return $this->marshaler->marshalItem($item); 391 | } 392 | 393 | /** 394 | * Marshal a value. 395 | * 396 | * @param mixed $value 397 | * @return array 398 | */ 399 | public function marshalValue($value) 400 | { 401 | return $this->marshaler->marshalValue($value); 402 | } 403 | 404 | /** 405 | * Unmarshal an item. 406 | * 407 | * @param array $item 408 | * @return array|\stdClass 409 | */ 410 | public function unmarshalItem($item) 411 | { 412 | return $this->marshaler->unmarshalItem($item); 413 | } 414 | 415 | /** 416 | * Set a new id for this model. 417 | * 418 | * @param mixed $id 419 | * @return \Rennokki\DynamoDb\DynamoDbModel 420 | */ 421 | public function setId($id) 422 | { 423 | if (! is_array($id)) { 424 | $this->setAttribute($this->getKeyName(), $id); 425 | 426 | return $this; 427 | } 428 | 429 | foreach ($id as $keyName => $value) { 430 | $this->setAttribute($keyName, $value); 431 | } 432 | 433 | return $this; 434 | } 435 | 436 | /** 437 | * Get the DynamoDb client. 438 | * 439 | * @return \Aws\DynamoDb\DynamoDbClient 440 | */ 441 | public function getClient() 442 | { 443 | return static::$dynamoDb->getClient($this->connection); 444 | } 445 | 446 | /** 447 | * Get the value of the model's primary key. 448 | * 449 | * @return mixed 450 | */ 451 | public function getKey() 452 | { 453 | return $this->getAttribute($this->getKeyName()); 454 | } 455 | 456 | /** 457 | * Get the value of the model's primary / composite key. 458 | * Use this if you always want the key values in associative array form. 459 | * 460 | * @return array 461 | * 462 | * ['id' => 'foo'] 463 | * 464 | * or 465 | * 466 | * ['id' => 'foo', 'id2' => 'bar'] 467 | */ 468 | public function getKeys() 469 | { 470 | if ($this->hasCompositeKey()) { 471 | $key = []; 472 | 473 | foreach ($this->compositeKey as $name) { 474 | $key[$name] = $this->getAttribute($name); 475 | } 476 | 477 | return $key; 478 | } 479 | 480 | $name = $this->getKeyName(); 481 | 482 | return [$name => $this->getAttribute($name)]; 483 | } 484 | 485 | /** 486 | * Get the primary key for the model. 487 | * 488 | * @return string 489 | */ 490 | public function getKeyName() 491 | { 492 | return $this->primaryKey; 493 | } 494 | 495 | /** 496 | * Get the primary/composite key for the model. 497 | * 498 | * @return array 499 | */ 500 | public function getKeyNames() 501 | { 502 | return $this->hasCompositeKey() ? $this->compositeKey : [$this->primaryKey]; 503 | } 504 | 505 | /** 506 | * Get the DynamoDb index keys. 507 | * 508 | * @return array 509 | */ 510 | public function getDynamoDbIndexKeys() 511 | { 512 | return $this->dynamoDbIndexKeys; 513 | } 514 | 515 | /** 516 | * Set the DynamoDb index keys. 517 | * 518 | * @param array $dynamoDbIndexKeys 519 | * @return void 520 | */ 521 | public function setDynamoDbIndexKeys($dynamoDbIndexKeys) 522 | { 523 | $this->dynamoDbIndexKeys = $dynamoDbIndexKeys; 524 | } 525 | 526 | /** 527 | * Get the DynamoDb marshaler. 528 | * 529 | * @return \Aws\DynamoDb\Marshaler 530 | */ 531 | public function getMarshaler() 532 | { 533 | return $this->marshaler; 534 | } 535 | 536 | /** 537 | * Get the total item count for the table using 538 | * the describeTable() method from AWS SDK. 539 | * 540 | * @return int 541 | */ 542 | public static function getItemsCount(): int 543 | { 544 | $model = new static; 545 | 546 | $describeTable = $model->getClient()->describeTable([ 547 | 'TableName' => $model->getTable(), 548 | ]); 549 | 550 | return $describeTable->get('Table')['ItemCount']; 551 | } 552 | 553 | /** 554 | * Remove non-serializable properties when serializing. 555 | * 556 | * @return array 557 | */ 558 | public function __sleep() 559 | { 560 | return array_keys( 561 | Arr::except(get_object_vars($this), ['marshaler', 'attributeFilter']) 562 | ); 563 | } 564 | 565 | /** 566 | * When a model is being unserialized, check if it needs to be booted and setup DynamoDB. 567 | * 568 | * @return void 569 | */ 570 | public function __wakeup() 571 | { 572 | parent::__wakeup(); 573 | 574 | $this->setupDynamoDb(); 575 | } 576 | } 577 | -------------------------------------------------------------------------------- /src/DynamoDbQueryBuilder.php: -------------------------------------------------------------------------------- 1 | model = $model; 95 | $this->client = $model->getClient(); 96 | 97 | $this->setupExpressions(); 98 | } 99 | 100 | /** 101 | * Alias to set the "limit" value of the query. 102 | * 103 | * @param int $value 104 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 105 | */ 106 | public function take($value) 107 | { 108 | return $this->limit($value); 109 | } 110 | 111 | /** 112 | * Set the "limit" value of the query. 113 | * 114 | * @param int $value 115 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 116 | */ 117 | public function limit($value) 118 | { 119 | $this->limit = $value; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Alias to set the "offset" value of the query. 126 | * 127 | * @param int $value 128 | * @throws NotSupportedException 129 | */ 130 | public function skip($value) 131 | { 132 | return $this->offset($value); 133 | } 134 | 135 | /** 136 | * Set the "offset" value of the query. 137 | * 138 | * @param int $value 139 | * @throws NotSupportedException 140 | */ 141 | public function offset($value) 142 | { 143 | throw new NotSupportedException('Skip/Offset is not supported. Consider using after() instead'); 144 | } 145 | 146 | /** 147 | * Determine the starting point (exclusively) of the query. 148 | * Unfortunately, offset of how many records to skip does not make sense for DynamoDb. 149 | * Instead, provide the last result of the previous query as the starting point for the next query. 150 | * 151 | * @param DynamoDbModel|null $after 152 | * Examples: 153 | * 154 | * For query such as 155 | * $query = $model->where('count', 10)->limit(2); 156 | * $last = $query->all()->last(); 157 | * Take the last item of this query result as the next "offset": 158 | * $nextPage = $query->after($last)->limit(2)->all(); 159 | * 160 | * Alternatively, pass in nothing to reset the starting point. 161 | * 162 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 163 | */ 164 | public function after(DynamoDbModel $after = null) 165 | { 166 | if (empty($after)) { 167 | $this->lastEvaluatedKey = null; 168 | 169 | return $this; 170 | } 171 | 172 | $afterKey = $after->getKeys(); 173 | 174 | $analyzer = $this->getConditionAnalyzer(); 175 | 176 | if ($index = $analyzer->index()) { 177 | foreach ($index->columns() as $column) { 178 | $afterKey[$column] = $after->getAttribute($column); 179 | } 180 | } 181 | 182 | $this->lastEvaluatedKey = DynamoDb::marshalItem($afterKey); 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * Similar to after(), but instead of using the model instance, the model's keys are used. 189 | * Use $collection->lastKey() or $model->getKeys() to retrieve the value. 190 | * 191 | * @param array $key 192 | * Examples: 193 | * 194 | * For query such as 195 | * $query = $model->where('count', 10)->limit(2); 196 | * $items = $query->all(); 197 | * Take the last item of this query result as the next "offset": 198 | * $nextPage = $query->afterKey($items->lastKey())->limit(2)->all(); 199 | * 200 | * Alternatively, pass in nothing to reset the starting point. 201 | * 202 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 203 | */ 204 | public function afterKey($key = null) 205 | { 206 | $this->lastEvaluatedKey = empty($key) ? null : DynamoDb::marshalItem($key); 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * Set the index name manually. 213 | * 214 | * @param string $index 215 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 216 | */ 217 | public function withIndex($index) 218 | { 219 | $this->index = $index; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * Add a new where. 226 | * 227 | * @param string $column 228 | * @param string|null $operator 229 | * @param mixed $value 230 | * @param string $boolean 231 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 232 | */ 233 | public function where($column, $operator = null, $value = null, $boolean = 'and') 234 | { 235 | // If the column is an array, we will assume it is an array of key-value pairs 236 | // and can add them each as a where clause. We will maintain the boolean we 237 | // received when the method was called and pass it into the nested where. 238 | if (is_array($column)) { 239 | foreach ($column as $key => $value) { 240 | $this->where($key, '=', $value, $boolean); 241 | } 242 | 243 | return $this; 244 | } 245 | 246 | // Here we will make some assumptions about the operator. If only 2 values are 247 | // passed to the method, we will assume that the operator is an equals sign 248 | // and keep going. Otherwise, we'll require the operator to be passed in. 249 | if (func_num_args() === 2) { 250 | [$value, $operator] = [$operator, '=']; 251 | } 252 | 253 | // If the columns is actually a Closure instance, we will assume the developer 254 | // wants to begin a nested where statement which is wrapped in parenthesis. 255 | // We'll add that Closure to the query then return back out immediately. 256 | if ($column instanceof Closure) { 257 | return $this->whereNested($column, $boolean); 258 | } 259 | 260 | // If the given operator is not found in the list of valid operators we will 261 | // assume that the developer is just short-cutting the '=' operators and 262 | // we will set the operators to '=' and set the values appropriately. 263 | if (! ComparisonOperator::isValidOperator($operator)) { 264 | [$value, $operator] = [$operator, '=']; 265 | } 266 | 267 | // If the value is a Closure, it means the developer is performing an entire 268 | // sub-select within the query and we will need to compile the sub-select 269 | // within the where clause to get the appropriate query record results. 270 | if ($value instanceof Closure) { 271 | throw new NotSupportedException('Closure in where clause is not supported'); 272 | } 273 | 274 | $this->wheres[] = [ 275 | 'column' => $column, 276 | 'type' => ComparisonOperator::getDynamoDbOperator($operator), 277 | 'value' => $value, 278 | 'boolean' => $boolean, 279 | ]; 280 | 281 | return $this; 282 | } 283 | 284 | /** 285 | * Add a nested where statement to the query. 286 | * 287 | * @param \Closure $callback 288 | * @param string $boolean 289 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 290 | */ 291 | public function whereNested(Closure $callback, $boolean = 'and') 292 | { 293 | call_user_func($callback, $query = $this->forNestedWhere()); 294 | 295 | return $this->addNestedWhereQuery($query, $boolean); 296 | } 297 | 298 | /** 299 | * Create a new query instance for nested where condition. 300 | * 301 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 302 | */ 303 | public function forNestedWhere() 304 | { 305 | return $this->newQuery(); 306 | } 307 | 308 | /** 309 | * Add another query builder as a nested where to the query builder. 310 | * 311 | * @param DynamoDbQueryBuilder $query 312 | * @param string $boolean 313 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 314 | */ 315 | public function addNestedWhereQuery($query, $boolean = 'and') 316 | { 317 | if (count($query->wheres)) { 318 | $type = 'Nested'; 319 | 320 | $column = null; 321 | 322 | $value = $query->wheres; 323 | 324 | $this->wheres[] = compact('column', 'type', 'value', 'boolean'); 325 | } 326 | 327 | return $this; 328 | } 329 | 330 | /** 331 | * Add an "or where" clause to the query. 332 | * 333 | * @param string $column 334 | * @param string $operator 335 | * @param mixed $value 336 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 337 | */ 338 | public function orWhere($column, $operator = null, $value = null) 339 | { 340 | return $this->where($column, $operator, $value, 'or'); 341 | } 342 | 343 | /** 344 | * Add a "where in" clause to the query. 345 | * 346 | * @param string $column 347 | * @param mixed $values 348 | * @param string $boolean 349 | * @param bool $not 350 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 351 | * @throws NotSupportedException 352 | */ 353 | public function whereIn($column, $values, $boolean = 'and', $not = false) 354 | { 355 | if ($not) { 356 | throw new NotSupportedException('"not in" is not a valid DynamoDB comparison operator'); 357 | } 358 | 359 | // If the value is a query builder instance, not supported 360 | if ($values instanceof static) { 361 | throw new NotSupportedException('Value is a query builder instance'); 362 | } 363 | 364 | // If the value of the where in clause is actually a Closure, not supported 365 | if ($values instanceof Closure) { 366 | throw new NotSupportedException('Value is a Closure'); 367 | } 368 | 369 | // Next, if the value is Arrayable we need to cast it to its raw array form 370 | if ($values instanceof Arrayable) { 371 | $values = $values->toArray(); 372 | } 373 | 374 | return $this->where($column, ComparisonOperator::IN, $values, $boolean); 375 | } 376 | 377 | /** 378 | * Add an "or where in" clause to the query. 379 | * 380 | * @param string $column 381 | * @param mixed $values 382 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 383 | */ 384 | public function orWhereIn($column, $values) 385 | { 386 | return $this->whereIn($column, $values, 'or'); 387 | } 388 | 389 | /** 390 | * Add a "where null" clause to the query. 391 | * 392 | * @param string $column 393 | * @param string $boolean 394 | * @param bool $not 395 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 396 | */ 397 | public function whereNull($column, $boolean = 'and', $not = false) 398 | { 399 | $type = $not ? ComparisonOperator::NOT_NULL : ComparisonOperator::NULL; 400 | 401 | $this->wheres[] = compact('column', 'type', 'boolean'); 402 | 403 | return $this; 404 | } 405 | 406 | /** 407 | * Add an "or where null" clause to the query. 408 | * 409 | * @param string $column 410 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 411 | */ 412 | public function orWhereNull($column) 413 | { 414 | return $this->whereNull($column, 'or'); 415 | } 416 | 417 | /** 418 | * Add an "or where not null" clause to the query. 419 | * 420 | * @param string $column 421 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 422 | */ 423 | public function orWhereNotNull($column) 424 | { 425 | return $this->whereNotNull($column, 'or'); 426 | } 427 | 428 | /** 429 | * Add a "where not null" clause to the query. 430 | * 431 | * @param string $column 432 | * @param string $boolean 433 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 434 | */ 435 | public function whereNotNull($column, $boolean = 'and') 436 | { 437 | return $this->whereNull($column, $boolean, true); 438 | } 439 | 440 | /** 441 | * Get a new instance of the query builder. 442 | * 443 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 444 | */ 445 | public function newQuery() 446 | { 447 | return new static($this->getModel()); 448 | } 449 | 450 | /** 451 | * Implements the Query Chunk method. 452 | * 453 | * @param int $chunkSize 454 | * @param callable $callback 455 | * @return bool 456 | */ 457 | public function chunk($chunkSize, callable $callback) 458 | { 459 | while (true) { 460 | $results = $this->getAll([], $chunkSize, false); 461 | 462 | if (! $results->isEmpty()) { 463 | if (call_user_func($callback, $results) === false) { 464 | return false; 465 | } 466 | } 467 | 468 | if (empty($this->lastEvaluatedKey)) { 469 | break; 470 | } 471 | } 472 | 473 | return true; 474 | } 475 | 476 | /** 477 | * Find a record by ID. 478 | * 479 | * @param mixed $id 480 | * @param array $columns 481 | * @return DynamoDbModel|\Illuminate\Database\Eloquent\Collection|null 482 | */ 483 | public function find($id, array $columns = []) 484 | { 485 | if ($this->isMultipleIds($id)) { 486 | return $this->findMany($id, $columns); 487 | } 488 | 489 | $this->resetExpressions(); 490 | 491 | $this->model->setId($id); 492 | 493 | $query = DynamoDb::table($this->model->getTable()) 494 | ->setKey(DynamoDb::marshalItem($this->model->getKeys())) 495 | ->setConsistentRead(true); 496 | 497 | if (! empty($columns)) { 498 | $query->setProjectionExpression($this->projectionExpression->parse($columns)) 499 | ->setExpressionAttributeNames($this->expressionAttributeNames->all()); 500 | } 501 | 502 | $item = $query->prepare($this->client)->getItem(); 503 | $item = Arr::get($item->toArray(), 'Item'); 504 | 505 | if (empty($item)) { 506 | return; 507 | } 508 | 509 | $item = DynamoDb::unmarshalItem($item); 510 | $model = $this->model->newInstance([], true); 511 | 512 | $model->setRawAttributes($item, true); 513 | 514 | return $model; 515 | } 516 | 517 | /** 518 | * If more records by ids. 519 | * 520 | * @param array $ids 521 | * @param array $columns 522 | * @return \Illuminate\Database\Eloquent\Collection 523 | */ 524 | public function findMany($ids, array $columns = []) 525 | { 526 | $collection = $this->model->newCollection(); 527 | 528 | if (empty($ids)) { 529 | return $collection; 530 | } 531 | 532 | $this->resetExpressions(); 533 | 534 | $table = $this->model->getTable(); 535 | 536 | $keys = collect($ids)->map(function ($id) { 537 | if (! is_array($id)) { 538 | $id = [$this->model->getKeyName() => $id]; 539 | } 540 | 541 | return DynamoDb::marshalItem($id); 542 | }); 543 | 544 | $subQuery = DynamoDb::newQuery() 545 | ->setKeys($keys->toArray()) 546 | ->setProjectionExpression($this->projectionExpression->parse($columns)) 547 | ->setExpressionAttributeNames($this->expressionAttributeNames->all()) 548 | ->prepare($this->client) 549 | ->query; 550 | 551 | $results = DynamoDb::newQuery() 552 | ->setRequestItems([$table => $subQuery]) 553 | ->prepare($this->client) 554 | ->batchGetItem(); 555 | 556 | foreach ($results['Responses'][$table] as $item) { 557 | $item = DynamoDb::unmarshalItem($item); 558 | $model = $this->model->newInstance([], true); 559 | $model->setRawAttributes($item, true); 560 | $collection->add($model); 561 | } 562 | 563 | return $collection; 564 | } 565 | 566 | /** 567 | * Find a model by ID or throw 404. 568 | * 569 | * @param mixed $id 570 | * @param array $columns 571 | * @return DynamoDbModel|\Illuminate\Database\Eloquent\Collection|null 572 | * @throws ModelNotFoundException 573 | */ 574 | public function findOrFail($id, $columns = []) 575 | { 576 | $result = $this->find($id, $columns); 577 | 578 | if ($this->isMultipleIds($id)) { 579 | if (count($result) === count(array_unique($id))) { 580 | return $result; 581 | } 582 | } elseif (! is_null($result)) { 583 | return $result; 584 | } 585 | 586 | throw (new ModelNotFoundException)->setModel( 587 | get_class($this->model), 588 | $id 589 | ); 590 | } 591 | 592 | /** 593 | * Get the first record. 594 | * 595 | * @param array $columns 596 | * @return DynamoDbModel 597 | */ 598 | public function first($columns = []) 599 | { 600 | $items = $this->getAll($columns, 1); 601 | 602 | return $items->first(); 603 | } 604 | 605 | /** 606 | * Get the first record or throw 404. 607 | * 608 | * @param array $columns 609 | * @return DynamoDbModel 610 | * @throws ModelNotFoundException 611 | */ 612 | public function firstOrFail($columns = []) 613 | { 614 | if (! is_null($model = $this->first($columns))) { 615 | return $model; 616 | } 617 | 618 | throw (new ModelNotFoundException)->setModel(get_class($this->model)); 619 | } 620 | 621 | /** 622 | * Remove attributes from an existing item. 623 | * 624 | * @param array ...$attributes 625 | * @return bool 626 | * @throws InvalidQuery 627 | */ 628 | public function removeAttribute(...$attributes) 629 | { 630 | $keySet = ! empty(array_filter($this->model->getKeys())); 631 | 632 | if (! $keySet) { 633 | $analyzer = $this->getConditionAnalyzer(); 634 | 635 | if (! $analyzer->isExactSearch()) { 636 | throw new InvalidQuery('Need to provide the key in your query'); 637 | } 638 | 639 | $id = $analyzer->identifierConditionValues(); 640 | $this->model->setId($id); 641 | } 642 | 643 | $key = DynamoDb::marshalItem($this->model->getKeys()); 644 | 645 | $this->resetExpressions(); 646 | 647 | $result = DynamoDb::table($this->model->getTable()) 648 | ->setKey($key) 649 | ->setUpdateExpression($this->updateExpression->remove($attributes)) 650 | ->setExpressionAttributeNames($this->expressionAttributeNames->all()) 651 | ->setReturnValues('ALL_NEW') 652 | ->prepare($this->client) 653 | ->updateItem(); 654 | 655 | $success = Arr::get($result, '@metadata.statusCode') === 200; 656 | 657 | if ($success) { 658 | $this->model->setRawAttributes(DynamoDb::unmarshalItem($result->get('Attributes'))); 659 | $this->model->syncOriginal(); 660 | } 661 | 662 | return $success; 663 | } 664 | 665 | /** 666 | * Delete the model. 667 | * 668 | * @return bool 669 | */ 670 | public function delete() 671 | { 672 | $result = DynamoDb::table($this->model->getTable()) 673 | ->setKey(DynamoDb::marshalItem($this->model->getKeys())) 674 | ->prepare($this->client) 675 | ->deleteItem(); 676 | 677 | return Arr::get($result->toArray(), '@metadata.statusCode') === 200; 678 | } 679 | 680 | /** 681 | * Delete the model and return a promise. 682 | * 683 | * @return bool|\GuzzleHttp\Promise\Promise 684 | */ 685 | public function deleteAsync() 686 | { 687 | $promise = DynamoDb::table($this->model->getTable()) 688 | ->setKey(DynamoDb::marshalItem($this->model->getKeys())) 689 | ->prepare($this->client) 690 | ->deleteItemAsync(); 691 | 692 | return $promise; 693 | } 694 | 695 | /** 696 | * Save the model. 697 | * 698 | * @return bool 699 | */ 700 | public function save() 701 | { 702 | $result = DynamoDb::table($this->model->getTable()) 703 | ->setItem(DynamoDb::marshalItem($this->model->getAttributes())) 704 | ->prepare($this->client) 705 | ->putItem(); 706 | 707 | return Arr::get($result, '@metadata.statusCode') === 200; 708 | } 709 | 710 | /** 711 | * Save the model and return a promise. 712 | * 713 | * @return bool|\GuzzleHttp\Promise\Promise 714 | */ 715 | public function saveAsync() 716 | { 717 | $promise = DynamoDb::table($this->model->getTable()) 718 | ->setItem(DynamoDb::marshalItem($this->model->getAttributes())) 719 | ->prepare($this->client) 720 | ->putItemAsync(); 721 | 722 | return $promise; 723 | } 724 | 725 | /** 726 | * Get the reocrds. 727 | * 728 | * @param array $columns 729 | * @return \Illuminate\Database\Eloquent\Collection 730 | */ 731 | public function get($columns = []) 732 | { 733 | return $this->all($columns); 734 | } 735 | 736 | /** 737 | * Get all the records. 738 | * 739 | * @param array $columns 740 | * @return \Illuminate\Database\Eloquent\Collection 741 | */ 742 | public function all($columns = []) 743 | { 744 | $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT; 745 | 746 | return $this->getAll($columns, $limit, ! isset($this->limit)); 747 | } 748 | 749 | /** 750 | * Get the count for this query. 751 | * 752 | * @return int 753 | */ 754 | public function count() 755 | { 756 | $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT; 757 | $raw = $this->toDynamoDbQuery(['count(*)'], $limit); 758 | 759 | $res = $raw->op === 'Scan' 760 | ? $this->client->scan($raw->query) 761 | : $this->client->query($raw->query); 762 | 763 | return $res['Count']; 764 | } 765 | 766 | /** 767 | * Decorate the query builder. 768 | * 769 | * @param \Closure $closure 770 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 771 | */ 772 | public function decorate(Closure $closure) 773 | { 774 | $this->decorator = $closure; 775 | 776 | return $this; 777 | } 778 | 779 | /** 780 | * Get all results for the iterator. 781 | * 782 | * @param array $columns 783 | * @param int $limit 784 | * @param string $useIterator 785 | * @return \Rennokki\DynamoDb\DynamoDbCollection 786 | */ 787 | protected function getAll($columns = [], $limit = self::MAX_LIMIT, $useIterator = self::DEFAULT_TO_ITERATOR) 788 | { 789 | $analyzer = $this->getConditionAnalyzer(); 790 | 791 | if ($analyzer->isExactSearch()) { 792 | $item = $this->find($analyzer->identifierConditionValues(), $columns); 793 | 794 | return $this->getModel()->newCollection([$item]); 795 | } 796 | 797 | $raw = $this->toDynamoDbQuery($columns, $limit); 798 | 799 | if ($useIterator) { 800 | $iterator = $this->client->getIterator($raw->op, $raw->query); 801 | 802 | if (isset($raw->query['Limit'])) { 803 | $iterator = new \LimitIterator($iterator, 0, $raw->query['Limit']); 804 | } 805 | } else { 806 | $res = $raw->op === 'Scan' 807 | ? $this->client->scan($raw->query) 808 | : $this->client->query($raw->query); 809 | 810 | $this->lastEvaluatedKey = Arr::get($res, 'LastEvaluatedKey'); 811 | 812 | $iterator = $res['Items']; 813 | } 814 | 815 | $results = []; 816 | 817 | foreach ($iterator as $item) { 818 | $item = DynamoDb::unmarshalItem($item); 819 | $model = $this->model->newInstance([], true); 820 | $model->setRawAttributes($item, true); 821 | $results[] = $model; 822 | } 823 | 824 | return $this->getModel()->newCollection($results, $analyzer->index()); 825 | } 826 | 827 | /** 828 | * Return the raw DynamoDb query. 829 | * 830 | * @param array $columns 831 | * @param int $limit 832 | * @return RawDynamoDbQuery 833 | */ 834 | public function toDynamoDbQuery($columns = [], $limit = self::MAX_LIMIT) 835 | { 836 | $this->applyScopes(); 837 | 838 | $this->resetExpressions(); 839 | 840 | $op = 'Scan'; 841 | $queryBuilder = DynamoDb::table($this->model->getTable()); 842 | 843 | if (! empty($this->wheres)) { 844 | $analyzer = $this->getConditionAnalyzer(); 845 | 846 | if ($keyConditions = $analyzer->keyConditions()) { 847 | $op = 'Query'; 848 | $queryBuilder->setKeyConditionExpression($this->keyConditionExpression->parse($keyConditions)); 849 | } 850 | 851 | if ($filterConditions = $analyzer->filterConditions()) { 852 | $queryBuilder->setFilterExpression($this->filterExpression->parse($filterConditions)); 853 | } 854 | 855 | if ($index = $analyzer->index()) { 856 | $queryBuilder->setIndexName($index->name); 857 | } 858 | } 859 | 860 | if ($this->index) { 861 | // If user specifies the index manually, respect that 862 | 863 | $queryBuilder->setIndexName($this->index); 864 | } 865 | 866 | if ($limit !== static::MAX_LIMIT) { 867 | $queryBuilder->setLimit($limit); 868 | } 869 | 870 | if (! empty($columns)) { 871 | // Either we try to get the count or specific columns 872 | 873 | $columns === ['count(*)'] 874 | ? $queryBuilder->setSelect('COUNT') 875 | : $queryBuilder->setProjectionExpression($this->projectionExpression->parse($columns)); 876 | } 877 | 878 | if (! empty($this->lastEvaluatedKey)) { 879 | $queryBuilder->setExclusiveStartKey($this->lastEvaluatedKey); 880 | } 881 | 882 | $queryBuilder 883 | ->setExpressionAttributeNames($this->expressionAttributeNames->all()) 884 | ->setExpressionAttributeValues($this->expressionAttributeValues->all()); 885 | 886 | $raw = new RawDynamoDbQuery($op, $queryBuilder->prepare($this->client)->query); 887 | 888 | if ($this->decorator) { 889 | call_user_func($this->decorator, $raw); 890 | } 891 | 892 | return $raw; 893 | } 894 | 895 | /** 896 | * Get the condition analyzer. 897 | * 898 | * @return Analyzer 899 | */ 900 | protected function getConditionAnalyzer() 901 | { 902 | return with(new Analyzer) 903 | ->on($this->model) 904 | ->withIndex($this->index) 905 | ->analyze($this->wheres); 906 | } 907 | 908 | /** 909 | * Check if the input are more ids. 910 | * 911 | * @param string|array $id 912 | * @return bool 913 | */ 914 | protected function isMultipleIds($id) 915 | { 916 | $keys = collect($this->model->getKeyNames()); 917 | 918 | // could be ['id' => 'foo'], ['id1' => 'foo', 'id2' => 'bar'] 919 | $single = $keys->first(function ($name) use ($id) { 920 | return ! isset($id[$name]); 921 | }) === null; 922 | 923 | if ($single) { 924 | return false; 925 | } 926 | 927 | // could be ['foo', 'bar'], [['id1' => 'foo', 'id2' => 'bar'], ...] 928 | return $this->model->hasCompositeKey() ? is_array(Arr::first($id)) : is_array($id); 929 | } 930 | 931 | /** 932 | * Get the model. 933 | * 934 | * @return DynamoDbModel 935 | */ 936 | public function getModel() 937 | { 938 | return $this->model; 939 | } 940 | 941 | /** 942 | * Get the DynamoDb client. 943 | * 944 | * @return \Aws\DynamoDb\DynamoDbClient 945 | */ 946 | public function getClient() 947 | { 948 | return $this->client; 949 | } 950 | 951 | /** 952 | * Register a new global scope. 953 | * 954 | * @param string $identifier 955 | * @param \Illuminate\Database\Eloquent\Scope|\Closure $scope 956 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 957 | */ 958 | public function withGlobalScope($identifier, $scope) 959 | { 960 | $this->scopes[$identifier] = $scope; 961 | 962 | if (method_exists($scope, 'extend')) { 963 | $scope->extend($this); 964 | } 965 | 966 | return $this; 967 | } 968 | 969 | /** 970 | * Remove a registered global scope. 971 | * 972 | * @param \Illuminate\Database\Eloquent\Scope|string $scope 973 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 974 | */ 975 | public function withoutGlobalScope($scope) 976 | { 977 | if (! is_string($scope)) { 978 | $scope = get_class($scope); 979 | } 980 | 981 | unset($this->scopes[$scope]); 982 | 983 | $this->removedScopes[] = $scope; 984 | 985 | return $this; 986 | } 987 | 988 | /** 989 | * Remove all or passed registered global scopes. 990 | * 991 | * @param array|null $scopes 992 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 993 | */ 994 | public function withoutGlobalScopes(array $scopes = null) 995 | { 996 | if (is_array($scopes)) { 997 | foreach ($scopes as $scope) { 998 | $this->withoutGlobalScope($scope); 999 | } 1000 | } else { 1001 | $this->scopes = []; 1002 | } 1003 | 1004 | return $this; 1005 | } 1006 | 1007 | /** 1008 | * Get an array of global scopes that were removed from the query. 1009 | * 1010 | * @return array 1011 | */ 1012 | public function removedScopes(): array 1013 | { 1014 | return $this->removedScopes; 1015 | } 1016 | 1017 | /** 1018 | * Apply the scopes to the Eloquent builder instance and return it. 1019 | * 1020 | * @return \Rennokki\DynamoDb\DynamoDbQueryBuilder 1021 | */ 1022 | public function applyScopes() 1023 | { 1024 | if (! $this->scopes) { 1025 | return $this; 1026 | } 1027 | 1028 | $builder = $this; 1029 | 1030 | foreach ($builder->scopes as $identifier => $scope) { 1031 | if (! isset($builder->scopes[$identifier])) { 1032 | continue; 1033 | } 1034 | 1035 | $builder->callScope(function (self $builder) use ($scope) { 1036 | // If the scope is a Closure we will just go ahead and call the scope with the 1037 | // builder instance. The "callScope" method will properly group the clauses 1038 | // that are added to this query so "where" clauses maintain proper logic. 1039 | if ($scope instanceof Closure) { 1040 | $scope($builder); 1041 | } 1042 | 1043 | // If the scope is a scope object, we will call the apply method on this scope 1044 | // passing in the builder and the model instance. After we run all of these 1045 | // scopes we will return back the builder instance to the outside caller. 1046 | if ($scope instanceof Scope) { 1047 | throw new NotSupportedException('Scope object is not yet supported'); 1048 | } 1049 | }); 1050 | 1051 | $builder->withoutGlobalScope($identifier); 1052 | } 1053 | 1054 | return $builder; 1055 | } 1056 | 1057 | /** 1058 | * Apply the given scope on the current builder instance. 1059 | * 1060 | * @param callable $scope 1061 | * @param array $parameters 1062 | * @return mixed 1063 | */ 1064 | protected function callScope(callable $scope, $parameters = []) 1065 | { 1066 | array_unshift($parameters, $this); 1067 | 1068 | // $query = $this->getQuery(); 1069 | 1070 | // // We will keep track of how many wheres are on the query before running the 1071 | // // scope so that we can properly group the added scope constraints in the 1072 | // // query as their own isolated nested where statement and avoid issues. 1073 | // $originalWhereCount = is_null($query->wheres) 1074 | // ? 0 : count($query->wheres); 1075 | 1076 | $result = $scope(...array_values($parameters)) ?: $this; 1077 | 1078 | // if (count((array) $query->wheres) > $originalWhereCount) { 1079 | // $this->addNewWheresWithinGroup($query, $originalWhereCount); 1080 | // } 1081 | 1082 | return $result; 1083 | } 1084 | 1085 | /** 1086 | * Dynamically handle calls into the query instance. 1087 | * 1088 | * @param string $method 1089 | * @param array $parameters 1090 | * @return mixed 1091 | */ 1092 | public function __call($method, $parameters) 1093 | { 1094 | if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) { 1095 | return $this->callScope([$this->model, $scope], $parameters); 1096 | } 1097 | 1098 | return $this; 1099 | } 1100 | } 1101 | -------------------------------------------------------------------------------- /src/DynamoDbServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make(DynamoDbClientInterface::class) 20 | ); 21 | 22 | $this->publishes([ 23 | __DIR__.'/../config/dynamodb.php' => config_path('dynamodb.php'), 24 | ]); 25 | 26 | $this->mergeConfigFrom( 27 | __DIR__.'/../config/dynamodb.php', 'dynamodb' 28 | ); 29 | } 30 | 31 | /** 32 | * Register any application services. 33 | * 34 | * @return void 35 | */ 36 | public function register() 37 | { 38 | $marshalerOptions = [ 39 | 'nullify_invalid' => true, 40 | ]; 41 | 42 | $this->app->singleton(DynamoDbClientInterface::class, function () use ($marshalerOptions) { 43 | return new DynamoDbClientService( 44 | new Marshaler($marshalerOptions), new EmptyAttributeFilter 45 | ); 46 | }); 47 | 48 | $this->app->singleton('dynamodb', function () { 49 | return new DynamoDbManager( 50 | $this->app->make(DynamoDbClientInterface::class) 51 | ); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/EmptyAttributeFilter.php: -------------------------------------------------------------------------------- 1 | &$value) { 26 | $value = is_string($value) ? trim($value) : $value; 27 | $empty = $value === null || (is_array($value) && empty($value)); 28 | 29 | $empty = $empty || (is_scalar($value) && $value !== false && (string) $value === ''); 30 | 31 | if ($empty) { 32 | $store[$key] = null; 33 | } else { 34 | if (is_object($value)) { 35 | $value = (array) $value; 36 | } 37 | 38 | if (is_array($value)) { 39 | $this->filter($value); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Facades/DynamoDb.php: -------------------------------------------------------------------------------- 1 | dynamoDbClient = $dynamoDb->getClient(); 40 | $this->marshaler = $dynamoDb->getMarshaler(); 41 | $this->attributeFilter = $dynamoDb->getAttributeFilter(); 42 | } 43 | 44 | /** 45 | * Trigger the DynamoDb query to save the model. 46 | * 47 | * @param \Illuminate\Database\Eloquent\Model $model 48 | * @return void 49 | */ 50 | private function saveToDynamoDb($model) 51 | { 52 | $attrs = $model->attributesToArray(); 53 | 54 | try { 55 | $this->dynamoDbClient->putItem([ 56 | 'TableName' => $model->getDynamoDbTableName(), 57 | 'Item' => $this->marshaler->marshalItem($attrs), 58 | ]); 59 | } catch (Exception $e) { 60 | Log::error($e); 61 | } 62 | } 63 | 64 | /** 65 | * Trigger the DynamoDb query to delete the model. 66 | * 67 | * @param \Illuminate\Database\Eloquent\Model $model 68 | * @return void 69 | */ 70 | private function deleteFromDynamoDb($model) 71 | { 72 | $key = [$model->getKeyName() => $model->getKey()]; 73 | 74 | try { 75 | $this->dynamoDbClient->deleteItem([ 76 | 'TableName' => $model->getDynamoDbTableName(), 77 | 'Key' => $this->marshaler->marshalItem($key), 78 | ]); 79 | } catch (Exception $e) { 80 | Log::error($e); 81 | } 82 | } 83 | 84 | /** 85 | * Handle the Model "created" event. 86 | * 87 | * @param \Illuminate\Database\Eloquent\Model $model 88 | * @return void 89 | */ 90 | public function created($model) 91 | { 92 | $this->saveToDynamoDb($model); 93 | } 94 | 95 | /** 96 | * Handle the Model "updated" event. 97 | * 98 | * @param \Illuminate\Database\Eloquent\Model $model 99 | * @return void 100 | */ 101 | public function updated($model) 102 | { 103 | $this->saveToDynamoDb($model); 104 | } 105 | 106 | /** 107 | * Handle the Model "deleted" event. 108 | * 109 | * @param \Illuminate\Database\Eloquent\Model $model 110 | * @return void 111 | */ 112 | public function deleted($model) 113 | { 114 | $this->deleteFromDynamoDb($model); 115 | } 116 | 117 | /** 118 | * Handle the \Illuminate\Database\Eloquent\Model "restored" event. 119 | * 120 | * @param \Illuminate\Database\Eloquent\Model $model 121 | * @return void 122 | */ 123 | public function restored($model) 124 | { 125 | $this->saveToDynamoDb($model); 126 | } 127 | 128 | /** 129 | * Handle the \Illuminate\Database\Eloquent\Model "force deleted" event. 130 | * 131 | * @param \Illuminate\Database\Eloquent\Model $model 132 | * @return void 133 | */ 134 | public function forceDeleted($model) 135 | { 136 | $this->deleted($model); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/ModelTrait.php: -------------------------------------------------------------------------------- 1 | getTable(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/NotSupportedException.php: -------------------------------------------------------------------------------- 1 | '%s = :%s', 20 | ComparisonOperator::LE => '%s <= :%s', 21 | ComparisonOperator::LT => '%s < :%s', 22 | ComparisonOperator::GE => '%s >= :%s', 23 | ComparisonOperator::GT => '%s > :%s', 24 | ComparisonOperator::BEGINS_WITH => 'begins_with(%s, :%s)', 25 | ComparisonOperator::BETWEEN => '(%s BETWEEN :%s AND :%s)', 26 | ComparisonOperator::CONTAINS => 'contains(%s, :%s)', 27 | ComparisonOperator::NOT_CONTAINS => 'NOT contains(%s, :%s)', 28 | ComparisonOperator::NULL => 'attribute_not_exists(%s)', 29 | ComparisonOperator::NOT_NULL => 'attribute_exists(%s)', 30 | ComparisonOperator::NE => '%s <> :%s', 31 | ComparisonOperator::IN => '%s IN (%s)', 32 | ]; 33 | 34 | /** 35 | * The values for the condition expression. 36 | * 37 | * @var ExpressionAttributeValues 38 | */ 39 | protected $values; 40 | 41 | /** 42 | * The attribute names. 43 | * 44 | * @var ExpressionAttributeNames 45 | */ 46 | protected $names; 47 | 48 | /** 49 | * The placeholder. 50 | * 51 | * @var Placeholder 52 | */ 53 | protected $placeholder; 54 | 55 | /** 56 | * Initialize the class. 57 | * 58 | * @param Placeholder $placeholder 59 | * @param ExpressionAttributeValues $values 60 | * @param ExpressionAttributeNames $names 61 | * @return void 62 | */ 63 | public function __construct(Placeholder $placeholder, ExpressionAttributeValues $values, ExpressionAttributeNames $names) 64 | { 65 | $this->placeholder = $placeholder; 66 | $this->values = $values; 67 | $this->names = $names; 68 | } 69 | 70 | /** 71 | * Parse the where condition. 72 | * 73 | * @param array $where 74 | * [ 75 | * 'column' => 'name', 76 | * 'type' => 'EQ', 77 | * 'value' => 'foo', 78 | * 'boolean' => 'and', 79 | * ] 80 | * 81 | * @return string 82 | * @throws NotSupportedException 83 | */ 84 | public function parse($where) 85 | { 86 | if (empty($where)) { 87 | return ''; 88 | } 89 | 90 | $parsed = []; 91 | 92 | foreach ($where as $condition) { 93 | $boolean = Arr::get($condition, 'boolean'); 94 | $value = Arr::get($condition, 'value'); 95 | $type = Arr::get($condition, 'type'); 96 | 97 | $prefix = ''; 98 | 99 | if (count($parsed) > 0) { 100 | $prefix = strtoupper($boolean).' '; 101 | } 102 | 103 | if ($type === 'Nested') { 104 | $parsed[] = $prefix.$this->parseNestedCondition($value); 105 | continue; 106 | } 107 | 108 | $parsed[] = $prefix.$this->parseCondition( 109 | Arr::get($condition, 'column'), 110 | $type, 111 | $value 112 | ); 113 | } 114 | 115 | return implode(' ', $parsed); 116 | } 117 | 118 | /** 119 | * Reset the values. 120 | * 121 | * @return void 122 | */ 123 | public function reset() 124 | { 125 | $this->placeholder->reset(); 126 | $this->names->reset(); 127 | $this->values->reset(); 128 | } 129 | 130 | /** 131 | * Get a list of all supported operators. 132 | * 133 | * @return array 134 | */ 135 | protected function getSupportedOperators(): array 136 | { 137 | return static::OPERATORS; 138 | } 139 | 140 | /** 141 | * Parse the nested conditions. 142 | * 143 | * @param array $conditions 144 | * @return string 145 | */ 146 | protected function parseNestedCondition(array $conditions): string 147 | { 148 | $conditions = $this->parse($conditions); 149 | 150 | return "({$conditions})"; 151 | } 152 | 153 | /** 154 | * Parse the condition. 155 | * 156 | * @param string $name 157 | * @param string $operator 158 | * @param mixed $value 159 | * @return mixed 160 | */ 161 | protected function parseCondition($name, $operator, $value) 162 | { 163 | $operators = $this->getSupportedOperators(); 164 | 165 | if (empty($operators[$operator])) { 166 | throw new NotSupportedException("$operator is not supported"); 167 | } 168 | 169 | $template = $operators[$operator]; 170 | 171 | $this->names->set($name); 172 | 173 | if ($operator === ComparisonOperator::BETWEEN) { 174 | return $this->parseBetweenCondition($name, $value, $template); 175 | } 176 | 177 | if ($operator === ComparisonOperator::IN) { 178 | return $this->parseInCondition($name, $value, $template); 179 | } 180 | 181 | if ($operator === ComparisonOperator::NULL || $operator === ComparisonOperator::NOT_NULL) { 182 | return $this->parseNullCondition($name, $template); 183 | } 184 | 185 | $placeholder = $this->placeholder->next(); 186 | 187 | $this->values->set($placeholder, DynamoDb::marshalValue($value)); 188 | 189 | return sprintf($template, $this->names->placeholder($name), $placeholder); 190 | } 191 | 192 | /** 193 | * Parse the between condition. 194 | * 195 | * @param string $name 196 | * @param mixed $value 197 | * @param mixed $template 198 | * @return string 199 | */ 200 | protected function parseBetweenCondition($name, $value, $template) 201 | { 202 | $first = $this->placeholder->next(); 203 | $second = $this->placeholder->next(); 204 | 205 | $this->values->set($first, DynamoDb::marshalValue($value[0])); 206 | $this->values->set($second, DynamoDb::marshalValue($value[1])); 207 | 208 | return sprintf($template, $this->names->placeholder($name), $first, $second); 209 | } 210 | 211 | /** 212 | * Parse the whereIn condition. 213 | * 214 | * @param string $name 215 | * @param mixed $value 216 | * @param mixed $template 217 | * @return void 218 | */ 219 | protected function parseInCondition($name, $value, $template) 220 | { 221 | $valuePlaceholders = []; 222 | 223 | foreach ($value as $item) { 224 | $placeholder = $this->placeholder->next(); 225 | $valuePlaceholders[] = ':'.$placeholder; 226 | 227 | $this->values->set($placeholder, DynamoDb::marshalValue($item)); 228 | } 229 | 230 | return sprintf($template, $this->names->placeholder($name), implode(', ', $valuePlaceholders)); 231 | } 232 | 233 | /** 234 | * Parse the null condition. 235 | * 236 | * @param string $name 237 | * @param mixed $template 238 | * @return string 239 | */ 240 | protected function parseNullCondition($name, $template) 241 | { 242 | return sprintf($template, $this->names->placeholder($name)); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Parsers/ExpressionAttributeNames.php: -------------------------------------------------------------------------------- 1 | reset(); 37 | 38 | $this->prefix = $prefix; 39 | } 40 | 41 | /** 42 | * Set the attribute name. 43 | * 44 | * @param string $name 45 | * @return void 46 | */ 47 | public function set($name) 48 | { 49 | if ($this->isNested($name)) { 50 | $this->nested[] = $name; 51 | 52 | return; 53 | } 54 | 55 | $this->mapping["{$this->prefix}{$name}"] = $name; 56 | } 57 | 58 | /** 59 | * Get the attribute name. 60 | * 61 | * @param string $placeholder 62 | * @return mixed 63 | */ 64 | public function get($placeholder) 65 | { 66 | return $this->mapping[$placeholder]; 67 | } 68 | 69 | /** 70 | * Set the placeholder. 71 | * 72 | * @param string $name 73 | * @return string 74 | */ 75 | public function placeholder($name): string 76 | { 77 | $placeholder = "{$this->prefix}{$name}"; 78 | 79 | if (isset($this->mapping[$placeholder])) { 80 | return $placeholder; 81 | } 82 | 83 | return $name; 84 | } 85 | 86 | /** 87 | * Get all the mappings. 88 | * 89 | * @return array|null 90 | */ 91 | public function all() 92 | { 93 | return $this->mapping; 94 | } 95 | 96 | /** 97 | * Get the placeholders. 98 | * 99 | * @return array 100 | */ 101 | public function placeholders() 102 | { 103 | return array_merge(array_keys($this->mapping), $this->nested); 104 | } 105 | 106 | /** 107 | * Reset the values. 108 | * 109 | * @return \Rennokki\DynamoDb\Parsers\ExpressionAttributeNames 110 | */ 111 | public function reset() 112 | { 113 | $this->mapping = []; 114 | $this->nested = []; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * Check if the attribute name is nested. 121 | * 122 | * @param string $name 123 | * @return bool 124 | */ 125 | private function isNested($name) 126 | { 127 | return strpos($name, '.') !== false || (strpos($name, '[') !== false && strpos($name, ']') !== false); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Parsers/ExpressionAttributeValues.php: -------------------------------------------------------------------------------- 1 | reset(); 29 | 30 | $this->prefix = $prefix; 31 | } 32 | 33 | /** 34 | * Set a value for the attribute. 35 | * 36 | * @param string $placeholder 37 | * @param mixed $value 38 | * @return void 39 | */ 40 | public function set($placeholder, $value) 41 | { 42 | $this->mapping["{$this->prefix}{$placeholder}"] = $value; 43 | } 44 | 45 | /** 46 | * Get the value for a placeholder. 47 | * 48 | * @param string $placeholder 49 | * @return mixed 50 | */ 51 | public function get($placeholder) 52 | { 53 | return $this->mapping[$placeholder]; 54 | } 55 | 56 | /** 57 | * Get all the mappings. 58 | * 59 | * @return array|null 60 | */ 61 | public function all() 62 | { 63 | return $this->mapping; 64 | } 65 | 66 | /** 67 | * Get the placeholders. 68 | * 69 | * @return array 70 | */ 71 | public function placeholders(): array 72 | { 73 | return array_keys($this->mapping); 74 | } 75 | 76 | /** 77 | * Reset the mappings. 78 | * 79 | * @return \Rennokki\DynamoDb\Parsers\ExpressionAttributeValues 80 | */ 81 | public function reset() 82 | { 83 | $this->mapping = []; 84 | 85 | return $this; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Parsers/FilterExpression.php: -------------------------------------------------------------------------------- 1 | reset(); 22 | } 23 | 24 | /** 25 | * Increment the value number. 26 | * 27 | * @return string 28 | */ 29 | public function next(): string 30 | { 31 | $this->counter += 1; 32 | 33 | return "a{$this->counter}"; 34 | } 35 | 36 | /** 37 | * Reset the counter. 38 | * 39 | * @return \Rennokki\Parsers\Placeholder 40 | */ 41 | public function reset() 42 | { 43 | $this->counter = 0; 44 | 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Parsers/ProjectionExpression.php: -------------------------------------------------------------------------------- 1 | names = $names; 18 | } 19 | 20 | /** 21 | * Parse the columns for the projection. 22 | * 23 | * @param array $columns 24 | * @return string 25 | */ 26 | public function parse(array $columns): string 27 | { 28 | foreach ($columns as $column) { 29 | $this->names->set($column); 30 | } 31 | 32 | return implode(', ', $this->names->placeholders()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Parsers/UpdateExpression.php: -------------------------------------------------------------------------------- 1 | names = $names; 23 | } 24 | 25 | /** 26 | * Reset the names. 27 | * 28 | * @return void 29 | */ 30 | public function reset() 31 | { 32 | $this->names->reset(); 33 | } 34 | 35 | /** 36 | * Remove the given attributes. 37 | * 38 | * @param array $attributes 39 | * @return string 40 | */ 41 | public function remove(array $attributes): string 42 | { 43 | foreach ($attributes as $attribute) { 44 | $this->names->set($attribute); 45 | } 46 | 47 | return 'REMOVE '.implode(', ', $this->names->placeholders()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/RawDynamoDbQuery.php: -------------------------------------------------------------------------------- 1 | op = $op; 31 | $this->query = $query; 32 | } 33 | 34 | /** 35 | * Perform any final clean up. 36 | * Remove any empty values to avoid errors. 37 | * 38 | * @return \Rennokki\DynamoDb\RawDynamoDbQuery 39 | */ 40 | public function finalize() 41 | { 42 | $this->query = array_filter($this->query, function ($value) { 43 | return ! empty($value) || is_bool($value) || is_numeric($value); 44 | }); 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Whether a offset exists. 51 | * http://php.net/manual/en/arrayaccess.offsetexists.php. 52 | * @param mixed $offset 53 | * @return bool 54 | */ 55 | public function offsetExists($offset) 56 | { 57 | return isset($this->internal()[$offset]); 58 | } 59 | 60 | /** 61 | * Offset to retrieve. 62 | * http://php.net/manual/en/arrayaccess.offsetget.php. 63 | * 64 | * @param mixed $offset 65 | * @return mixed 66 | */ 67 | public function offsetGet($offset) 68 | { 69 | return $this->internal()[$offset]; 70 | } 71 | 72 | /** 73 | * Offset to set. 74 | * @link http://php.net/manual/en/arrayaccess.offsetset.php 75 | * @param mixed $offset

76 | * The offset to assign the value to. 77 | *

78 | * @param mixed $value

79 | * The value to set. 80 | *

81 | * @return void 82 | * @since 5.0.0 83 | */ 84 | public function offsetSet($offset, $value) 85 | { 86 | $this->internal()[$offset] = $value; 87 | } 88 | 89 | /** 90 | * Offset to unset. 91 | * http://php.net/manual/en/arrayaccess.offsetunset.php. 92 | * 93 | * @param mixed $offset 94 | * @return void 95 | */ 96 | public function offsetUnset($offset) 97 | { 98 | unset($this->internal()[$offset]); 99 | } 100 | 101 | /** 102 | * Retrieve an external iterator. 103 | * http://php.net/manual/en/iteratoraggregate.getiterator.php. 104 | * 105 | * @return \Traversable 106 | */ 107 | public function getIterator() 108 | { 109 | return new \ArrayObject($this->internal()); 110 | } 111 | 112 | /** 113 | * Count elements of an object. 114 | * http://php.net/manual/en/countable.count.php. 115 | * 116 | * @return int 117 | */ 118 | public function count() 119 | { 120 | return count($this->internal()); 121 | } 122 | 123 | /** 124 | * For backward compatibility, 125 | * previously we use array to represent the raw query. 126 | * 127 | * @return array 128 | */ 129 | private function internal(): array 130 | { 131 | return [$this->op, $this->query]; 132 | } 133 | } 134 | --------------------------------------------------------------------------------