├── src
├── Parsers
│ ├── FilterExpression.php
│ ├── Placeholder.php
│ ├── ProjectionExpression.php
│ ├── KeyConditionExpression.php
│ ├── UpdateExpression.php
│ ├── ExpressionAttributeValues.php
│ ├── ExpressionAttributeNames.php
│ └── ConditionExpression.php
├── InvalidQuery.php
├── NotSupportedException.php
├── DynamoDbClientInterface.php
├── ModelTrait.php
├── Facades
│ └── DynamoDb.php
├── ConditionAnalyzer
│ ├── Index.php
│ └── Analyzer.php
├── EmptyAttributeFilter.php
├── DynamoDbCollection.php
├── H.php
├── DynamoDbServiceProvider.php
├── DynamoDb
│ ├── DynamoDbManager.php
│ ├── QueryBuilder.php
│ └── ExecutableQuery.php
├── ModelObserver.php
├── DynamoDbClientService.php
├── Concerns
│ └── HasParsers.php
├── ComparisonOperator.php
├── RawDynamoDbQuery.php
├── DynamoDbModel.php
└── DynamoDbQueryBuilder.php
├── .gitignore
├── .gitattributes
├── scripts
└── dev-setup.sh
├── .github
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .travis.yml
├── phpcs.xml
├── phpunit.xml
├── CONTRIBUTING.md
├── LICENSE
├── composer.json
├── config
└── dynamodb.php
├── CODE_OF_CONDUCT.md
└── README.md
/src/Parsers/FilterExpression.php:
--------------------------------------------------------------------------------
1 | reset();
15 | }
16 |
17 | public function next()
18 | {
19 | $this->counter += 1;
20 | return "a{$this->counter}";
21 | }
22 |
23 | public function reset()
24 | {
25 | $this->counter = 0;
26 |
27 | return $this;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 |
9 | A clear and concise description of what the bug is.
10 |
11 | **Schema**
12 |
13 | Describe your table schema:
14 | * Primary key / composite key
15 | * Any index?
16 |
17 | **Debug info**
18 |
19 | Show the query that you're having trouble with by copy-pasting the result of:
20 |
21 | ```php
22 | print_r($query->toDynamoDbQuery());
23 | ```
24 |
25 | **Version info**
26 |
27 | * Laravel: 5.5
28 | * laravel-dynamodb: latest
29 |
--------------------------------------------------------------------------------
/src/Parsers/ProjectionExpression.php:
--------------------------------------------------------------------------------
1 | names = $names;
12 | }
13 |
14 | /**
15 | * @param array $columns
16 | * @return string
17 | */
18 | public function parse(array $columns)
19 | {
20 | foreach ($columns as $column) {
21 | $this->names->set($column);
22 | }
23 |
24 | return join(', ', $this->names->placeholders());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: php
4 |
5 | stages:
6 | - linter
7 | - test
8 |
9 | php:
10 | - '7.2'
11 | - '7.3'
12 | - '7.4'
13 |
14 | before_script:
15 | - java -Djava.library.path=./DynamoDBLocal_lib -jar dynamodb_local/DynamoDBLocal.jar --port 3000 &
16 | - sleep 2
17 | - composer self-update
18 | - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer install --prefer-dist --no-interaction
19 |
20 | script: ./vendor/bin/phpunit
21 |
22 | jobs:
23 | include:
24 | - stage: linter
25 | php: 7.2
26 | before_script: source ./scripts/dev-setup.sh
27 | script: phpcs -s {src/*,tests/*}
28 |
29 |
--------------------------------------------------------------------------------
/src/ModelTrait.php:
--------------------------------------------------------------------------------
1 | getTable();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Parsers/KeyConditionExpression.php:
--------------------------------------------------------------------------------
1 | names = $names;
15 | }
16 |
17 | public function reset()
18 | {
19 | $this->names->reset();
20 | }
21 |
22 | public function remove(array $attributes)
23 | {
24 | foreach ($attributes as $attribute) {
25 | $this->names->set($attribute);
26 | }
27 |
28 | return 'REMOVE ' . implode(', ', $this->names->placeholders());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/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 |
11 |
12 |
13 | ./tests/
14 |
15 |
16 |
17 |
18 | ./src/
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Facades/DynamoDb.php:
--------------------------------------------------------------------------------
1 | * DynamoDb local version: 2016-01-07_1.0
10 | > * DynamoDb local schema for tests created by the [DynamoDb local shell](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.Shell.html) is located [here](dynamodb_local_schema.js)
11 |
12 | Run the following commands:
13 |
14 | ```bash
15 | $ java -Djava.library.path=./DynamoDBLocal_lib -jar dynamodb_local/DynamoDBLocal.jar --port 3000
16 | # In a separate tab
17 | $ ./vendor/bin/phpunit
18 | ```
19 |
20 | or
21 |
22 | ```bash
23 | composer --timeout=0 run dynamodb_local
24 | # In a separate tab
25 | composer run-script test
26 | ```
--------------------------------------------------------------------------------
/src/ConditionAnalyzer/Index.php:
--------------------------------------------------------------------------------
1 | name = $name;
25 | $this->hash = $hash;
26 | $this->range = $range;
27 | }
28 |
29 | public function isComposite()
30 | {
31 | return isset($this->hash) && isset($this->range);
32 | }
33 |
34 | public function columns()
35 | {
36 | $columns = [];
37 |
38 | if ($this->hash) {
39 | $columns[] = $this->hash;
40 | }
41 |
42 | if ($this->range) {
43 | $columns[] = $this->range;
44 | }
45 |
46 | return $columns;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/EmptyAttributeFilter.php:
--------------------------------------------------------------------------------
1 | &$value) {
18 | $value = is_string($value) ? trim($value) : $value;
19 | $empty = $value === null || (is_array($value) && empty($value));
20 |
21 | $empty = $empty || (is_scalar($value) && $value !== false && (string) $value === '');
22 |
23 | if ($empty) {
24 | $store[$key] = null;
25 | } else {
26 | if (is_object($value)) {
27 | $value = (array) $value;
28 | }
29 | if (is_array($value)) {
30 | $this->filter($value);
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Parsers/ExpressionAttributeValues.php:
--------------------------------------------------------------------------------
1 | reset();
20 | $this->prefix = $prefix;
21 | }
22 |
23 | public function set($placeholder, $value)
24 | {
25 | $this->mapping["{$this->prefix}{$placeholder}"] = $value;
26 | }
27 |
28 | public function get($placeholder)
29 | {
30 | return $this->mapping[$placeholder];
31 | }
32 |
33 | public function all()
34 | {
35 | return $this->mapping;
36 | }
37 |
38 | public function placeholders()
39 | {
40 | return array_keys($this->mapping);
41 | }
42 |
43 | public function reset()
44 | {
45 | $this->mapping = [];
46 |
47 | return $this;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/DynamoDbCollection.php:
--------------------------------------------------------------------------------
1 | conditionIndex = $conditionIndex;
20 | }
21 |
22 | public function lastKey()
23 | {
24 | $after = $this->last();
25 |
26 | if (empty($after)) {
27 | return null;
28 | }
29 |
30 | $afterKey = $after->getKeys();
31 |
32 | $attributes = $this->conditionIndex ? $this->conditionIndex->columns() : [];
33 |
34 | foreach ($attributes as $attribute) {
35 | $afterKey[$attribute] = $after->getAttribute($attribute);
36 | }
37 |
38 | return $afterKey;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Bao Pham
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/H.php:
--------------------------------------------------------------------------------
1 | $value) {
27 | if (call_user_func($callback, $value, $key)) {
28 | return $value;
29 | }
30 | }
31 | return static::value($default);
32 | }
33 |
34 | public static function value($value)
35 | {
36 | return $value instanceof \Closure ? $value() : $value;
37 | }
38 | }
39 | // phpcs:enable Squiz.Classes.ValidClassName.NotCamelCaps
40 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "baopham/dynamodb",
3 | "description": "Eloquent syntax for DynamoDB",
4 | "keywords": [
5 | "laravel",
6 | "dynamodb",
7 | "aws"
8 | ],
9 | "require": {
10 | "aws/aws-sdk-php": "^3.0.0",
11 | "illuminate/support": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
12 | "illuminate/database": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0"
13 | },
14 | "license": "MIT",
15 | "authors": [
16 | {
17 | "name": "Bao Pham",
18 | "email": "gbaopham@gmail.com"
19 | }
20 | ],
21 | "autoload": {
22 | "psr-4": {
23 | "BaoPham\\DynamoDb\\": "src/"
24 | }
25 | },
26 | "require-dev": {
27 | "orchestra/testbench": "~3.0 || ~5.0|^8.0"
28 | },
29 | "scripts": {
30 | "test": "phpunit",
31 | "dynamodb_local": "java -Djava.library.path=./DynamoDBLocal_lib -jar dynamodb_local/DynamoDBLocal.jar --port 3000"
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "BaoPham\\DynamoDb\\Tests\\": "tests/"
36 | }
37 | },
38 | "extra": {
39 | "laravel": {
40 | "providers": [
41 | "BaoPham\\DynamoDb\\DynamoDbServiceProvider"
42 | ]
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/DynamoDbServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->make(DynamoDbClientInterface::class));
19 |
20 | $this->publishes([
21 | __DIR__.'/../config/dynamodb.php' => app()->basePath('config/dynamodb.php'),
22 | ]);
23 | }
24 |
25 | /**
26 | * Register the service provider.
27 | */
28 | public function register()
29 | {
30 | $marshalerOptions = [
31 | 'nullify_invalid' => true,
32 | ];
33 |
34 | $this->app->singleton(DynamoDbClientInterface::class, function () use ($marshalerOptions) {
35 | $client = new DynamoDbClientService(new Marshaler($marshalerOptions), new EmptyAttributeFilter());
36 |
37 | return $client;
38 | });
39 |
40 | $this->app->singleton('dynamodb', function () {
41 | return new DynamoDbManager(app(DynamoDbClientInterface::class));
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Parsers/ExpressionAttributeNames.php:
--------------------------------------------------------------------------------
1 | reset();
25 | $this->prefix = $prefix;
26 | }
27 |
28 | public function set($name)
29 | {
30 | if ($this->isNested($name)) {
31 | $this->nested[] = $name;
32 | return;
33 | }
34 | $this->mapping["{$this->prefix}{$name}"] = $name;
35 | }
36 |
37 | public function get($placeholder)
38 | {
39 | return $this->mapping[$placeholder];
40 | }
41 |
42 | public function placeholder($name)
43 | {
44 | $placeholder = "{$this->prefix}{$name}";
45 | if (isset($this->mapping[$placeholder])) {
46 | return $placeholder;
47 | }
48 | return $name;
49 | }
50 |
51 | public function all()
52 | {
53 | return $this->mapping;
54 | }
55 |
56 | public function placeholders()
57 | {
58 | return array_merge(array_keys($this->mapping), $this->nested);
59 | }
60 |
61 | public function reset()
62 | {
63 | $this->mapping = [];
64 | $this->nested = [];
65 |
66 | return $this;
67 | }
68 |
69 | private function isNested($name)
70 | {
71 | return strpos($name, '.') !== false || (strpos($name, '[') !== false && strpos($name, ']') !== false);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/DynamoDb/DynamoDbManager.php:
--------------------------------------------------------------------------------
1 | service = $service;
27 | $this->marshaler = $service->getMarshaler();
28 | }
29 |
30 | public function marshalItem($item)
31 | {
32 | return $this->marshaler->marshalItem($item);
33 | }
34 |
35 | public function marshalValue($value)
36 | {
37 | return $this->marshaler->marshalValue($value);
38 | }
39 |
40 | public function unmarshalItem($item)
41 | {
42 | return $this->marshaler->unmarshalItem($item);
43 | }
44 |
45 | public function unmarshalValue($value)
46 | {
47 | return $this->marshaler->unmarshalValue($value);
48 | }
49 |
50 | /**
51 | * @param string|null $connection
52 | * @return \Aws\DynamoDb\DynamoDbClient
53 | */
54 | public function client($connection = null)
55 | {
56 | return $this->service->getClient($connection);
57 | }
58 |
59 | /**
60 | * @return QueryBuilder
61 | */
62 | public function newQuery()
63 | {
64 | return new QueryBuilder($this->service);
65 | }
66 |
67 | /**
68 | * @param string $table
69 | * @return QueryBuilder
70 | */
71 | public function table($table)
72 | {
73 | return $this->newQuery()->setTableName($table);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/ModelObserver.php:
--------------------------------------------------------------------------------
1 | dynamoDbClient = $dynamoDb->getClient();
31 | $this->marshaler = $dynamoDb->getMarshaler();
32 | $this->attributeFilter = $dynamoDb->getAttributeFilter();
33 | }
34 |
35 | private function saveToDynamoDb($model)
36 | {
37 | $attrs = $model->attributesToArray();
38 |
39 | try {
40 | $this->dynamoDbClient->putItem([
41 | 'TableName' => $model->getDynamoDbTableName(),
42 | 'Item' => $this->marshaler->marshalItem($attrs),
43 | ]);
44 | } catch (Exception $e) {
45 | Log::error($e);
46 | }
47 | }
48 |
49 | private function deleteFromDynamoDb($model)
50 | {
51 | $key = [$model->getKeyName() => $model->getKey()];
52 |
53 | try {
54 | $this->dynamoDbClient->deleteItem([
55 | 'TableName' => $model->getDynamoDbTableName(),
56 | 'Key' => $this->marshaler->marshalItem($key),
57 | ]);
58 | } catch (Exception $e) {
59 | Log::error($e);
60 | }
61 | }
62 |
63 | public function created($model)
64 | {
65 | $this->saveToDynamoDb($model);
66 | }
67 |
68 | public function updated($model)
69 | {
70 | $this->saveToDynamoDb($model);
71 | }
72 |
73 | public function deleted($model)
74 | {
75 | $this->deleteFromDynamoDb($model);
76 | }
77 |
78 | public function restored($model)
79 | {
80 | $this->saveToDynamoDb($model);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/DynamoDbClientService.php:
--------------------------------------------------------------------------------
1 | marshaler = $marshaler;
30 | $this->attributeFilter = $filter;
31 | $this->clients = [];
32 | }
33 |
34 | /**
35 | * @return \Aws\DynamoDb\DynamoDbClient
36 | */
37 | public function getClient($connection = null)
38 | {
39 | $connection = $connection ?: config('dynamodb.default');
40 |
41 | if (isset($this->clients[$connection])) {
42 | return $this->clients[$connection];
43 | }
44 |
45 | $config = config("dynamodb.connections.$connection", []);
46 | $config['version'] = '2012-08-10';
47 | $config['debug'] = $this->getDebugOptions(Arr::get($config, 'debug'));
48 |
49 | $client = new DynamoDbClient($config);
50 |
51 | $this->clients[$connection] = $client;
52 |
53 | return $client;
54 | }
55 |
56 | /**
57 | * @return \Aws\DynamoDb\Marshaler
58 | */
59 | public function getMarshaler()
60 | {
61 | return $this->marshaler;
62 | }
63 |
64 | /**
65 | * @return \BaoPham\DynamoDb\EmptyAttributeFilter
66 | */
67 | public function getAttributeFilter()
68 | {
69 | return $this->attributeFilter;
70 | }
71 |
72 | protected function getDebugOptions($debug = false)
73 | {
74 | if ($debug === true) {
75 | $logfn = function ($msg) {
76 | Log::info($msg);
77 | };
78 |
79 | return ['logfn' => $logfn];
80 | }
81 |
82 | return $debug;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Concerns/HasParsers.php:
--------------------------------------------------------------------------------
1 | placeholder = new Placeholder();
53 |
54 | $this->expressionAttributeNames = new ExpressionAttributeNames();
55 |
56 | $this->expressionAttributeValues = new ExpressionAttributeValues();
57 |
58 | $this->keyConditionExpression = new KeyConditionExpression(
59 | $this->placeholder,
60 | $this->expressionAttributeValues,
61 | $this->expressionAttributeNames
62 | );
63 |
64 | $this->filterExpression = new FilterExpression(
65 | $this->placeholder,
66 | $this->expressionAttributeValues,
67 | $this->expressionAttributeNames
68 | );
69 |
70 | $this->projectionExpression = new ProjectionExpression($this->expressionAttributeNames);
71 |
72 | $this->updateExpression = new UpdateExpression($this->expressionAttributeNames);
73 | }
74 |
75 | public function resetExpressions()
76 | {
77 | $this->filterExpression->reset();
78 | $this->keyConditionExpression->reset();
79 | $this->updateExpression->reset();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/config/dynamodb.php:
--------------------------------------------------------------------------------
1 | env('DYNAMODB_CONNECTION', 'aws'),
15 |
16 | /*
17 | |--------------------------------------------------------------------------
18 | | DynamoDb Connections
19 | |--------------------------------------------------------------------------
20 | |
21 | | Here are each of the DynamoDb connections setup for your application.
22 | |
23 | | Most of the connection's config will be fed directly to AwsClient
24 | | constructor http://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.AwsClient.html#___construct
25 | */
26 |
27 | 'connections' => [
28 | 'aws' => [
29 | 'credentials' => [
30 | 'key' => env('DYNAMODB_KEY'),
31 | 'secret' => env('DYNAMODB_SECRET'),
32 | // If using as an assumed IAM role, you can also use the `token` parameter
33 | 'token' => env('AWS_SESSION_TOKEN'),
34 | ],
35 | 'region' => env('DYNAMODB_REGION'),
36 | // if true, it will use Laravel Log.
37 | // For advanced options, see http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/configuration.html
38 | 'debug' => env('DYNAMODB_DEBUG'),
39 | ],
40 | 'aws_iam_role' => [
41 | 'region' => env('DYNAMODB_REGION'),
42 | 'debug' => env('DYNAMODB_DEBUG'),
43 | ],
44 | 'local' => [
45 | 'credentials' => [
46 | 'key' => 'dynamodblocal',
47 | 'secret' => 'secret',
48 | ],
49 | 'region' => 'stub',
50 | // see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html
51 | 'endpoint' => env('DYNAMODB_LOCAL_ENDPOINT'),
52 | 'debug' => true,
53 | ],
54 | 'test' => [
55 | 'credentials' => [
56 | 'key' => 'dynamodblocal',
57 | 'secret' => 'secret',
58 | ],
59 | 'region' => 'test',
60 | 'endpoint' => env('DYNAMODB_LOCAL_ENDPOINT'),
61 | 'debug' => true,
62 | ],
63 | ],
64 | ];
65 |
--------------------------------------------------------------------------------
/src/ComparisonOperator.php:
--------------------------------------------------------------------------------
1 | static::EQ,
28 | '>' => static::GT,
29 | '>=' => static::GE,
30 | '<' => static::LT,
31 | '<=' => static::LE,
32 | 'in' => static::IN,
33 | '!=' => static::NE,
34 | 'begins_with' => static::BEGINS_WITH,
35 | 'between' => static::BETWEEN,
36 | 'not_contains' => static::NOT_CONTAINS,
37 | 'contains' => static::CONTAINS,
38 | 'null' => static::NULL,
39 | 'not_null' => static::NOT_NULL,
40 | ];
41 | }
42 |
43 | public static function getSupportedOperators()
44 | {
45 | return array_keys(static::getOperatorMapping());
46 | }
47 |
48 | public static function isValidOperator($operator)
49 | {
50 | $operator = strtolower($operator);
51 |
52 | $mapping = static::getOperatorMapping();
53 |
54 | return isset($mapping[$operator]);
55 | }
56 |
57 | public static function getDynamoDbOperator($operator)
58 | {
59 | $mapping = static::getOperatorMapping();
60 |
61 | $operator = strtolower($operator);
62 |
63 | return $mapping[$operator];
64 | }
65 |
66 | public static function getQuerySupportedOperators($isRangeKey = false)
67 | {
68 | if ($isRangeKey) {
69 | return [
70 | static::EQ,
71 | static::LE,
72 | static::LT,
73 | static::GE,
74 | static::GT,
75 | static::BEGINS_WITH,
76 | static::BETWEEN,
77 | ];
78 | }
79 |
80 | return [static::EQ];
81 | }
82 |
83 | public static function isValidQueryOperator($operator, $isRangeKey = false)
84 | {
85 | $dynamoDbOperator = static::getDynamoDbOperator($operator);
86 |
87 | return static::isValidQueryDynamoDbOperator($dynamoDbOperator, $isRangeKey);
88 | }
89 |
90 | public static function isValidQueryDynamoDbOperator($dynamoDbOperator, $isRangeKey = false)
91 | {
92 | return in_array($dynamoDbOperator, static::getQuerySupportedOperators($isRangeKey));
93 | }
94 |
95 | public static function is($op, $dynamoDbOperator)
96 | {
97 | $mapping = static::getOperatorMapping();
98 | return $mapping[strtolower($op)] === $dynamoDbOperator;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at gbaopham@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/src/DynamoDb/QueryBuilder.php:
--------------------------------------------------------------------------------
1 | `, where ``
17 | * is the key name of the query body to be sent.
18 | *
19 | * For example, to build a query:
20 | * [
21 | * 'AttributeDefinitions' => ...,
22 | * 'GlobalSecondaryIndexUpdates' => ...
23 | * 'TableName' => ...
24 | * ]
25 | *
26 | * Do:
27 | *
28 | * $query = $query->setAttributeDefinitions(...)->setGlobalSecondaryIndexUpdates(...)->setTableName(...);
29 | *
30 | * When ready:
31 | *
32 | * $query->prepare()->updateTable();
33 | *
34 | * Common methods:
35 | *
36 | * @method QueryBuilder setExpressionAttributeNames(array $mapping)
37 | * @method QueryBuilder setExpressionAttributeValues(array $mapping)
38 | * @method QueryBuilder setFilterExpression(string $expression)
39 | * @method QueryBuilder setKeyConditionExpression(string $expression)
40 | * @method QueryBuilder setProjectionExpression(string $expression)
41 | * @method QueryBuilder setUpdateExpression(string $expression)
42 | * @method QueryBuilder setAttributeUpdates(array $updates)
43 | * @method QueryBuilder setConsistentRead(bool $consistent)
44 | * @method QueryBuilder setScanIndexForward(bool $forward)
45 | * @method QueryBuilder setExclusiveStartKey(mixed $key)
46 | * @method QueryBuilder setReturnValues(string $type)
47 | * @method QueryBuilder setRequestItems(array $items)
48 | * @method QueryBuilder setTableName(string $table)
49 | * @method QueryBuilder setIndexName(string $index)
50 | * @method QueryBuilder setSelect(string $select)
51 | * @method QueryBuilder setItem(array $item)
52 | * @method QueryBuilder setKeys(array $keys)
53 | * @method QueryBuilder setLimit(int $limit)
54 | * @method QueryBuilder setKey(array $key)
55 | */
56 | class QueryBuilder
57 | {
58 | /**
59 | * @var DynamoDbClientInterface
60 | */
61 | private $service;
62 |
63 | /**
64 | * Query body to be sent to AWS
65 | *
66 | * @var array
67 | */
68 | public $query = [];
69 |
70 | public function __construct(DynamoDbClientInterface $service)
71 | {
72 | $this->service = $service;
73 | }
74 |
75 | public function hydrate(array $query)
76 | {
77 | $this->query = $query;
78 |
79 | return $this;
80 | }
81 |
82 | public function setExpressionAttributeName($placeholder, $name)
83 | {
84 | $this->query['ExpressionAttributeNames'][$placeholder] = $name;
85 |
86 | return $this;
87 | }
88 |
89 | public function setExpressionAttributeValue($placeholder, $value)
90 | {
91 | $this->query['ExpressionAttributeValues'][$placeholder] = $value;
92 |
93 | return $this;
94 | }
95 |
96 | /**
97 | * @param DynamoDbClient|null $client
98 | * @return ExecutableQuery
99 | */
100 | public function prepare(DynamoDbClient $client = null)
101 | {
102 | $raw = new RawDynamoDbQuery(null, $this->query);
103 | return new ExecutableQuery($client ?: $this->service->getClient(), $raw->finalize()->query);
104 | }
105 |
106 | /**
107 | * @param string $method
108 | * @param array $parameters
109 | * @return mixed
110 | */
111 | public function __call($method, $parameters)
112 | {
113 | if (Str::startsWith($method, 'set')) {
114 | $key = array_reverse(explode('set', $method, 2))[0];
115 | $this->query[$key] = current($parameters);
116 |
117 | return $this;
118 | }
119 |
120 | throw new BadMethodCallException(sprintf(
121 | 'Method %s::%s does not exist.',
122 | static::class,
123 | $method
124 | ));
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/RawDynamoDbQuery.php:
--------------------------------------------------------------------------------
1 | op = $op;
29 | $this->query = $query;
30 | }
31 |
32 | /**
33 | * Perform any final clean up.
34 | * Remove any empty values to avoid errors.
35 | *
36 | * @return $this
37 | */
38 | public function finalize()
39 | {
40 | $this->query = array_filter($this->query, function ($value) {
41 | return !empty($value) || is_bool($value) || is_numeric($value);
42 | });
43 |
44 | return $this;
45 | }
46 |
47 | /**
48 | * Whether a offset exists
49 | * @link http://php.net/manual/en/arrayaccess.offsetexists.php
50 | * @param mixed $offset
51 | * An offset to check for.
52 | *
53 | * @return boolean true on success or false on failure.
54 | *
55 | *
56 | * The return value will be casted to boolean if non-boolean was returned.
57 | * @since 5.0.0
58 | */
59 | #[\ReturnTypeWillChange]
60 | public function offsetExists($offset)
61 | {
62 | return isset($this->internal()[$offset]);
63 | }
64 |
65 | /**
66 | * Offset to retrieve
67 | * @link http://php.net/manual/en/arrayaccess.offsetget.php
68 | * @param mixed $offset
69 | * The offset to retrieve.
70 | *
71 | * @return mixed Can return all value types.
72 | * @since 5.0.0
73 | */
74 | #[\ReturnTypeWillChange]
75 | public function offsetGet($offset)
76 | {
77 | return $this->internal()[$offset];
78 | }
79 |
80 | /**
81 | * Offset to set
82 | * @link http://php.net/manual/en/arrayaccess.offsetset.php
83 | * @param mixed $offset
84 | * The offset to assign the value to.
85 | *
86 | * @param mixed $value
87 | * The value to set.
88 | *
89 | * @return void
90 | * @since 5.0.0
91 | */
92 | #[\ReturnTypeWillChange]
93 | public function offsetSet($offset, $value)
94 | {
95 | $this->internal()[$offset] = $value;
96 | }
97 |
98 | /**
99 | * Offset to unset
100 | * @link http://php.net/manual/en/arrayaccess.offsetunset.php
101 | * @param mixed $offset
102 | * The offset to unset.
103 | *
104 | * @return void
105 | * @since 5.0.0
106 | */
107 | #[\ReturnTypeWillChange]
108 | public function offsetUnset($offset)
109 | {
110 | unset($this->internal()[$offset]);
111 | }
112 |
113 | /**
114 | * Retrieve an external iterator
115 | * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
116 | * @return \Traversable An instance of an object implementing Iterator or
117 | * Traversable
118 | * @since 5.0.0
119 | */
120 | #[\ReturnTypeWillChange]
121 | public function getIterator()
122 | {
123 | return new \ArrayObject($this->internal());
124 | }
125 |
126 | /**
127 | * Count elements of an object
128 | * @link http://php.net/manual/en/countable.count.php
129 | * @return int The custom count as an integer.
130 | *
131 | *
132 | * The return value is cast to an integer.
133 | * @since 5.1.0
134 | */
135 | #[\ReturnTypeWillChange]
136 | public function count()
137 | {
138 | return count($this->internal());
139 | }
140 |
141 | /**
142 | * For backward compatibility, previously we use array to represent the raw query
143 | *
144 | * @var array
145 | *
146 | * @return array
147 | */
148 | private function internal()
149 | {
150 | return [$this->op, $this->query];
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/DynamoDb/ExecutableQuery.php:
--------------------------------------------------------------------------------
1 | client = $client;
86 | $this->query = $query;
87 | }
88 |
89 | /**
90 | * @param string $method
91 | * @param array $parameters
92 | * @return mixed
93 | */
94 | public function __call($method, $parameters)
95 | {
96 | return $this->client->{$method}($this->query);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Parsers/ConditionExpression.php:
--------------------------------------------------------------------------------
1 | '%s = :%s',
14 | ComparisonOperator::LE => '%s <= :%s',
15 | ComparisonOperator::LT => '%s < :%s',
16 | ComparisonOperator::GE => '%s >= :%s',
17 | ComparisonOperator::GT => '%s > :%s',
18 | ComparisonOperator::BEGINS_WITH => 'begins_with(%s, :%s)',
19 | ComparisonOperator::BETWEEN => '(%s BETWEEN :%s AND :%s)',
20 | ComparisonOperator::CONTAINS => 'contains(%s, :%s)',
21 | ComparisonOperator::NOT_CONTAINS => 'NOT contains(%s, :%s)',
22 | ComparisonOperator::NULL => 'attribute_not_exists(%s)',
23 | ComparisonOperator::NOT_NULL => 'attribute_exists(%s)',
24 | ComparisonOperator::NE => '%s <> :%s',
25 | ComparisonOperator::IN => '%s IN (%s)',
26 | ];
27 |
28 | /**
29 | * @var ExpressionAttributeValues
30 | */
31 | protected $values;
32 |
33 | /**
34 | * @var ExpressionAttributeNames
35 | */
36 | protected $names;
37 |
38 | /**
39 | * @var Placeholder
40 | */
41 | protected $placeholder;
42 |
43 | public function __construct(
44 | Placeholder $placeholder,
45 | ExpressionAttributeValues $values,
46 | ExpressionAttributeNames $names
47 | ) {
48 | $this->placeholder = $placeholder;
49 | $this->values = $values;
50 | $this->names = $names;
51 | }
52 |
53 | /**
54 | * @param array $where
55 | * [
56 | * 'column' => 'name',
57 | * 'type' => 'EQ',
58 | * 'value' => 'foo',
59 | * 'boolean' => 'and',
60 | * ]
61 | *
62 | * @return string
63 | * @throws NotSupportedException
64 | */
65 | public function parse($where)
66 | {
67 | if (empty($where)) {
68 | return '';
69 | }
70 |
71 | $parsed = [];
72 |
73 | foreach ($where as $condition) {
74 | $boolean = Arr::get($condition, 'boolean');
75 | $value = Arr::get($condition, 'value');
76 | $type = Arr::get($condition, 'type');
77 |
78 | $prefix = '';
79 |
80 | if (count($parsed) > 0) {
81 | $prefix = strtoupper($boolean) . ' ';
82 | }
83 |
84 | if ($type === 'Nested') {
85 | $parsed[] = $prefix . $this->parseNestedCondition($value);
86 | continue;
87 | }
88 |
89 | $parsed[] = $prefix . $this->parseCondition(
90 | Arr::get($condition, 'column'),
91 | $type,
92 | $value
93 | );
94 | }
95 |
96 | return implode(' ', $parsed);
97 | }
98 |
99 | public function reset()
100 | {
101 | $this->placeholder->reset();
102 | $this->names->reset();
103 | $this->values->reset();
104 | }
105 |
106 | protected function getSupportedOperators()
107 | {
108 | return static::OPERATORS;
109 | }
110 |
111 | protected function parseNestedCondition(array $conditions)
112 | {
113 | return '(' . $this->parse($conditions) . ')';
114 | }
115 |
116 | protected function parseCondition($name, $operator, $value)
117 | {
118 | $operators = $this->getSupportedOperators();
119 |
120 | if (empty($operators[$operator])) {
121 | throw new NotSupportedException("$operator is not supported");
122 | }
123 |
124 | $template = $operators[$operator];
125 |
126 | $this->names->set($name);
127 |
128 | if ($operator === ComparisonOperator::BETWEEN) {
129 | return $this->parseBetweenCondition($name, $value, $template);
130 | }
131 |
132 | if ($operator === ComparisonOperator::IN) {
133 | return $this->parseInCondition($name, $value, $template);
134 | }
135 |
136 | if ($operator === ComparisonOperator::NULL || $operator === ComparisonOperator::NOT_NULL) {
137 | return $this->parseNullCondition($name, $template);
138 | }
139 |
140 | $placeholder = $this->placeholder->next();
141 |
142 | $this->values->set($placeholder, DynamoDb::marshalValue($value));
143 |
144 | return sprintf($template, $this->names->placeholder($name), $placeholder);
145 | }
146 |
147 | protected function parseBetweenCondition($name, $value, $template)
148 | {
149 | $first = $this->placeholder->next();
150 |
151 | $second = $this->placeholder->next();
152 |
153 | $this->values->set($first, DynamoDb::marshalValue($value[0]));
154 |
155 | $this->values->set($second, DynamoDb::marshalValue($value[1]));
156 |
157 | return sprintf($template, $this->names->placeholder($name), $first, $second);
158 | }
159 |
160 | protected function parseInCondition($name, $value, $template)
161 | {
162 | $valuePlaceholders = [];
163 |
164 | foreach ($value as $item) {
165 | $placeholder = $this->placeholder->next();
166 |
167 | $valuePlaceholders[] = ":" . $placeholder;
168 |
169 | $this->values->set($placeholder, DynamoDb::marshalValue($item));
170 | }
171 |
172 | return sprintf($template, $this->names->placeholder($name), implode(', ', $valuePlaceholders));
173 | }
174 |
175 | protected function parseNullCondition($name, $template)
176 | {
177 | return sprintf($template, $this->names->placeholder($name));
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/ConditionAnalyzer/Analyzer.php:
--------------------------------------------------------------------------------
1 | on($model)
18 | * ->withIndex($index)
19 | * ->analyze($conditions);
20 | *
21 | * $analyzer->isExactSearch();
22 | * $analyzer->keyConditions();
23 | * $analyzer->filterConditions();
24 | * $analyzer->index();
25 | */
26 | class Analyzer
27 | {
28 | /**
29 | * @var DynamoDbModel
30 | */
31 | private $model;
32 |
33 | /**
34 | * @var array
35 | */
36 | private $conditions = [];
37 |
38 | /**
39 | * @var string
40 | */
41 | private $indexName;
42 |
43 | public function on(DynamoDbModel $model)
44 | {
45 | $this->model = $model;
46 |
47 | return $this;
48 | }
49 |
50 | public function withIndex($index)
51 | {
52 | $this->indexName = $index;
53 |
54 | return $this;
55 | }
56 |
57 | public function analyze($conditions)
58 | {
59 | $this->conditions = $conditions;
60 |
61 | return $this;
62 | }
63 |
64 | public function isExactSearch()
65 | {
66 | if (empty($this->conditions)) {
67 | return false;
68 | }
69 |
70 | if (empty($this->identifierConditions())) {
71 | return false;
72 | }
73 |
74 | if (count($this->conditions) !== count($this->model->getKeyNames())) {
75 | return false;
76 | }
77 |
78 | foreach ($this->conditions as $condition) {
79 | if (Arr::get($condition, 'type') !== ComparisonOperator::EQ) {
80 | return false;
81 | }
82 |
83 | if (array_search(Arr::get($condition, 'column'), $this->model->getKeyNames()) === false) {
84 | return false;
85 | }
86 | }
87 |
88 | return true;
89 | }
90 |
91 | /**
92 | * @return Index|null
93 | */
94 | public function index()
95 | {
96 | return $this->getIndex();
97 | }
98 |
99 | public function keyConditions()
100 | {
101 | $index = $this->getIndex();
102 |
103 | if ($index) {
104 | return $this->getConditions($index->columns());
105 | }
106 |
107 | return $this->identifierConditions();
108 | }
109 |
110 | public function filterConditions()
111 | {
112 | $keyConditions = $this->keyConditions() ?: [];
113 |
114 | return array_filter($this->conditions, function ($condition) use ($keyConditions) {
115 | return array_search($condition, $keyConditions) === false;
116 | });
117 | }
118 |
119 | public function identifierConditions()
120 | {
121 | $keyNames = $this->model->getKeyNames();
122 |
123 | $conditions = $this->getConditions($keyNames);
124 |
125 | if (!$this->hasValidQueryOperator(...$keyNames)) {
126 | return null;
127 | }
128 |
129 | return $conditions;
130 | }
131 |
132 | public function identifierConditionValues()
133 | {
134 | $idConditions = $this->identifierConditions();
135 |
136 | if (!$idConditions) {
137 | return [];
138 | }
139 |
140 | $values = [];
141 |
142 | foreach ($idConditions as $condition) {
143 | $values[$condition['column']] = $condition['value'];
144 | }
145 |
146 | return $values;
147 | }
148 |
149 | /**
150 | * @param $column
151 | *
152 | * @return array
153 | */
154 | private function getCondition($column)
155 | {
156 | return H::array_first($this->conditions, function ($condition) use ($column) {
157 | return $condition['column'] === $column;
158 | });
159 | }
160 |
161 | /**
162 | * @param $columns
163 | *
164 | * @return array
165 | */
166 | private function getConditions($columns)
167 | {
168 | return array_filter($this->conditions, function ($condition) use ($columns) {
169 | return in_array($condition['column'], $columns);
170 | });
171 | }
172 |
173 | /**
174 | * @return Index|null
175 | */
176 | private function getIndex()
177 | {
178 | if (empty($this->conditions)) {
179 | return null;
180 | }
181 |
182 | $index = null;
183 |
184 | foreach ($this->model->getDynamoDbIndexKeys() as $name => $keysInfo) {
185 | $conditionKeys = Arr::pluck($this->conditions, 'column');
186 | $keys = array_values($keysInfo);
187 |
188 | if (count(array_intersect($conditionKeys, $keys)) === count($keys)) {
189 | if (!isset($this->indexName) || $this->indexName === $name) {
190 | $index = new Index(
191 | $name,
192 | Arr::get($keysInfo, 'hash'),
193 | Arr::get($keysInfo, 'range')
194 | );
195 |
196 | break;
197 | }
198 | }
199 | }
200 |
201 | if ($index && !$this->hasValidQueryOperator($index->hash, $index->range)) {
202 | $index = null;
203 | }
204 |
205 | return $index;
206 | }
207 |
208 | private function hasValidQueryOperator($hash, $range = null)
209 | {
210 | $hashConditionType = $this->getCondition($hash)['type'] ?? null;
211 | $validQueryOp = ComparisonOperator::isValidQueryDynamoDbOperator($hashConditionType);
212 |
213 | if ($validQueryOp && $range && $this->getCondition($range) !== null) {
214 | $rangeConditionType = $this->getCondition($range)['type'];
215 | $validQueryOp = ComparisonOperator::isValidQueryDynamoDbOperator(
216 | $rangeConditionType,
217 | true
218 | );
219 | }
220 |
221 | return $validQueryOp;
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/DynamoDbModel.php:
--------------------------------------------------------------------------------
1 | ' => [
43 | * 'hash' => ''
44 | * ],
45 | * '' => [
46 | * 'hash' => '',
47 | * 'range' => ''
48 | * ],
49 | * ]
50 | *
51 | * @var array
52 | */
53 | protected $dynamoDbIndexKeys = [];
54 |
55 | /**
56 | * Array of your composite key.
57 | * ['', '']
58 | *
59 | * @var array
60 | */
61 | protected $compositeKey = [];
62 |
63 | /**
64 | * Default Date format
65 | * ISO 8601 Compliant
66 | */
67 | protected $dateFormat = DateTime::ATOM;
68 |
69 |
70 | public function __construct(array $attributes = [])
71 | {
72 | $this->bootIfNotBooted();
73 |
74 | $this->syncOriginal();
75 |
76 | $this->fill($attributes);
77 |
78 | $this->setupDynamoDb();
79 | }
80 |
81 | /**
82 | * Get the DynamoDbClient service that is being used by the models.
83 | *
84 | * @return DynamoDbClientInterface
85 | */
86 | public static function getDynamoDbClientService()
87 | {
88 | return static::$dynamoDb;
89 | }
90 |
91 | /**
92 | * Set the DynamoDbClient used by models.
93 | *
94 | * @param DynamoDbClientInterface $dynamoDb
95 | *
96 | * @return void
97 | */
98 | public static function setDynamoDbClientService(DynamoDbClientInterface $dynamoDb)
99 | {
100 | static::$dynamoDb = $dynamoDb;
101 | }
102 |
103 | /**
104 | * Unset the DynamoDbClient service for models.
105 | *
106 | * @return void
107 | */
108 | public static function unsetDynamoDbClientService()
109 | {
110 | static::$dynamoDb = null;
111 | }
112 |
113 | protected function setupDynamoDb()
114 | {
115 | $this->marshaler = static::$dynamoDb->getMarshaler();
116 | $this->attributeFilter = static::$dynamoDb->getAttributeFilter();
117 | }
118 |
119 | public function newCollection(array $models = [], $index = null)
120 | {
121 | return new DynamoDbCollection($models, $index);
122 | }
123 |
124 | public function save(array $options = [])
125 | {
126 | $create = !$this->exists;
127 |
128 | if ($this->fireModelEvent('saving') === false) {
129 | return false;
130 | }
131 |
132 | if ($create && $this->fireModelEvent('creating') === false) {
133 | return false;
134 | }
135 |
136 | if (!$create && $this->fireModelEvent('updating') === false) {
137 | return false;
138 | }
139 |
140 | if ($this->usesTimestamps()) {
141 | $this->updateTimestamps();
142 | }
143 |
144 | $saved = $this->newQuery()->save();
145 |
146 | if (!$saved) {
147 | return $saved;
148 | }
149 |
150 | $this->exists = true;
151 | $this->wasRecentlyCreated = $create;
152 | $this->fireModelEvent($create ? 'created' : 'updated', false);
153 |
154 | $this->finishSave($options);
155 |
156 | return $saved;
157 | }
158 |
159 | /**
160 | * Saves the model to DynamoDb asynchronously and returns a promise
161 | * @param array $options
162 | * @return bool|\GuzzleHttp\Promise\Promise
163 | */
164 | public function saveAsync(array $options = [])
165 | {
166 | $create = !$this->exists;
167 |
168 | if ($this->fireModelEvent('saving') === false) {
169 | return false;
170 | }
171 |
172 | if ($create && $this->fireModelEvent('creating') === false) {
173 | return false;
174 | }
175 |
176 | if (!$create && $this->fireModelEvent('updating') === false) {
177 | return false;
178 | }
179 |
180 | if ($this->usesTimestamps()) {
181 | $this->updateTimestamps();
182 | }
183 |
184 | $savePromise = $this->newQuery()->saveAsync();
185 |
186 | $savePromise->then(function ($result) use ($create, $options) {
187 | if (Arr::get($result, '@metadata.statusCode') === 200) {
188 | $this->exists = true;
189 | $this->wasRecentlyCreated = $create;
190 | $this->fireModelEvent($create ? 'created' : 'updated', false);
191 |
192 | $this->finishSave($options);
193 | }
194 | });
195 |
196 | return $savePromise;
197 | }
198 |
199 | public function update(array $attributes = [], array $options = [])
200 | {
201 | return $this->fill($attributes)->save();
202 | }
203 |
204 | public function updateAsync(array $attributes = [], array $options = [])
205 | {
206 | return $this->fill($attributes)->saveAsync($options);
207 | }
208 |
209 | public static function create(array $attributes = [])
210 | {
211 | $model = new static;
212 |
213 | $model->fill($attributes)->save();
214 |
215 | return $model;
216 | }
217 |
218 | public function delete()
219 | {
220 | if (is_null($this->getKeyName())) {
221 | throw new Exception('No primary key defined on model.');
222 | }
223 |
224 | if ($this->exists) {
225 | if ($this->fireModelEvent('deleting') === false) {
226 | return false;
227 | }
228 |
229 | $this->exists = false;
230 |
231 | $success = $this->newQuery()->delete();
232 |
233 | if ($success) {
234 | $this->fireModelEvent('deleted', false);
235 | }
236 |
237 | return $success;
238 | }
239 | }
240 |
241 | public function deleteAsync()
242 | {
243 | if (is_null($this->getKeyName())) {
244 | throw new Exception('No primary key defined on model.');
245 | }
246 |
247 | if ($this->exists) {
248 | if ($this->fireModelEvent('deleting') === false) {
249 | return false;
250 | }
251 |
252 | $this->exists = false;
253 |
254 | $deletePromise = $this->newQuery()->deleteAsync();
255 |
256 | $deletePromise->then(function () {
257 | $this->fireModelEvent('deleted', false);
258 | });
259 |
260 | return $deletePromise;
261 | }
262 | }
263 |
264 | public static function all($columns = [])
265 | {
266 | $instance = new static;
267 |
268 | return $instance->newQuery()->get($columns);
269 | }
270 |
271 | public function refresh()
272 | {
273 | if (! $this->exists) {
274 | return $this;
275 | }
276 |
277 | $query = $this->newQuery();
278 |
279 | $refreshed = $query->find($this->getKeys());
280 |
281 | $this->setRawAttributes($refreshed->toArray());
282 |
283 | return $this;
284 | }
285 |
286 | /**
287 | * @return DynamoDbQueryBuilder
288 | */
289 | public function newQuery()
290 | {
291 | $builder = new DynamoDbQueryBuilder($this);
292 |
293 | foreach ($this->getGlobalScopes() as $identifier => $scope) {
294 | $builder->withGlobalScope($identifier, $scope);
295 | }
296 |
297 | return $builder;
298 | }
299 |
300 | public function hasCompositeKey()
301 | {
302 | return !empty($this->compositeKey);
303 | }
304 |
305 | /**
306 | * @deprecated
307 | * @param $item
308 | * @return array
309 | */
310 | public function marshalItem($item)
311 | {
312 | return $this->marshaler->marshalItem($item);
313 | }
314 |
315 | /**
316 | * @deprecated
317 | * @param $value
318 | * @return array
319 | */
320 | public function marshalValue($value)
321 | {
322 | return $this->marshaler->marshalValue($value);
323 | }
324 |
325 | /**
326 | * @deprecated
327 | * @param $item
328 | * @return array|\stdClass
329 | */
330 | public function unmarshalItem($item)
331 | {
332 | return $this->marshaler->unmarshalItem($item);
333 | }
334 |
335 | public function setId($id)
336 | {
337 | if (!is_array($id)) {
338 | $this->setAttribute($this->getKeyName(), $id);
339 |
340 | return $this;
341 | }
342 |
343 | foreach ($id as $keyName => $value) {
344 | $this->setAttribute($keyName, $value);
345 | }
346 |
347 | return $this;
348 | }
349 |
350 | /**
351 | * @return \Aws\DynamoDb\DynamoDbClient
352 | */
353 | public function getClient()
354 | {
355 | return static::$dynamoDb->getClient($this->getConnectionName());
356 | }
357 |
358 | /**
359 | * Get the value of the model's primary key.
360 | *
361 | * @return mixed
362 | */
363 | public function getKey()
364 | {
365 | return $this->getAttribute($this->getKeyName());
366 | }
367 |
368 | /**
369 | * Get the value of the model's primary / composite key.
370 | * Use this if you always want the key values in associative array form.
371 | *
372 | * @return array
373 | *
374 | * ['id' => 'foo']
375 | *
376 | * or
377 | *
378 | * ['id' => 'foo', 'id2' => 'bar']
379 | */
380 | public function getKeys()
381 | {
382 | if ($this->hasCompositeKey()) {
383 | $key = [];
384 |
385 | foreach ($this->compositeKey as $name) {
386 | $key[$name] = $this->getAttribute($name);
387 | }
388 |
389 | return $key;
390 | }
391 |
392 | $name = $this->getKeyName();
393 |
394 | return [$name => $this->getAttribute($name)];
395 | }
396 |
397 | /**
398 | * Get the primary key for the model.
399 | *
400 | * @return string
401 | */
402 | public function getKeyName()
403 | {
404 | return $this->primaryKey;
405 | }
406 |
407 | /**
408 | * Get the primary/composite key for the model.
409 | *
410 | * @return array
411 | */
412 | public function getKeyNames()
413 | {
414 | return $this->hasCompositeKey() ? $this->compositeKey : [$this->primaryKey];
415 | }
416 |
417 | /**
418 | * @return array
419 | */
420 | public function getDynamoDbIndexKeys()
421 | {
422 | return $this->dynamoDbIndexKeys;
423 | }
424 |
425 | /**
426 | * @param array $dynamoDbIndexKeys
427 | */
428 | public function setDynamoDbIndexKeys($dynamoDbIndexKeys)
429 | {
430 | $this->dynamoDbIndexKeys = $dynamoDbIndexKeys;
431 | }
432 |
433 | /**
434 | * @deprecated
435 | * @return \Aws\DynamoDb\Marshaler
436 | */
437 | public function getMarshaler()
438 | {
439 | return $this->marshaler;
440 | }
441 |
442 | /**
443 | * Remove non-serializable properties when serializing.
444 | *
445 | * @return array
446 | */
447 | public function __sleep()
448 | {
449 | return array_keys(
450 | Arr::except(get_object_vars($this), ['marshaler', 'attributeFilter'])
451 | );
452 | }
453 |
454 | /**
455 | * When a model is being unserialized, check if it needs to be booted and setup DynamoDB.
456 | *
457 | * @return void
458 | */
459 | public function __wakeup()
460 | {
461 | parent::__wakeup();
462 | $this->setupDynamoDb();
463 | }
464 | }
465 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | laravel-dynamodb
2 | ================
3 |
4 | [](https://packagist.org/packages/baopham/dynamodb)
5 | [](https://packagist.org/packages/baopham/dynamodb)
6 | [](https://packagist.org/packages/baopham/dynamodb)
7 | [](https://travis-ci.org/baopham/laravel-dynamodb)
8 | [](https://scrutinizer-ci.com/g/baopham/laravel-dynamodb/?branch=master)
9 | [](https://packagist.org/packages/baopham/dynamodb)
10 |
11 | Supports all key types - primary hash key and composite keys.
12 |
13 | > For advanced users only. If you're not familiar with Laravel, Laravel Eloquent and DynamoDB, then I suggest that you get familiar with those first.
14 |
15 | **Breaking changes in v2: config no longer lives in config/services.php**
16 |
17 | * [Install](#install)
18 | * [Usage](#usage)
19 | * [find() and delete()](#find-and-delete)
20 | * [Conditions](#conditions)
21 | * [all() and first()](#all-and-first)
22 | * [Pagination](#pagination)
23 | * [update](#update) / [updateAsync()](#updateasync)
24 | * [save](#save) / [saveAsync()](#saveasync)
25 | * [delete](#delete) / [deleteAsync()](#deleteasync)
26 | * [chunk](#chunk)
27 | * [limit() and take()](#limit-and-take)
28 | * [firstOrFail()](#firstorfail)
29 | * [findOrFail()](#findorfail)
30 | * [refresh()](#refresh)
31 | * [Query scope](#query-scope)
32 | * [REMOVE — Deleting Attributes From An Item](#remove--deleting-attributes-from-an-item)
33 | * [toSql() Style](#tosql-style)
34 | * [Decorate Query](#decorate-query)
35 | * [Indexes](#indexes)
36 | * [Composite Keys](#composite-keys)
37 | * [Query Builder](#query-builder)
38 | * [Requirements](#requirements)
39 | * [Migrate from v1 to v2](#migrate-from-v1-to-v2)
40 | * [FAQ](#faq)
41 | * [License](LICENSE)
42 | * [Author and Contributors](#author-and-contributors)
43 |
44 | Install
45 | ------
46 |
47 | * Composer install
48 | ```bash
49 | composer require baopham/dynamodb
50 | ```
51 |
52 | * Install service provider (< Laravel 5.5):
53 |
54 | ```php
55 | // config/app.php
56 |
57 | 'providers' => [
58 | ...
59 | BaoPham\DynamoDb\DynamoDbServiceProvider::class,
60 | ...
61 | ];
62 | ```
63 |
64 | * Run
65 |
66 | ```php
67 | php artisan vendor:publish --provider 'BaoPham\DynamoDb\DynamoDbServiceProvider'
68 | ```
69 |
70 | * Update DynamoDb config in [config/dynamodb.php](config/dynamodb.php)
71 |
72 | **For Lumen**
73 |
74 | * Try [this](https://github.com/laravelista/lumen-vendor-publish) to install the `vendor:publish` command
75 |
76 | * Load configuration file and enable Eloquent support in `bootstrap/app.php`:
77 |
78 | ```php
79 | $app = new Laravel\Lumen\Application(
80 | realpath(__DIR__.'/../')
81 | );
82 |
83 | // Load dynamodb config file
84 | $app->configure('dynamodb');
85 |
86 | // Enable Facade support
87 | $app->withFacades();
88 |
89 | // Enable Eloquent support
90 | $app->withEloquent();
91 | ```
92 |
93 |
94 |
95 | Usage
96 | -----
97 | * Extends your model with `BaoPham\DynamoDb\DynamoDbModel`, then you can use Eloquent methods that are supported. The idea here is that you can switch back to Eloquent without changing your queries.
98 | * Or if you want to sync your DB table with a DynamoDb table, use trait `BaoPham\DynamoDb\ModelTrait`, it will call a `PutItem` after the model is saved.
99 | * Alternatively, you can use the [query builder](#query-builder) facade to build more complex queries.
100 | * 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.
101 |
102 | ### Supported features:
103 |
104 | #### find() and delete()
105 |
106 | ```php
107 | $model->find($id, array $columns = []);
108 | $model->findMany($ids, array $columns = []);
109 | $model->delete();
110 | $model->deleteAsync()->wait();
111 | ```
112 |
113 | #### Conditions
114 |
115 | ```php
116 | // Using getIterator()
117 | // If 'key' is the primary key or a global/local index and it is a supported Query condition,
118 | // will use 'Query', otherwise 'Scan'.
119 | $model->where('key', 'key value')->get();
120 |
121 | $model->where(['key' => 'key value']);
122 |
123 | // Chainable for 'AND'.
124 | $model->where('foo', 'bar')
125 | ->where('foo2', '!=', 'bar2')
126 | ->get();
127 |
128 | // Chainable for 'OR'.
129 | $model->where('foo', 'bar')
130 | ->orWhere('foo2', '!=', 'bar2')
131 | ->get();
132 |
133 | // Other types of conditions
134 | $model->where('count', '>', 0)->get();
135 | $model->where('count', '>=', 0)->get();
136 | $model->where('count', '<', 0)->get();
137 | $model->where('count', '<=', 0)->get();
138 | $model->whereIn('count', [0, 100])->get();
139 | $model->whereNotIn('count', [0, 100])->get();
140 | $model->where('count', 'between', [0, 100])->get();
141 | $model->where('description', 'begins_with', 'foo')->get();
142 | $model->where('description', 'contains', 'foo')->get();
143 | $model->where('description', 'not_contains', 'foo')->get();
144 |
145 | // Nested conditions
146 | $model->where('name', 'foo')
147 | ->where(function ($query) {
148 | $query->where('count', 10)->orWhere('count', 20);
149 | })
150 | ->get();
151 |
152 | // Nested attributes
153 | $model->where('nestedMap.foo', 'bar')->where('list[0]', 'baz')->get();
154 | ```
155 |
156 | ##### whereNull() and whereNotNull()
157 |
158 | > NULL and NOT_NULL only check for the attribute presence not its value being null
159 | > See: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
160 |
161 | ```php
162 | $model->whereNull('name');
163 | $model->whereNotNull('name');
164 | ```
165 |
166 | #### all() and first()
167 |
168 | ```php
169 | // Using scan operator, not too reliable since DynamoDb will only give 1MB total of data.
170 | $model->all();
171 |
172 | // Basically a scan but with limit of 1 item.
173 | $model->first();
174 | ```
175 |
176 | #### Pagination
177 |
178 | Unfortunately, offset of how many records to skip does not make sense for DynamoDb.
179 | Instead, provide the last result of the previous query as the starting point for the next query.
180 |
181 | **Examples:**
182 |
183 | For query such as:
184 |
185 | ```php
186 | $query = $model->where('count', 10)->limit(2);
187 | $items = $query->all();
188 | $last = $items->last();
189 | ```
190 |
191 | Take the last item of this query result as the next "offset":
192 |
193 | ```php
194 | $nextPage = $query->after($last)->limit(2)->all();
195 | // or
196 | $nextPage = $query->afterKey($items->lastKey())->limit(2)->all();
197 | // or (for query without index condition only)
198 | $nextPage = $query->afterKey($last->getKeys())->limit(2)->all();
199 | ```
200 |
201 | #### update()
202 |
203 | ```php
204 | // update
205 | $model->update($attributes);
206 | ```
207 |
208 | #### updateAsync()
209 |
210 | ```php
211 | // update asynchronously and wait on the promise for completion.
212 | $model->updateAsync($attributes)->wait();
213 | ```
214 |
215 | #### save()
216 |
217 | ```php
218 | $model = new Model();
219 | // Define fillable attributes in your Model class.
220 | $model->fillableAttr1 = 'foo';
221 | $model->fillableAttr2 = 'foo';
222 | // DynamoDb doesn't support incremented Id, so you need to use UUID for the primary key.
223 | $model->id = 'de305d54-75b4-431b-adb2-eb6b9e546014';
224 | $model->save();
225 | ```
226 |
227 | #### saveAsync()
228 |
229 | Saving single model asynchronously and waiting on the promise for completion.
230 |
231 | ```php
232 | $model = new Model();
233 | // Define fillable attributes in your Model class.
234 | $model->fillableAttr1 = 'foo';
235 | $model->fillableAttr2 = 'bar';
236 | // DynamoDb doesn't support incremented Id, so you need to use UUID for the primary key.
237 | $model->id = 'de305d54-75b4-431b-adb2-eb6b9e546014';
238 | $model->saveAsync()->wait();
239 | ```
240 |
241 | Saving multiple models asynchronously and waiting on all of them simultaneously.
242 |
243 | ```php
244 | for($i = 0; $i < 10; $i++){
245 | $model = new Model();
246 | // Define fillable attributes in your Model class.
247 | $model->fillableAttr1 = 'foo';
248 | $model->fillableAttr2 = 'bar';
249 | // DynamoDb doesn't support incremented Id, so you need to use UUID for the primary key.
250 | $model->id = uniqid();
251 | // Returns a promise which you can wait on later.
252 | $promises[] = $model->saveAsync();
253 | }
254 |
255 | \GuzzleHttp\Promise\all($promises)->wait();
256 | ```
257 |
258 | #### delete()
259 |
260 | ```php
261 | $model->delete();
262 | ```
263 |
264 | #### deleteAsync()
265 |
266 | ```php
267 | $model->deleteAsync()->wait();
268 | ```
269 |
270 | #### chunk()
271 |
272 | ```php
273 | $model->chunk(10, function ($records) {
274 | foreach ($records as $record) {
275 |
276 | }
277 | });
278 | ```
279 |
280 | #### limit() and take()
281 |
282 | ```php
283 | // Use this with caution unless your limit is small.
284 | // DynamoDB has a limit of 1MB so if your limit is very big, the results will not be expected.
285 | $model->where('name', 'foo')->take(3)->get();
286 | ```
287 |
288 | #### firstOrFail()
289 |
290 | ```php
291 | $model->where('name', 'foo')->firstOrFail();
292 | // for composite key
293 | $model->where('id', 'foo')->where('id2', 'bar')->firstOrFail();
294 | ```
295 |
296 | #### findOrFail()
297 |
298 | ```php
299 | $model->findOrFail('foo');
300 | // for composite key
301 | $model->findOrFail(['id' => 'foo', 'id2' => 'bar']);
302 | ```
303 |
304 | #### refresh()
305 |
306 | ```php
307 | $model = Model::first();
308 | $model->refresh();
309 | ```
310 |
311 | #### Query Scope
312 |
313 | ```php
314 | class Foo extends DynamoDbModel
315 | {
316 | protected static function boot()
317 | {
318 | parent::boot();
319 |
320 | static::addGlobalScope('count', function (DynamoDbQueryBuilder $builder) {
321 | $builder->where('count', '>', 6);
322 | });
323 | }
324 |
325 | public function scopeCountUnderFour($builder)
326 | {
327 | return $builder->where('count', '<', 4);
328 | }
329 |
330 | public function scopeCountUnder($builder, $count)
331 | {
332 | return $builder->where('count', '<', $count);
333 | }
334 | }
335 |
336 | $foo = new Foo();
337 | // Global scope will be applied
338 | $foo->all();
339 | // Local scope
340 | $foo->withoutGlobalScopes()->countUnderFour()->get();
341 | // Dynamic local scope
342 | $foo->withoutGlobalScopes()->countUnder(6)->get();
343 | ```
344 |
345 | #### REMOVE — Deleting Attributes From An Item
346 |
347 | > See: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.REMOVE
348 |
349 | ```php
350 | $model = new Model();
351 | $model->where('id', 'foo')->removeAttribute('name', 'description', 'nested.foo', 'nestedArray[0]');
352 |
353 | // Or
354 | Model::find('foo')->removeAttribute('name', 'description', 'nested.foo', 'nestedArray[0]');
355 | ```
356 |
357 |
358 | #### toSql() Style
359 |
360 | For debugging purposes, you can choose to convert to the actual DynamoDb query
361 |
362 | ```php
363 | $raw = $model->where('count', '>', 10)->toDynamoDbQuery();
364 | // $op is either "Scan" or "Query"
365 | $op = $raw->op;
366 | // The query body being sent to AWS
367 | $query = $raw->query;
368 | ```
369 |
370 | where `$raw` is an instance of [RawDynamoDbQuery](./src/RawDynamoDbQuery.php)
371 |
372 |
373 | #### Decorate Query
374 |
375 | Use `decorate` when you want to enhance the query. For example:
376 |
377 | To set the order of the sort key:
378 |
379 | ```php
380 | $items = $model
381 | ->where('hash', 'hash-value')
382 | ->where('range', '>', 10)
383 | ->decorate(function (RawDynamoDbQuery $raw) {
384 | // desc order
385 | $raw->query['ScanIndexForward'] = false;
386 | })
387 | ->get();
388 | ```
389 |
390 | To force to use "Query" instead of "Scan" if the library fails to detect the correct operation:
391 |
392 | ```php
393 | $items = $model
394 | ->where('hash', 'hash-value')
395 | ->decorate(function (RawDynamoDbQuery $raw) {
396 | $raw->op = 'Query';
397 | })
398 | ->get();
399 | ```
400 |
401 | Indexes
402 | -----------
403 | If your table has indexes, make sure to declare them in your model class like so
404 |
405 | ```php
406 | /**
407 | * Indexes.
408 | * [
409 | * '' => [
410 | * 'hash' => ''
411 | * ],
412 | * '' => [
413 | * 'hash' => '',
414 | * 'range' => ''
415 | * ],
416 | * ]
417 | *
418 | * @var array
419 | */
420 | protected $dynamoDbIndexKeys = [
421 | 'count_index' => [
422 | 'hash' => 'count'
423 | ],
424 | ];
425 | ```
426 |
427 | Note that order of index matters when a key exists in multiple indexes.
428 | For example, we have this
429 |
430 | ```php
431 | $model->where('user_id', 123)->where('count', '>', 10)->get();
432 | ```
433 |
434 | with
435 |
436 | ```php
437 | protected $dynamoDbIndexKeys = [
438 | 'count_index' => [
439 | 'hash' => 'user_id',
440 | 'range' => 'count'
441 | ],
442 | 'user_index' => [
443 | 'hash' => 'user_id',
444 | ],
445 | ];
446 | ```
447 |
448 | will use `count_index`.
449 |
450 | ```php
451 | protected $dynamoDbIndexKeys = [
452 | 'user_index' => [
453 | 'hash' => 'user_id',
454 | ],
455 | 'count_index' => [
456 | 'hash' => 'user_id',
457 | 'range' => 'count'
458 | ]
459 | ];
460 | ```
461 |
462 | will use `user_index`.
463 |
464 |
465 | 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
466 |
467 | ```php
468 | $model->where('user_id', 123)->where('count', '>', 10)->withIndex('count_index')->get();
469 | ```
470 |
471 |
472 | Composite Keys
473 | --------------
474 | To use composite keys with your model:
475 |
476 | * Set `$compositeKey` to an array of the attributes names comprising the key, e.g.
477 |
478 | ```php
479 | protected $primaryKey = 'customer_id';
480 | protected $compositeKey = ['customer_id', 'agent_id'];
481 | ```
482 |
483 | * To find a record with a composite key
484 |
485 | ```php
486 | $model->find(['customer_id' => 'value1', 'agent_id' => 'value2']);
487 | ```
488 |
489 | Query Builder
490 | -------------
491 |
492 | Use `DynamoDb` facade to build raw queries
493 |
494 | ```php
495 | use BaoPham\DynamoDb\Facades\DynamoDb;
496 |
497 | DynamoDb::table('articles')
498 | // call set to build the query body to be sent to AWS
499 | ->setFilterExpression('#name = :name')
500 | ->setExpressionAttributeNames(['#name' => 'author_name'])
501 | ->setExpressionAttributeValues([':name' => DynamoDb::marshalValue('Bao')])
502 | ->prepare()
503 | // the query body will be sent upon calling this.
504 | ->scan(); // supports any DynamoDbClient methods (e.g. batchWriteItem, batchGetItem, etc.)
505 |
506 | DynamoDb::table('articles')
507 | ->setIndexName('author_name')
508 | ->setKeyConditionExpression('#name = :name')
509 | ->setProjectionExpression('id, author_name')
510 | // Can set the attribute mapping one by one instead
511 | ->setExpressionAttributeName('#name', 'author_name')
512 | ->setExpressionAttributeValue(':name', DynamoDb::marshalValue('Bao'))
513 | ->prepare()
514 | ->query();
515 |
516 | DynamoDb::table('articles')
517 | ->setKey(DynamoDb::marshalItem(['id' => 'ae025ed8']))
518 | ->setUpdateExpression('REMOVE #c, #t')
519 | ->setExpressionAttributeName('#c', 'comments')
520 | ->setExpressionAttributeName('#t', 'tags')
521 | ->prepare()
522 | ->updateItem();
523 |
524 | DynamoDb::table('articles')
525 | ->setKey(DynamoDb::marshalItem(['id' => 'ae025ed8']))
526 | ->prepare()
527 | ->deleteItem();
528 |
529 | DynamoDb::table('articles')
530 | ->setItem(DynamoDb::marshalItem(['id' => 'ae025ed8', 'author_name' => 'New Name']))
531 | ->prepare()
532 | ->putItem();
533 |
534 | // Or, instead of ::table()
535 | DynamoDb::newQuery()
536 | ->setTableName('articles')
537 |
538 | // Or access the DynamoDbClient instance directly
539 | DynamoDb::client();
540 | // pass in the connection name to get a different client instance other than the default.
541 | DynamoDb::client('test');
542 | ```
543 |
544 | The query builder methods are in the form of `set`, where `` is the key name of the query body to be sent.
545 |
546 | For example, to build an [`UpdateTable`](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-dynamodb-2012-08-10.html#updatetable) query:
547 |
548 | ```php
549 | [
550 | 'AttributeDefinitions' => ...,
551 | 'GlobalSecondaryIndexUpdates' => ...,
552 | 'TableName' => ...
553 | ]
554 | ```
555 |
556 | Do:
557 |
558 | ```php
559 | $query = DynamoDb::table('articles')
560 | ->setAttributeDefinitions(...)
561 | ->setGlobalSecondaryIndexUpdates(...);
562 | ```
563 |
564 | And when ready:
565 |
566 | ```php
567 | $query->prepare()->updateTable();
568 | ```
569 |
570 | Requirements
571 | -------------
572 | Laravel ^5.1
573 |
574 |
575 | Migrate from v1 to v2
576 | ---------------------
577 |
578 | Follow these steps:
579 |
580 | 1. Update your `composer.json` to use v2
581 | 1. Run `composer update`
582 | 1. Run `php artisan vendor:publish`
583 | 1. Move your DynamoDb config in `config/services.php` to the new config file `config/dynamodb.php` as one of the connections
584 | 1. Move `key`, `secret`, `token` inside `credentials`
585 | 1. Rename `local_endpoint` to `endpoint`
586 | 1. Remove `local` field
587 |
588 |
589 | FAQ
590 | ---
591 | Q: Cannot assign `id` property if its not in the fillable array
592 | A: Try [this](https://github.com/baopham/laravel-dynamodb/issues/10)?
593 |
594 |
595 | Q: How to create migration?
596 | A: Please see [this issue](https://github.com/baopham/laravel-dynamodb/issues/90)
597 |
598 |
599 | Q: How to use with factory?
600 | A: Please see [this issue](https://github.com/baopham/laravel-dynamodb/issues/111)
601 |
602 |
603 | Q: How do I use with Job? Getting a SerializesModels error
604 | A: You can either [write your own restoreModel](https://github.com/baopham/laravel-dynamodb/issues/132) or remove the `SerializesModels` trait from your Job.
605 |
606 |
607 | Author and Contributors
608 | -------
609 | * [Bao Pham](https://github.com/baopham/laravel-dynamodb)
610 | * [warrick-loyaltycorp](https://github.com/warrick-loyaltycorp)
611 | * [Alexander Ward](https://github.com/cthos)
612 | * [Quang Ngo](https://github.com/vanquang9387)
613 | * [David Higgins](https://github.com/zoul0813)
614 | * [Damon Williams](https://github.com/footballencarta)
615 | * [David Palmer](https://github.com/dp88)
616 |
--------------------------------------------------------------------------------
/src/DynamoDbQueryBuilder.php:
--------------------------------------------------------------------------------
1 | model = $model;
82 | $this->client = $model->getClient();
83 | $this->setupExpressions();
84 | }
85 |
86 | /**
87 | * Alias to set the "limit" value of the query.
88 | *
89 | * @param int $value
90 | * @return DynamoDbQueryBuilder
91 | */
92 | public function take($value)
93 | {
94 | return $this->limit($value);
95 | }
96 |
97 | /**
98 | * Set the "limit" value of the query.
99 | *
100 | * @param int $value
101 | * @return $this
102 | */
103 | public function limit($value)
104 | {
105 | $this->limit = $value;
106 |
107 | return $this;
108 | }
109 |
110 | /**
111 | * Alias to set the "offset" value of the query.
112 | *
113 | * @param int $value
114 | * @throws NotSupportedException
115 | */
116 | public function skip($value)
117 | {
118 | return $this->offset($value);
119 | }
120 |
121 | /**
122 | * Set the "offset" value of the query.
123 | *
124 | * @param int $value
125 | * @throws NotSupportedException
126 | */
127 | public function offset($value)
128 | {
129 | throw new NotSupportedException('Skip/Offset is not supported. Consider using after() instead');
130 | }
131 |
132 | /**
133 | * Determine the starting point (exclusively) of the query.
134 | * Unfortunately, offset of how many records to skip does not make sense for DynamoDb.
135 | * Instead, provide the last result of the previous query as the starting point for the next query.
136 | *
137 | * @param DynamoDbModel|null $after
138 | * Examples:
139 | *
140 | * For query such as
141 | * $query = $model->where('count', 10)->limit(2);
142 | * $last = $query->all()->last();
143 | * Take the last item of this query result as the next "offset":
144 | * $nextPage = $query->after($last)->limit(2)->all();
145 | *
146 | * Alternatively, pass in nothing to reset the starting point.
147 | *
148 | * @return $this
149 | */
150 | public function after(DynamoDbModel $after = null)
151 | {
152 | if (empty($after)) {
153 | $this->lastEvaluatedKey = null;
154 |
155 | return $this;
156 | }
157 |
158 | $afterKey = $after->getKeys();
159 |
160 | $analyzer = $this->getConditionAnalyzer();
161 |
162 | if ($index = $analyzer->index()) {
163 | foreach ($index->columns() as $column) {
164 | $afterKey[$column] = $after->getAttribute($column);
165 | }
166 | }
167 |
168 | $this->lastEvaluatedKey = DynamoDb::marshalItem($afterKey);
169 |
170 | return $this;
171 | }
172 |
173 | /**
174 | * Similar to after(), but instead of using the model instance, the model's keys are used.
175 | * Use $collection->lastKey() or $model->getKeys() to retrieve the value.
176 | *
177 | * @param Array $key
178 | * Examples:
179 | *
180 | * For query such as
181 | * $query = $model->where('count', 10)->limit(2);
182 | * $items = $query->all();
183 | * Take the last item of this query result as the next "offset":
184 | * $nextPage = $query->afterKey($items->lastKey())->limit(2)->all();
185 | *
186 | * Alternatively, pass in nothing to reset the starting point.
187 | *
188 | * @return $this
189 | */
190 | public function afterKey($key = null)
191 | {
192 | $this->lastEvaluatedKey = empty($key) ? null : DynamoDb::marshalItem($key);
193 | return $this;
194 | }
195 |
196 | /**
197 | * Set the index name manually
198 | *
199 | * @param string $index The index name
200 | * @return $this
201 | */
202 | public function withIndex($index)
203 | {
204 | $this->index = $index;
205 | return $this;
206 | }
207 |
208 | public function where($column, $operator = null, $value = null, $boolean = 'and')
209 | {
210 | // If the column is an array, we will assume it is an array of key-value pairs
211 | // and can add them each as a where clause. We will maintain the boolean we
212 | // received when the method was called and pass it into the nested where.
213 | if (is_array($column)) {
214 | foreach ($column as $key => $value) {
215 | $this->where($key, '=', $value, $boolean);
216 | }
217 |
218 | return $this;
219 | }
220 |
221 | // Here we will make some assumptions about the operator. If only 2 values are
222 | // passed to the method, we will assume that the operator is an equals sign
223 | // and keep going. Otherwise, we'll require the operator to be passed in.
224 | if (func_num_args() == 2) {
225 | list($value, $operator) = [$operator, '='];
226 | }
227 |
228 | // If the columns is actually a Closure instance, we will assume the developer
229 | // wants to begin a nested where statement which is wrapped in parenthesis.
230 | // We'll add that Closure to the query then return back out immediately.
231 | if ($column instanceof Closure) {
232 | return $this->whereNested($column, $boolean);
233 | }
234 |
235 | // If the given operator is not found in the list of valid operators we will
236 | // assume that the developer is just short-cutting the '=' operators and
237 | // we will set the operators to '=' and set the values appropriately.
238 | if (!ComparisonOperator::isValidOperator($operator)) {
239 | list($value, $operator) = [$operator, '='];
240 | }
241 |
242 | // If the value is a Closure, it means the developer is performing an entire
243 | // sub-select within the query and we will need to compile the sub-select
244 | // within the where clause to get the appropriate query record results.
245 | if ($value instanceof Closure) {
246 | throw new NotSupportedException('Closure in where clause is not supported');
247 | }
248 |
249 | $this->wheres[] = [
250 | 'column' => $column,
251 | 'type' => ComparisonOperator::getDynamoDbOperator($operator),
252 | 'value' => $value,
253 | 'boolean' => $boolean,
254 | ];
255 |
256 | return $this;
257 | }
258 |
259 | /**
260 | * Add a nested where statement to the query.
261 | *
262 | * @param \Closure $callback
263 | * @param string $boolean
264 | * @return $this
265 | */
266 | public function whereNested(Closure $callback, $boolean = 'and')
267 | {
268 | call_user_func($callback, $query = $this->forNestedWhere());
269 |
270 | return $this->addNestedWhereQuery($query, $boolean);
271 | }
272 |
273 | /**
274 | * Create a new query instance for nested where condition.
275 | *
276 | * @return $this
277 | */
278 | public function forNestedWhere()
279 | {
280 | return $this->newQuery();
281 | }
282 |
283 | /**
284 | * Add another query builder as a nested where to the query builder.
285 | *
286 | * @param DynamoDbQueryBuilder $query
287 | * @param string $boolean
288 | * @return $this
289 | */
290 | public function addNestedWhereQuery($query, $boolean = 'and')
291 | {
292 | if (count($query->wheres)) {
293 | $type = 'Nested';
294 | $column = null;
295 | $value = $query->wheres;
296 | $this->wheres[] = compact('column', 'type', 'value', 'boolean');
297 | }
298 |
299 | return $this;
300 | }
301 |
302 | /**
303 | * Add an "or where" clause to the query.
304 | *
305 | * @param string $column
306 | * @param string $operator
307 | * @param mixed $value
308 | * @return $this
309 | */
310 | public function orWhere($column, $operator = null, $value = null)
311 | {
312 | return $this->where($column, $operator, $value, 'or');
313 | }
314 |
315 | /**
316 | * Add a "where in" clause to the query.
317 | *
318 | * @param string $column
319 | * @param mixed $values
320 | * @param string $boolean
321 | * @param bool $not
322 | * @return $this
323 | * @throws NotSupportedException
324 | */
325 | public function whereIn($column, $values, $boolean = 'and', $not = false)
326 | {
327 | if ($not) {
328 | throw new NotSupportedException('"not in" is not a valid DynamoDB comparison operator');
329 | }
330 |
331 | // If the value is a query builder instance, not supported
332 | if ($values instanceof static) {
333 | throw new NotSupportedException('Value is a query builder instance');
334 | }
335 |
336 | // If the value of the where in clause is actually a Closure, not supported
337 | if ($values instanceof Closure) {
338 | throw new NotSupportedException('Value is a Closure');
339 | }
340 |
341 | // Next, if the value is Arrayable we need to cast it to its raw array form
342 | if ($values instanceof Arrayable) {
343 | $values = $values->toArray();
344 | }
345 |
346 | return $this->where($column, ComparisonOperator::IN, $values, $boolean);
347 | }
348 |
349 | /**
350 | * Add an "or where in" clause to the query.
351 | *
352 | * @param string $column
353 | * @param mixed $values
354 | * @return $this
355 | */
356 | public function orWhereIn($column, $values)
357 | {
358 | return $this->whereIn($column, $values, 'or');
359 | }
360 |
361 | /**
362 | * Add a "where null" clause to the query.
363 | *
364 | * @param string $column
365 | * @param string $boolean
366 | * @param bool $not
367 | * @return $this
368 | */
369 | public function whereNull($column, $boolean = 'and', $not = false)
370 | {
371 | $type = $not ? ComparisonOperator::NOT_NULL : ComparisonOperator::NULL;
372 |
373 | $this->wheres[] = compact('column', 'type', 'boolean');
374 |
375 | return $this;
376 | }
377 |
378 | /**
379 | * Add an "or where null" clause to the query.
380 | *
381 | * @param string $column
382 | * @return $this
383 | */
384 | public function orWhereNull($column)
385 | {
386 | return $this->whereNull($column, 'or');
387 | }
388 |
389 | /**
390 | * Add an "or where not null" clause to the query.
391 | *
392 | * @param string $column
393 | * @return $this
394 | */
395 | public function orWhereNotNull($column)
396 | {
397 | return $this->whereNotNull($column, 'or');
398 | }
399 |
400 | /**
401 | * Add a "where not null" clause to the query.
402 | *
403 | * @param string $column
404 | * @param string $boolean
405 | * @return $this
406 | */
407 | public function whereNotNull($column, $boolean = 'and')
408 | {
409 | return $this->whereNull($column, $boolean, true);
410 | }
411 |
412 | /**
413 | * Get a new instance of the query builder.
414 | *
415 | * @return DynamoDbQueryBuilder
416 | */
417 | public function newQuery()
418 | {
419 | return new static($this->getModel());
420 | }
421 |
422 | /**
423 | * Implements the Query Chunk method
424 | *
425 | * @param int $chunkSize
426 | * @param callable $callback
427 | */
428 | public function chunk($chunkSize, callable $callback)
429 | {
430 | while (true) {
431 | $results = $this->getAll([], $chunkSize, false);
432 |
433 | if (!$results->isEmpty()) {
434 | if (call_user_func($callback, $results) === false) {
435 | return false;
436 | }
437 | }
438 |
439 | if (empty($this->lastEvaluatedKey)) {
440 | break;
441 | }
442 | }
443 |
444 | return true;
445 | }
446 |
447 | /**
448 | * @param $id
449 | * @param array $columns
450 | * @return DynamoDbModel|\Illuminate\Database\Eloquent\Collection|null
451 | */
452 | public function find($id, array $columns = [])
453 | {
454 | if ($this->isMultipleIds($id)) {
455 | return $this->findMany($id, $columns);
456 | }
457 |
458 | $this->resetExpressions();
459 |
460 | $this->model->setId($id);
461 |
462 | $query = DynamoDb::table($this->model->getTable())
463 | ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
464 | ->setConsistentRead(true);
465 |
466 | if (!empty($columns)) {
467 | $query
468 | ->setProjectionExpression($this->projectionExpression->parse($columns))
469 | ->setExpressionAttributeNames($this->expressionAttributeNames->all());
470 | }
471 |
472 | $item = $query->prepare($this->client)->getItem();
473 |
474 | $item = Arr::get($item->toArray(), 'Item');
475 |
476 | if (empty($item)) {
477 | return null;
478 | }
479 |
480 | $item = DynamoDb::unmarshalItem($item);
481 |
482 | $model = $this->model->newInstance([], true);
483 |
484 | $model->setRawAttributes($item, true);
485 |
486 | return $model;
487 | }
488 |
489 | /**
490 | * @param $ids
491 | * @param array $columns
492 | * @return \Illuminate\Database\Eloquent\Collection
493 | */
494 | public function findMany($ids, array $columns = [])
495 | {
496 | $collection = $this->model->newCollection();
497 |
498 | if (empty($ids)) {
499 | return $collection;
500 | }
501 |
502 | $this->resetExpressions();
503 |
504 | $table = $this->model->getTable();
505 |
506 | $keys = collect($ids)->map(function ($id) {
507 | if (! is_array($id)) {
508 | $id = [$this->model->getKeyName() => $id];
509 | }
510 |
511 | return DynamoDb::marshalItem($id);
512 | });
513 |
514 | $subQuery = DynamoDb::newQuery()
515 | ->setKeys($keys->toArray())
516 | ->setProjectionExpression($this->projectionExpression->parse($columns))
517 | ->setExpressionAttributeNames($this->expressionAttributeNames->all())
518 | ->prepare($this->client)
519 | ->query;
520 |
521 | $results = DynamoDb::newQuery()
522 | ->setRequestItems([$table => $subQuery])
523 | ->prepare($this->client)
524 | ->batchGetItem();
525 |
526 | foreach ($results['Responses'][$table] as $item) {
527 | $item = DynamoDb::unmarshalItem($item);
528 | $model = $this->model->newInstance([], true);
529 | $model->setRawAttributes($item, true);
530 | $collection->add($model);
531 | }
532 |
533 | return $collection;
534 | }
535 |
536 | public function findOrFail($id, $columns = [])
537 | {
538 | $result = $this->find($id, $columns);
539 |
540 | if ($this->isMultipleIds($id)) {
541 | if (count($result) == count(array_unique($id))) {
542 | return $result;
543 | }
544 | } elseif (! is_null($result)) {
545 | return $result;
546 | }
547 |
548 | throw (new ModelNotFoundException)->setModel(
549 | get_class($this->model),
550 | $id
551 | );
552 | }
553 |
554 | public function first($columns = [])
555 | {
556 | $items = $this->getAll($columns, 1);
557 |
558 | return $items->first();
559 | }
560 |
561 | public function firstOrFail($columns = [])
562 | {
563 | if (! is_null($model = $this->first($columns))) {
564 | return $model;
565 | }
566 |
567 | throw (new ModelNotFoundException)->setModel(get_class($this->model));
568 | }
569 |
570 | /**
571 | * Remove attributes from an existing item
572 | *
573 | * @param array ...$attributes
574 | * @return bool
575 | * @throws InvalidQuery
576 | */
577 | public function removeAttribute(...$attributes)
578 | {
579 | $keySet = !empty(array_filter($this->model->getKeys()));
580 |
581 | if (!$keySet) {
582 | $analyzer = $this->getConditionAnalyzer();
583 |
584 | if (!$analyzer->isExactSearch()) {
585 | throw new InvalidQuery('Need to provide the key in your query');
586 | }
587 |
588 | $id = $analyzer->identifierConditionValues();
589 | $this->model->setId($id);
590 | }
591 |
592 | $key = DynamoDb::marshalItem($this->model->getKeys());
593 |
594 | $this->resetExpressions();
595 |
596 | /** @var \Aws\Result $result */
597 | $result = DynamoDb::table($this->model->getTable())
598 | ->setKey($key)
599 | ->setUpdateExpression($this->updateExpression->remove($attributes))
600 | ->setExpressionAttributeNames($this->expressionAttributeNames->all())
601 | ->setReturnValues('ALL_NEW')
602 | ->prepare($this->client)
603 | ->updateItem();
604 |
605 | $success = Arr::get($result, '@metadata.statusCode') === 200;
606 |
607 | if ($success) {
608 | $this->model->setRawAttributes(DynamoDb::unmarshalItem($result->get('Attributes')));
609 | $this->model->syncOriginal();
610 | }
611 |
612 | return $success;
613 | }
614 |
615 | public function delete()
616 | {
617 | $result = DynamoDb::table($this->model->getTable())
618 | ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
619 | ->prepare($this->client)
620 | ->deleteItem();
621 |
622 | return Arr::get($result->toArray(), '@metadata.statusCode') === 200;
623 | }
624 |
625 | public function deleteAsync()
626 | {
627 | $promise = DynamoDb::table($this->model->getTable())
628 | ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
629 | ->prepare($this->client)
630 | ->deleteItemAsync();
631 |
632 | return $promise;
633 | }
634 |
635 | public function save()
636 | {
637 | $result = DynamoDb::table($this->model->getTable())
638 | ->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
639 | ->prepare($this->client)
640 | ->putItem();
641 |
642 | return Arr::get($result, '@metadata.statusCode') === 200;
643 | }
644 |
645 | public function saveAsync()
646 | {
647 | $promise = DynamoDb::table($this->model->getTable())
648 | ->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
649 | ->prepare($this->client)
650 | ->putItemAsync();
651 |
652 | return $promise;
653 | }
654 |
655 | public function get($columns = [])
656 | {
657 | return $this->all($columns);
658 | }
659 |
660 | public function all($columns = [])
661 | {
662 | $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
663 | return $this->getAll($columns, $limit, !isset($this->limit));
664 | }
665 |
666 | public function count()
667 | {
668 | $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
669 | $raw = $this->toDynamoDbQuery(['count(*)'], $limit);
670 |
671 | if ($raw->op === 'Scan') {
672 | $res = $this->client->scan($raw->query);
673 | } else {
674 | $res = $this->client->query($raw->query);
675 | }
676 |
677 | return $res['Count'];
678 | }
679 |
680 | public function decorate(Closure $closure)
681 | {
682 | $this->decorator = $closure;
683 | return $this;
684 | }
685 |
686 | protected function getAll(
687 | $columns = [],
688 | $limit = DynamoDbQueryBuilder::MAX_LIMIT,
689 | $useIterator = DynamoDbQueryBuilder::DEFAULT_TO_ITERATOR
690 | ) {
691 | $analyzer = $this->getConditionAnalyzer();
692 |
693 | if ($analyzer->isExactSearch()) {
694 | $item = $this->find($analyzer->identifierConditionValues(), $columns);
695 |
696 | return $this->getModel()->newCollection([$item]);
697 | }
698 |
699 | $raw = $this->toDynamoDbQuery($columns, $limit);
700 |
701 | if ($useIterator) {
702 | $iterator = $this->client->getIterator($raw->op, $raw->query);
703 |
704 | if (isset($raw->query['Limit'])) {
705 | $iterator = new \LimitIterator($iterator, 0, $raw->query['Limit']);
706 | }
707 | } else {
708 | if ($raw->op === 'Scan') {
709 | $res = $this->client->scan($raw->query);
710 | } else {
711 | $res = $this->client->query($raw->query);
712 | }
713 |
714 | $this->lastEvaluatedKey = Arr::get($res, 'LastEvaluatedKey');
715 | $iterator = $res['Items'];
716 | }
717 |
718 | $results = [];
719 |
720 | foreach ($iterator as $item) {
721 | $item = DynamoDb::unmarshalItem($item);
722 | $model = $this->model->newInstance([], true);
723 | $model->setRawAttributes($item, true);
724 | $results[] = $model;
725 | }
726 |
727 | return $this->getModel()->newCollection($results, $analyzer->index());
728 | }
729 |
730 | /**
731 | * Return the raw DynamoDb query
732 | *
733 | * @param array $columns
734 | * @param int $limit
735 | * @return RawDynamoDbQuery
736 | */
737 | public function toDynamoDbQuery(
738 | $columns = [],
739 | $limit = DynamoDbQueryBuilder::MAX_LIMIT
740 | ) {
741 | $this->applyScopes();
742 |
743 | $this->resetExpressions();
744 |
745 | $op = 'Scan';
746 | $queryBuilder = DynamoDb::table($this->model->getTable());
747 |
748 | if (! empty($this->wheres)) {
749 | $analyzer = $this->getConditionAnalyzer();
750 |
751 | if ($keyConditions = $analyzer->keyConditions()) {
752 | $op = 'Query';
753 | $queryBuilder->setKeyConditionExpression($this->keyConditionExpression->parse($keyConditions));
754 | }
755 |
756 | if ($filterConditions = $analyzer->filterConditions()) {
757 | $queryBuilder->setFilterExpression($this->filterExpression->parse($filterConditions));
758 | }
759 |
760 | if ($index = $analyzer->index()) {
761 | $queryBuilder->setIndexName($index->name);
762 | }
763 | }
764 |
765 | if ($this->index) {
766 | // If user specifies the index manually, respect that
767 | $queryBuilder->setIndexName($this->index);
768 | }
769 |
770 | if ($limit !== static::MAX_LIMIT) {
771 | $queryBuilder->setLimit($limit);
772 | }
773 |
774 | if (!empty($columns)) {
775 | // Either we try to get the count or specific columns
776 | if ($columns == ['count(*)']) {
777 | $queryBuilder->setSelect('COUNT');
778 | } else {
779 | $queryBuilder->setProjectionExpression($this->projectionExpression->parse($columns));
780 | }
781 | }
782 |
783 | if (!empty($this->lastEvaluatedKey)) {
784 | $queryBuilder->setExclusiveStartKey($this->lastEvaluatedKey);
785 | }
786 |
787 | $queryBuilder
788 | ->setExpressionAttributeNames($this->expressionAttributeNames->all())
789 | ->setExpressionAttributeValues($this->expressionAttributeValues->all());
790 |
791 | $raw = new RawDynamoDbQuery($op, $queryBuilder->prepare($this->client)->query);
792 |
793 | if ($this->decorator) {
794 | call_user_func($this->decorator, $raw);
795 | }
796 |
797 | return $raw;
798 | }
799 |
800 | /**
801 | * @return Analyzer
802 | */
803 | protected function getConditionAnalyzer()
804 | {
805 | return with(new Analyzer)
806 | ->on($this->model)
807 | ->withIndex($this->index)
808 | ->analyze($this->wheres);
809 | }
810 |
811 | protected function isMultipleIds($id)
812 | {
813 | $keys = collect($this->model->getKeyNames());
814 |
815 | // could be ['id' => 'foo'], ['id1' => 'foo', 'id2' => 'bar']
816 | $single = $keys->first(function ($name) use ($id) {
817 | return !isset($id[$name]);
818 | }) === null;
819 |
820 | if ($single) {
821 | return false;
822 | }
823 |
824 | // could be ['foo', 'bar'], [['id1' => 'foo', 'id2' => 'bar'], ...]
825 | return $this->model->hasCompositeKey() ? is_array(H::array_first($id)) : is_array($id);
826 | }
827 |
828 | /**
829 | * @return DynamoDbModel
830 | */
831 | public function getModel()
832 | {
833 | return $this->model;
834 | }
835 |
836 | /**
837 | * @return \Aws\DynamoDb\DynamoDbClient
838 | */
839 | public function getClient()
840 | {
841 | return $this->client;
842 | }
843 |
844 | /**
845 | * Register a new global scope.
846 | *
847 | * @param string $identifier
848 | * @param \Illuminate\Database\Eloquent\Scope|\Closure $scope
849 | * @return $this
850 | */
851 | public function withGlobalScope($identifier, $scope)
852 | {
853 | $this->scopes[$identifier] = $scope;
854 |
855 | if (method_exists($scope, 'extend')) {
856 | $scope->extend($this);
857 | }
858 |
859 | return $this;
860 | }
861 |
862 | /**
863 | * Remove a registered global scope.
864 | *
865 | * @param \Illuminate\Database\Eloquent\Scope|string $scope
866 | * @return $this
867 | */
868 | public function withoutGlobalScope($scope)
869 | {
870 | if (! is_string($scope)) {
871 | $scope = get_class($scope);
872 | }
873 |
874 | unset($this->scopes[$scope]);
875 |
876 | $this->removedScopes[] = $scope;
877 |
878 | return $this;
879 | }
880 |
881 | /**
882 | * Remove all or passed registered global scopes.
883 | *
884 | * @param array|null $scopes
885 | * @return $this
886 | */
887 | public function withoutGlobalScopes(array $scopes = null)
888 | {
889 | if (is_array($scopes)) {
890 | foreach ($scopes as $scope) {
891 | $this->withoutGlobalScope($scope);
892 | }
893 | } else {
894 | $this->scopes = [];
895 | }
896 |
897 | return $this;
898 | }
899 |
900 | /**
901 | * Get an array of global scopes that were removed from the query.
902 | *
903 | * @return array
904 | */
905 | public function removedScopes()
906 | {
907 | return $this->removedScopes;
908 | }
909 |
910 | /**
911 | * Apply the scopes to the Eloquent builder instance and return it.
912 | *
913 | * @return DynamoDbQueryBuilder
914 | */
915 | public function applyScopes()
916 | {
917 | if (! $this->scopes) {
918 | return $this;
919 | }
920 |
921 | $builder = $this;
922 |
923 | foreach ($builder->scopes as $identifier => $scope) {
924 | if (! isset($builder->scopes[$identifier])) {
925 | continue;
926 | }
927 |
928 | $builder->callScope(function (DynamoDbQueryBuilder $builder) use ($scope) {
929 | // If the scope is a Closure we will just go ahead and call the scope with the
930 | // builder instance. The "callScope" method will properly group the clauses
931 | // that are added to this query so "where" clauses maintain proper logic.
932 | if ($scope instanceof Closure) {
933 | $scope($builder);
934 | }
935 |
936 | // If the scope is a scope object, we will call the apply method on this scope
937 | // passing in the builder and the model instance. After we run all of these
938 | // scopes we will return back the builder instance to the outside caller.
939 | if ($scope instanceof Scope) {
940 | throw new NotSupportedException('Scope object is not yet supported');
941 | }
942 | });
943 |
944 | $builder->withoutGlobalScope($identifier);
945 | }
946 |
947 | return $builder;
948 | }
949 |
950 | /**
951 | * Apply the given scope on the current builder instance.
952 | *
953 | * @param callable $scope
954 | * @param array $parameters
955 | * @return mixed
956 | */
957 | protected function callScope(callable $scope, $parameters = [])
958 | {
959 | array_unshift($parameters, $this);
960 |
961 | // $query = $this->getQuery();
962 |
963 | // // We will keep track of how many wheres are on the query before running the
964 | // // scope so that we can properly group the added scope constraints in the
965 | // // query as their own isolated nested where statement and avoid issues.
966 | // $originalWhereCount = is_null($query->wheres)
967 | // ? 0 : count($query->wheres);
968 |
969 | $result = $scope(...array_values($parameters)) ?: $this;
970 |
971 | // if (count((array) $query->wheres) > $originalWhereCount) {
972 | // $this->addNewWheresWithinGroup($query, $originalWhereCount);
973 | // }
974 |
975 | return $result;
976 | }
977 |
978 | /**
979 | * Dynamically handle calls into the query instance.
980 | *
981 | * @param string $method
982 | * @param array $parameters
983 | * @return mixed
984 | */
985 | public function __call($method, $parameters)
986 | {
987 | if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
988 | return $this->callScope([$this->model, $scope], $parameters);
989 | }
990 |
991 | return $this;
992 | }
993 | }
994 |
--------------------------------------------------------------------------------