Skip to content

Commit 3e07acf

Browse files
authored
Merge pull request #119 from baopham/bao/query-decorator
Add RawDynamoDbQuery class and ability to decorate query
2 parents 9b0b1f5 + 0948287 commit 3e07acf

File tree

5 files changed

+252
-21
lines changed

5 files changed

+252
-21
lines changed

README.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ Supports all key types - primary hash key and composite keys.
2727
* [findOrFail()](#findorfail)
2828
* [Query scope](#query-scope)
2929
* [REMOVE — Deleting Attributes From An Item](#remove--deleting-attributes-from-an-item)
30-
* [toSql() style](#tosql-style)
30+
* [toSql() Style](#tosql-style)
31+
* [Decorate Query](#decorate-query)
3132
* [Indexes](#indexes)
3233
* [Composite Keys](#composite-keys)
3334
* [Requirements](#requirements)
@@ -258,17 +259,49 @@ Model::find('foo')->removeAttribute('name', 'description', 'nested.foo', 'nested
258259
```
259260
260261
261-
#### toSql() style
262+
#### toSql() Style
262263
263264
For debugging purposes, you can choose to convert to the actual DynamoDb query
264265
265266
```php
266-
list($op, $query) = $model->where('count', '>', 10)->toDynamoDbQuery();
267+
$raw = $model->where('count', '>', 10)->toDynamoDbQuery();
268+
// $op is either "Scan" or "Query"
269+
$op = $raw->op;
270+
// The query body being sent to AWS
271+
$query = $raw->query;
267272
```
268273
269-
where `$op` will be either `Scan` or `Query` and `$query` will be the query body being sent to AWS.
274+
where `$raw` is an instance of [RawDynamoDbQuery](./src/RawDynamoDbQuery.php)
270275
271276
277+
#### Decorate Query
278+
279+
Use `decorate` when you want to enhance the query. For example:
280+
281+
To set the order of the sort key:
282+
283+
```php
284+
$items = $model
285+
->where('hash', 'hash-value')
286+
->where('range', '>', 10)
287+
->decorate(function (RawDynamoDbQuery $raw) {
288+
// desc order
289+
$raw->query['ScanIndexForward'] = false;
290+
})
291+
->get();
292+
```
293+
294+
To force to use "Query" instead of "Scan" if the library fails to detect the correct operation:
295+
296+
```php
297+
$items = $model
298+
->where('hash', 'hash-value')
299+
->decorate(function (RawDynamoDbQuery $raw) {
300+
$raw->op = 'Query';
301+
})
302+
->get();
303+
```
304+
272305
Indexes
273306
-----------
274307
If your table has indexes, make sure to declare them in your model class like so

src/DynamoDbQueryBuilder.php

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ class DynamoDbQueryBuilder
3838
*/
3939
protected $client;
4040

41+
/**
42+
* @var Closure
43+
*/
44+
protected $decorator;
45+
4146
/**
4247
* Applied global scopes.
4348
*
@@ -557,6 +562,12 @@ public function count()
557562
return $this->getAll($this->model->getKeyNames())->count();
558563
}
559564

565+
public function decorate(Closure $closure)
566+
{
567+
$this->decorator = $closure;
568+
return $this;
569+
}
570+
560571
protected function getAll(
561572
$columns = [],
562573
$limit = DynamoDbQueryBuilder::MAX_LIMIT,
@@ -570,19 +581,23 @@ protected function getAll(
570581
}
571582
}
572583

573-
list($op, $query) = $this->toDynamoDbQuery($columns, $limit);
584+
$raw = $this->toDynamoDbQuery($columns, $limit);
585+
586+
if ($this->decorator) {
587+
call_user_func($this->decorator, $raw);
588+
}
574589

575590
if ($useIterator) {
576-
$iterator = $this->client->getIterator($op, $query);
591+
$iterator = $this->client->getIterator($raw->op, $raw->query);
577592

578-
if (isset($query['Limit'])) {
579-
$iterator = new \LimitIterator($iterator, 0, $query['Limit']);
593+
if (isset($raw->query['Limit'])) {
594+
$iterator = new \LimitIterator($iterator, 0, $raw->query['Limit']);
580595
}
581596
} else {
582-
if ($op === 'Scan') {
583-
$res = $this->client->scan($query);
597+
if ($raw->op === 'Scan') {
598+
$res = $this->client->scan($raw->query);
584599
} else {
585-
$res = $this->client->query($query);
600+
$res = $this->client->query($raw->query);
586601
}
587602

588603
$this->lastEvaluatedKey = array_get($res, 'LastEvaluatedKey');
@@ -601,13 +616,22 @@ protected function getAll(
601616
return $this->getModel()->newCollection($results);
602617
}
603618

619+
/**
620+
* Return the raw DynamoDb query
621+
*
622+
* @param array $columns
623+
* @param int $limit
624+
* @return RawDynamoDbQuery
625+
*/
604626
public function toDynamoDbQuery(
605627
$columns = [],
606628
$limit = DynamoDbQueryBuilder::MAX_LIMIT
607629
) {
608630
$this->applyScopes();
609631

610-
list($op, $query) = $this->buildExpressionQuery();
632+
$raw = $this->buildExpressionQuery();
633+
634+
$query = $raw->query;
611635

612636
$query['TableName'] = $this->model->getTable();
613637

@@ -623,8 +647,8 @@ public function toDynamoDbQuery(
623647
$query['ExclusiveStartKey'] = $this->lastEvaluatedKey;
624648
}
625649

626-
$query = $this->cleanUpQuery($query);
627-
return [$op, $query];
650+
$raw->query = $this->cleanUpQuery($query);
651+
return $raw;
628652
}
629653

630654
protected function buildExpressionQuery()
@@ -635,7 +659,7 @@ protected function buildExpressionQuery()
635659
$query = [];
636660

637661
if (empty($this->wheres)) {
638-
return [$op, $query];
662+
return new RawDynamoDbQuery($op, $query);
639663
}
640664

641665
// Index key condition exists, then use Query instead of Scan.
@@ -695,7 +719,7 @@ protected function buildExpressionQuery()
695719

696720
$query['ExpressionAttributeValues'] = $this->expressionAttributeValues->all();
697721

698-
return [$op, $query];
722+
return new RawDynamoDbQuery($op, $query);
699723
}
700724

701725
protected function conditionsAreExactSearch()

src/RawDynamoDbQuery.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
namespace BaoPham\DynamoDb;
4+
5+
/**
6+
* Class RawDynamoDbQuery
7+
*
8+
* @package BaoPham\DynamoDb
9+
*/
10+
class RawDynamoDbQuery implements \IteratorAggregate, \ArrayAccess, \Countable
11+
{
12+
/**
13+
* Either 'Scan', or 'Query'
14+
*
15+
* @var string
16+
*/
17+
public $op;
18+
19+
/**
20+
* The query body being sent to AWS
21+
*
22+
* @var array
23+
*/
24+
public $query;
25+
26+
/**
27+
* For backward compatibility, previously we use array to represent the raw query
28+
*
29+
* @var array
30+
*/
31+
private $array;
32+
33+
public function __construct($op, $query)
34+
{
35+
$this->op = $op;
36+
$this->query = $query;
37+
$this->array = [$op, $query];
38+
}
39+
40+
/**
41+
* Whether a offset exists
42+
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
43+
* @param mixed $offset <p>
44+
* An offset to check for.
45+
* </p>
46+
* @return boolean true on success or false on failure.
47+
* </p>
48+
* <p>
49+
* The return value will be casted to boolean if non-boolean was returned.
50+
* @since 5.0.0
51+
*/
52+
public function offsetExists($offset)
53+
{
54+
return isset($this->array[$offset]);
55+
}
56+
57+
/**
58+
* Offset to retrieve
59+
* @link http://php.net/manual/en/arrayaccess.offsetget.php
60+
* @param mixed $offset <p>
61+
* The offset to retrieve.
62+
* </p>
63+
* @return mixed Can return all value types.
64+
* @since 5.0.0
65+
*/
66+
public function offsetGet($offset)
67+
{
68+
return $this->array[$offset];
69+
}
70+
71+
/**
72+
* Offset to set
73+
* @link http://php.net/manual/en/arrayaccess.offsetset.php
74+
* @param mixed $offset <p>
75+
* The offset to assign the value to.
76+
* </p>
77+
* @param mixed $value <p>
78+
* The value to set.
79+
* </p>
80+
* @return void
81+
* @since 5.0.0
82+
*/
83+
public function offsetSet($offset, $value)
84+
{
85+
$this->array[$offset] = $value;
86+
}
87+
88+
/**
89+
* Offset to unset
90+
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
91+
* @param mixed $offset <p>
92+
* The offset to unset.
93+
* </p>
94+
* @return void
95+
* @since 5.0.0
96+
*/
97+
public function offsetUnset($offset)
98+
{
99+
unset($this->array[$offset]);
100+
}
101+
102+
/**
103+
* Retrieve an external iterator
104+
* @link http://php.net/manual/en/iteratoraggregate.getiterator.php
105+
* @return Traversable An instance of an object implementing <b>Iterator</b> or
106+
* <b>Traversable</b>
107+
* @since 5.0.0
108+
*/
109+
public function getIterator()
110+
{
111+
return new \ArrayObject($this->array);
112+
}
113+
114+
/**
115+
* Count elements of an object
116+
* @link http://php.net/manual/en/countable.count.php
117+
* @return int The custom count as an integer.
118+
* </p>
119+
* <p>
120+
* The return value is cast to an integer.
121+
* @since 5.1.0
122+
*/
123+
public function count()
124+
{
125+
return count($this->array);
126+
}
127+
}

tests/DynamoDbCompositeModelTest.php

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace BaoPham\DynamoDb\Tests;
44

55
use BaoPham\DynamoDb\NotSupportedException;
6+
use BaoPham\DynamoDb\RawDynamoDbQuery;
67
use \Illuminate\Database\Eloquent\ModelNotFoundException;
78

89
/**
@@ -297,14 +298,14 @@ public function testConditionContainingIndexKey()
297298

298299
public function testSetIndexManually()
299300
{
300-
list($op, $query) = $this->testModel
301+
$raw = $this->testModel
301302
->where('id', 'id1')
302303
->where('author', 'BP')
303304
->withIndex('id_author_index')
304305
->toDynamoDbQuery();
305306

306-
$this->assertEquals('id_author_index', array_get($query, 'IndexName'));
307-
$this->assertEquals('Query', $op);
307+
$this->assertEquals('id_author_index', array_get($raw->query, 'IndexName'));
308+
$this->assertEquals('Query', $raw->op);
308309
}
309310

310311
public function testConditionsNotContainingAllCompositeKeys()
@@ -384,7 +385,7 @@ public function testRemoveAttributesOnModel()
384385

385386
public function testAfterForQueryOperation()
386387
{
387-
for ($i = 0; $i < 10; $i++) {
388+
foreach (range(0, 9) as $i) {
388389
$this->seed(['count' => ['N' => $i]]);
389390
}
390391

@@ -405,6 +406,29 @@ public function testAfterForQueryOperation()
405406
$this->assertEquals(range(0, 9), $paginationResult->sort()->values()->toArray());
406407
}
407408

409+
public function testDecorateRawQuery()
410+
{
411+
foreach (range(0, 9) as $i) {
412+
$this->seed(['id' => ['S' => 'id'], 'count' => ['N' => $i]]);
413+
}
414+
415+
$query = $this->testModel
416+
->where('id', 'id')
417+
->where('count', '>=', 0);
418+
419+
$forward = $query->get();
420+
421+
$this->assertEquals(range(0, 9), $forward->pluck('count')->toArray());
422+
423+
$query->decorate(function (RawDynamoDbQuery $raw) {
424+
$raw->query['ScanIndexForward'] = false;
425+
});
426+
427+
$reverse = $query->get();
428+
429+
$this->assertEquals(range(9, 0, -1), $reverse->pluck('count')->toArray());
430+
}
431+
408432
public function seed($attributes = [], $exclude = [])
409433
{
410434
$item = [

0 commit comments

Comments
 (0)