Completed
Pull Request — master (#128)
by Bao
05:47
created

DynamoDbQueryBuilder   F

Complexity

Total Complexity 113

Size/Duplication

Total Lines 980
Duplicated Lines 0 %

Test Coverage

Coverage 92.52%

Importance

Changes 0
Metric Value
wmc 113
dl 0
loc 980
ccs 297
cts 321
cp 0.9252
rs 1.62
c 0
b 0
f 0

47 Methods

Rating   Name   Duplication   Size   Complexity  
A limit() 0 5 1
A __construct() 0 5 1
A skip() 0 3 1
A after() 0 21 4
A take() 0 3 1
A offset() 0 3 1
A first() 0 5 1
A findOrFail() 0 15 4
A orWhereIn() 0 3 1
A withIndex() 0 4 1
A whereNotNull() 0 3 1
A removeAttribute() 0 26 3
A chunk() 0 11 4
A forNestedWhere() 0 3 1
A save() 0 8 1
A orWhereNull() 0 3 1
A all() 0 4 2
A count() 0 12 3
A decorate() 0 4 1
A whereNested() 0 5 1
A firstOrFail() 0 7 2
A delete() 0 8 1
A addNestedWhereQuery() 0 10 2
A get() 0 3 1
A findMany() 0 40 4
A find() 0 35 4
A whereNull() 0 7 2
A whereIn() 0 22 5
B where() 0 47 7
A newQuery() 0 3 1
A orWhereNotNull() 0 3 1
A afterKey() 0 4 2
A orWhere() 0 3 1
A isMultipleIds() 0 15 3
A withGlobalScope() 0 9 2
A getModel() 0 3 1
A removedScopes() 0 3 1
A callScope() 0 19 2
A __call() 0 7 2
A getConditionAnalyzer() 0 6 1
A getDynamoDbKey() 0 14 4
A withoutGlobalScopes() 0 11 3
D toDynamoDbQuery() 0 61 11
A getClient() 0 3 1
B applyScopes() 0 33 6
A withoutGlobalScope() 0 11 2
B getAll() 0 46 7

How to fix   Complexity   

Complex Class

Complex classes like DynamoDbQueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DynamoDbQueryBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace BaoPham\DynamoDb;
4
5
use BaoPham\DynamoDb\Concerns\HasParsers;
6
use BaoPham\DynamoDb\ConditionAnalyzer\Analyzer;
7
use BaoPham\DynamoDb\Facades\DynamoDb;
8
use Closure;
9
use Illuminate\Contracts\Support\Arrayable;
10
use Illuminate\Database\Eloquent\ModelNotFoundException;
11
use Illuminate\Database\Eloquent\Scope;
12
13
class DynamoDbQueryBuilder
14
{
15
    use HasParsers;
16
17
    const MAX_LIMIT = -1;
18
    const DEFAULT_TO_ITERATOR = true;
19
20
    /**
21
     * The maximum number of records to return.
22
     *
23
     * @var int
24
     */
25
    public $limit;
26
27
    /**
28
     * @var array
29
     */
30
    public $wheres = [];
31
32
    /**
33
     * @var DynamoDbModel
34
     */
35
    protected $model;
36
37
    /**
38
     * @var \Aws\DynamoDb\DynamoDbClient
39
     */
40
    protected $client;
41
42
    /**
43
     * @var Closure
44
     */
45
    protected $decorator;
46
47
    /**
48
     * Applied global scopes.
49
     *
50
     * @var array
51
     */
52
    protected $scopes = [];
53
54
    /**
55
     * Removed global scopes.
56
     *
57
     * @var array
58
     */
59
    protected $removedScopes = [];
60
61
    /**
62
     * When not using the iterator, you can store the lastEvaluatedKey to
63
     * paginate through the results. The getAll method will take this into account
64
     * when used with $use_iterator = false.
65
     *
66
     * @var mixed
67
     */
68
    protected $lastEvaluatedKey;
69
70
    /**
71
     * Specified index name for the query.
72
     *
73
     * @var string
74
     */
75
    protected $index;
76
77 119
    public function __construct(DynamoDbModel $model)
78
    {
79 119
        $this->model = $model;
80 119
        $this->client = $model->getClient();
81 119
        $this->setupExpressions();
82 119
    }
83
84
    /**
85
     * Alias to set the "limit" value of the query.
86
     *
87
     * @param  int  $value
88
     * @return DynamoDbQueryBuilder
89
     */
90 6
    public function take($value)
91
    {
92 6
        return $this->limit($value);
93
    }
94
95
    /**
96
     * Set the "limit" value of the query.
97
     *
98
     * @param  int  $value
99
     * @return $this
100
     */
101 14
    public function limit($value)
102
    {
103 14
        $this->limit = $value;
104
105 14
        return $this;
106
    }
107
108
    /**
109
     * Alias to set the "offset" value of the query.
110
     *
111
     * @param  int $value
112
     * @throws NotSupportedException
113
     */
114
    public function skip($value)
115
    {
116
        return $this->offset($value);
117
    }
118
119
    /**
120
     * Set the "offset" value of the query.
121
     *
122
     * @param  int $value
123
     * @throws NotSupportedException
124
     */
125
    public function offset($value)
126
    {
127
        throw new NotSupportedException('Skip/Offset is not supported. Consider using after() instead');
128
    }
129
130
    /**
131
     * Determine the starting point (exclusively) of the query.
132
     * Unfortunately, offset of how many records to skip does not make sense for DynamoDb.
133
     * Instead, provide the last result of the previous query as the starting point for the next query.
134
     *
135
     * @param  DynamoDbModel|null  $after
136
     *   Examples:
137
     *
138
     *   For query such as
139
     *       $query = $model->where('count', 10)->limit(2);
140
     *       $last = $query->all()->last();
141
     *   Take the last item of this query result as the next "offset":
142
     *       $nextPage = $query->after($last)->limit(2)->all();
143
     *
144
     *   Alternatively, pass in nothing to reset the starting point.
145
     *
146
     * @return $this
147
     */
148 4
    public function after(DynamoDbModel $after = null)
149
    {
150 4
        if (empty($after)) {
151 4
            $this->lastEvaluatedKey = null;
152
153 4
            return $this;
154
        }
155
156 4
        $afterKey = $after->getKeys();
157
158 4
        $analyzer = $this->getConditionAnalyzer();
159
160 4
        if ($index = $analyzer->index()) {
161 1
            foreach ($index->columns() as $column) {
162 1
                $afterKey[$column] = $after->getAttribute($column);
163
            }
164
        }
165
166 4
        $this->lastEvaluatedKey = $this->getDynamoDbKey($afterKey);
167
168 4
        return $this;
169
    }
170
171
    /**
172
     * Similar to after(), but instead of using the model instance, the model's keys are used.
173
     * Use $items->lastKey() to retrieve the value
174
     *
175
     * @param  Array  $key
176
     *   Examples:
177
     *
178
     *   For query such as
179
     *       $query = $model->where('count', 10)->limit(2);
180
     *       $items = $query->all();
181
     *   Take the last item of this query result as the next "offset":
182
     *       $nextPage = $query->afterKey($items->lastKey())->limit(2)->all();
183
     *
184
     *   Alternatively, pass in nothing to reset the starting point.
185
     *
186
     * @return $this
187
     */
188 4
    public function afterKey($key = null)
189
    {
190 4
        $this->lastEvaluatedKey = empty($key) ? null : DynamoDb::marshalItem($key);
191 4
        return $this;
192
    }
193
194
    /**
195
     * Set the index name manually
196
     *
197
     * @param string $index The index name
198
     * @return $this
199
     */
200 1
    public function withIndex($index)
201
    {
202 1
        $this->index = $index;
203 1
        return $this;
204
    }
205
206 74
    public function where($column, $operator = null, $value = null, $boolean = 'and')
207
    {
208
        // If the column is an array, we will assume it is an array of key-value pairs
209
        // and can add them each as a where clause. We will maintain the boolean we
210
        // received when the method was called and pass it into the nested where.
211 74
        if (is_array($column)) {
212
            foreach ($column as $key => $value) {
213
                return $this->where($key, '=', $value);
214
            }
215
        }
216
217
        // Here we will make some assumptions about the operator. If only 2 values are
218
        // passed to the method, we will assume that the operator is an equals sign
219
        // and keep going. Otherwise, we'll require the operator to be passed in.
220 74
        if (func_num_args() == 2) {
221 44
            list($value, $operator) = [$operator, '='];
222
        }
223
224
        // If the columns is actually a Closure instance, we will assume the developer
225
        // wants to begin a nested where statement which is wrapped in parenthesis.
226
        // We'll add that Closure to the query then return back out immediately.
227 74
        if ($column instanceof Closure) {
228 2
            return $this->whereNested($column, $boolean);
229
        }
230
231
        // If the given operator is not found in the list of valid operators we will
232
        // assume that the developer is just short-cutting the '=' operators and
233
        // we will set the operators to '=' and set the values appropriately.
234 74
        if (!ComparisonOperator::isValidOperator($operator)) {
235 4
            list($value, $operator) = [$operator, '='];
236
        }
237
238
        // If the value is a Closure, it means the developer is performing an entire
239
        // sub-select within the query and we will need to compile the sub-select
240
        // within the where clause to get the appropriate query record results.
241 74
        if ($value instanceof Closure) {
242
            throw new NotSupportedException('Closure in where clause is not supported');
243
        }
244
245 74
        $this->wheres[] = [
246 74
            'column' => $column,
247 74
            'type' => ComparisonOperator::getDynamoDbOperator($operator),
248 74
            'value' => $value,
249 74
            'boolean' => $boolean,
250
        ];
251
252 74
        return $this;
253
    }
254
255
    /**
256
     * Add a nested where statement to the query.
257
     *
258
     * @param  \Closure $callback
259
     * @param  string   $boolean
260
     * @return $this
261
     */
262 2
    public function whereNested(Closure $callback, $boolean = 'and')
263
    {
264 2
        call_user_func($callback, $query = $this->forNestedWhere());
265
266 2
        return $this->addNestedWhereQuery($query, $boolean);
267
    }
268
269
    /**
270
     * Create a new query instance for nested where condition.
271
     *
272
     * @return $this
273
     */
274 2
    public function forNestedWhere()
275
    {
276 2
        return $this->newQuery();
277
    }
278
279
    /**
280
     * Add another query builder as a nested where to the query builder.
281
     *
282
     * @param  DynamoDbQueryBuilder $query
283
     * @param  string  $boolean
284
     * @return $this
285
     */
286 2
    public function addNestedWhereQuery($query, $boolean = 'and')
287
    {
288 2
        if (count($query->wheres)) {
289 2
            $type = 'Nested';
290 2
            $column = null;
291 2
            $value = $query->wheres;
292 2
            $this->wheres[] = compact('column', 'type', 'value', 'boolean');
293
        }
294
295 2
        return $this;
296
    }
297
298
    /**
299
     * Add an "or where" clause to the query.
300
     *
301
     * @param  string  $column
302
     * @param  string  $operator
303
     * @param  mixed   $value
304
     * @return $this
305
     */
306 22
    public function orWhere($column, $operator = null, $value = null)
307
    {
308 22
        return $this->where($column, $operator, $value, 'or');
309
    }
310
311
    /**
312
     * Add a "where in" clause to the query.
313
     *
314
     * @param  string  $column
315
     * @param  mixed   $values
316
     * @param  string  $boolean
317
     * @param  bool    $not
318
     * @return $this
319
     * @throws NotSupportedException
320
     */
321 2
    public function whereIn($column, $values, $boolean = 'and', $not = false)
322
    {
323 2
        if ($not) {
324
            throw new NotSupportedException('"not in" is not a valid DynamoDB comparison operator');
325
        }
326
327
        // If the value is a query builder instance, not supported
328 2
        if ($values instanceof static) {
329
            throw new NotSupportedException('Value is a query builder instance');
330
        }
331
332
        // If the value of the where in clause is actually a Closure, not supported
333 2
        if ($values instanceof Closure) {
334
            throw new NotSupportedException('Value is a Closure');
335
        }
336
337
        // Next, if the value is Arrayable we need to cast it to its raw array form
338 2
        if ($values instanceof Arrayable) {
339
            $values = $values->toArray();
340
        }
341
342 2
        return $this->where($column, ComparisonOperator::IN, $values, $boolean);
343
    }
344
345
    /**
346
     * Add an "or where in" clause to the query.
347
     *
348
     * @param  string  $column
349
     * @param  mixed   $values
350
     * @return $this
351
     */
352 2
    public function orWhereIn($column, $values)
353
    {
354 2
        return $this->whereIn($column, $values, 'or');
355
    }
356
357
    /**
358
     * Add a "where null" clause to the query.
359
     *
360
     * @param  string  $column
361
     * @param  string  $boolean
362
     * @param  bool    $not
363
     * @return $this
364
     */
365 4
    public function whereNull($column, $boolean = 'and', $not = false)
366
    {
367 4
        $type = $not ? ComparisonOperator::NOT_NULL : ComparisonOperator::NULL;
368
369 4
        $this->wheres[] = compact('column', 'type', 'boolean');
370
371 4
        return $this;
372
    }
373
374
    /**
375
     * Add an "or where null" clause to the query.
376
     *
377
     * @param  string  $column
378
     * @return $this
379
     */
380 2
    public function orWhereNull($column)
381
    {
382 2
        return $this->whereNull($column, 'or');
383
    }
384
385
    /**
386
     * Add an "or where not null" clause to the query.
387
     *
388
     * @param  string  $column
389
     * @return $this
390
     */
391 2
    public function orWhereNotNull($column)
392
    {
393 2
        return $this->whereNotNull($column, 'or');
394
    }
395
396
    /**
397
     * Add a "where not null" clause to the query.
398
     *
399
     * @param  string  $column
400
     * @param  string  $boolean
401
     * @return $this
402
     */
403 2
    public function whereNotNull($column, $boolean = 'and')
404
    {
405 2
        return $this->whereNull($column, $boolean, true);
406
    }
407
408
    /**
409
     * Get a new instance of the query builder.
410
     *
411
     * @return DynamoDbQueryBuilder
412
     */
413 2
    public function newQuery()
414
    {
415 2
        return new static($this->getModel());
416
    }
417
418
    /**
419
     * Implements the Query Chunk method
420
     *
421
     * @param int $chunkSize
422
     * @param callable $callback
423
     */
424 7
    public function chunk($chunkSize, callable $callback)
425
    {
426 7
        while (true) {
427 7
            $results = $this->getAll([], $chunkSize, false);
428
429 7
            if ($results->isNotEmpty()) {
430 7
                call_user_func($callback, $results);
431
            }
432
433 7
            if (empty($this->lastEvaluatedKey)) {
434 7
                break;
435
            }
436
        }
437 7
    }
438
439
    /**
440
     * @param $id
441
     * @param array $columns
442
     * @return DynamoDbModel|\Illuminate\Database\Eloquent\Collection|null
443
     */
444 34
    public function find($id, array $columns = [])
445
    {
446 34
        if ($this->isMultipleIds($id)) {
447 4
            return $this->findMany($id, $columns);
448
        }
449
450 30
        $this->resetExpressions();
451
452 30
        $this->model->setId($id);
453
454 30
        $query = DynamoDb::table($this->model->getTable())
455 30
            ->setKey($this->getDynamoDbKey())
456 30
            ->setConsistentRead(true);
457
458 30
        if (!empty($columns)) {
459
            $query
460 3
                ->setProjectionExpression($this->projectionExpression->parse($columns))
461 3
                ->setExpressionAttributeNames($this->expressionAttributeNames->all());
462
        }
463
464 30
        $item = $query->prepare($this->client)->getItem();
465
466 30
        $item = array_get($item->toArray(), 'Item');
467
468 30
        if (empty($item)) {
469 4
            return null;
470
        }
471
472 26
        $item = DynamoDb::unmarshalItem($item);
473
474 26
        $model = $this->model->newInstance([], true);
475
476 26
        $model->setRawAttributes($item, true);
477
478 26
        return $model;
479
    }
480
481
    /**
482
     * @param $ids
483
     * @param array $columns
484
     * @return \Illuminate\Database\Eloquent\Collection
485
     */
486 4
    public function findMany($ids, array $columns = [])
487
    {
488 4
        $collection = $this->model->newCollection();
489
490 4
        if (empty($ids)) {
491
            return $collection;
492
        }
493
494 4
        $this->resetExpressions();
495
496 4
        $table = $this->model->getTable();
497
498
        $keys = collect($ids)->map(function ($id) {
499 4
            if (! is_array($id)) {
500 2
                $id = [$this->model->getKeyName() => $id];
501
            }
502
503 4
            return DynamoDb::marshalItem($id);
504 4
        });
505
506 4
        $subQuery = DynamoDb::newQuery()
507 4
            ->setKeys($keys->toArray())
508 4
            ->setProjectionExpression($this->projectionExpression->parse($columns))
509 4
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
510 4
            ->prepare($this->client)
511 4
            ->query;
512
513 4
        $results = DynamoDb::newQuery()
514 4
            ->setRequestItems([$table => $subQuery])
515 4
            ->prepare($this->client)
516 4
            ->batchGetItem();
517
518 4
        foreach ($results['Responses'][$table] as $item) {
519 4
            $item = DynamoDb::unmarshalItem($item);
520 4
            $model = $this->model->newInstance([], true);
521 4
            $model->setRawAttributes($item, true);
522 4
            $collection->add($model);
523
        }
524
525 4
        return $collection;
526
    }
527
528 5
    public function findOrFail($id, $columns = [])
529
    {
530 5
        $result = $this->find($id, $columns);
531
532 5
        if ($this->isMultipleIds($id)) {
533 1
            if (count($result) == count(array_unique($id))) {
534 1
                return $result;
535
            }
536 4
        } elseif (! is_null($result)) {
537 2
            return $result;
538
        }
539
540 2
        throw (new ModelNotFoundException)->setModel(
541 2
            get_class($this->model),
542 2
            $id
543
        );
544
    }
545
546 16
    public function first($columns = [])
547
    {
548 16
        $items = $this->getAll($columns, 1);
549
550 16
        return $items->first();
551
    }
552
553 6
    public function firstOrFail($columns = [])
554
    {
555 6
        if (! is_null($model = $this->first($columns))) {
556 4
            return $model;
557
        }
558
559 2
        throw (new ModelNotFoundException)->setModel(get_class($this->model));
560
    }
561
562
    /**
563
     * Remove attributes from an existing item
564
     *
565
     * @param array ...$attributes
566
     * @return bool
567
     * @throws InvalidQuery
568
     */
569 6
    public function removeAttribute(...$attributes)
570
    {
571 6
        $key = $this->getDynamoDbKey();
572
573 6
        if (empty($key)) {
574 4
            $analyzer = $this->getConditionAnalyzer();
575
576 4
            if (!$analyzer->isExactSearch()) {
577
                throw new InvalidQuery('Need to provide the key in your query');
578
            }
579
580 4
            $id = $analyzer->identifierConditionValues();
581 4
            $this->model->setId($id);
582 4
            $key = $this->getDynamoDbKey();
583
        }
584
585 6
        $this->resetExpressions();
586
587 6
        $result = DynamoDb::table($this->model->getTable())
588 6
            ->setKey($key)
589 6
            ->setUpdateExpression($this->updateExpression->remove($attributes))
590 6
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
591 6
            ->prepare($this->client)
592 6
            ->updateItem();
593
594 6
        return array_get($result, '@metadata.statusCode') === 200;
595
    }
596
597 2
    public function delete()
598
    {
599 2
        $result = DynamoDb::table($this->model->getTable())
600 2
            ->setKey($this->getDynamoDbKey())
601 2
            ->prepare($this->client)
602 2
            ->deleteItem();
603
604 2
        return array_get($result->toArray(), '@metadata.statusCode') === 200;
605
    }
606
607 8
    public function save()
608
    {
609 8
        $result = DynamoDb::table($this->model->getTable())
610 8
            ->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
611 8
            ->prepare($this->client)
612 8
            ->putItem();
613
614 8
        return array_get($result, '@metadata.statusCode') === 200;
615
    }
616
617 55
    public function get($columns = [])
618
    {
619 55
        return $this->all($columns);
620
    }
621
622 67
    public function all($columns = [])
623
    {
624 67
        $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
625 67
        return $this->getAll($columns, $limit, !isset($this->limit));
626
    }
627
628 4
    public function count()
629
    {
630 4
        $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
631 4
        $raw = $this->toDynamoDbQuery(['count(*)'], $limit);
632
633 4
        if ($raw->op === 'Scan') {
634 4
            $res = $this->client->scan($raw->query);
635
        } else {
636
            $res = $this->client->query($raw->query);
637
        }
638
639 4
        return $res['Count'];
640
    }
641
642 4
    public function decorate(Closure $closure)
643
    {
644 4
        $this->decorator = $closure;
645 4
        return $this;
646
    }
647
648 86
    protected function getAll(
649
        $columns = [],
650
        $limit = DynamoDbQueryBuilder::MAX_LIMIT,
651
        $useIterator = DynamoDbQueryBuilder::DEFAULT_TO_ITERATOR
652
    ) {
653 86
        $analyzer = $this->getConditionAnalyzer();
654
655 86
        if ($analyzer->isExactSearch()) {
656 7
            $item = $this->find($analyzer->identifierConditionValues(), $columns);
657
658 7
            return $this->getModel()->newCollection([$item]);
659
        }
660
661 80
        $raw = $this->toDynamoDbQuery($columns, $limit);
662
663 80
        if ($useIterator) {
664 65
            $iterator = $this->client->getIterator($raw->op, $raw->query);
665
666 65
            if (isset($raw->query['Limit'])) {
667 12
                $iterator = new \LimitIterator($iterator, 0, $raw->query['Limit']);
668
            }
669
        } else {
670 19
            if ($raw->op === 'Scan') {
671 17
                $res = $this->client->scan($raw->query);
672
            } else {
673 2
                $res = $this->client->query($raw->query);
674
            }
675
676 19
            $this->lastEvaluatedKey = array_get($res, 'LastEvaluatedKey');
677 19
            $iterator = $res['Items'];
678
        }
679
680 80
        $results = [];
681
682 80
        foreach ($iterator as $item) {
683 80
            $item = DynamoDb::unmarshalItem($item);
684 80
            $model = $this->model->newInstance([], true);
685 80
            $model->setRawAttributes($item, true);
686 80
            $results[] = $model;
687
        }
688
689 80
        $conditionIndex = $analyzer->index();
690
691 80
        return $this->getModel()->newCollection(
692 80
            $results,
693 80
            $conditionIndex ? $conditionIndex->columns() : null
694
        );
695
    }
696
697
    /**
698
     * Return the raw DynamoDb query
699
     *
700
     * @param array $columns
701
     * @param int $limit
702
     * @return RawDynamoDbQuery
703
     */
704 90
    public function toDynamoDbQuery(
705
        $columns = [],
706
        $limit = DynamoDbQueryBuilder::MAX_LIMIT
707
    ) {
708 90
        $this->applyScopes();
709
710 90
        $this->resetExpressions();
711
712 90
        $op = 'Scan';
713 90
        $queryBuilder = DynamoDb::table($this->model->getTable());
714
715 90
        if (! empty($this->wheres)) {
716 70
            $analyzer = $this->getConditionAnalyzer();
717
718 70
            if ($keyConditions = $analyzer->keyConditions()) {
719 15
                $op = 'Query';
720 15
                $queryBuilder->setKeyConditionExpression($this->keyConditionExpression->parse($keyConditions));
721
            }
722
723 70
            if ($filterConditions = $analyzer->filterConditions()) {
724 62
                $queryBuilder->setFilterExpression($this->filterExpression->parse($filterConditions));
725
            }
726
727 70
            if ($index = $analyzer->index()) {
728 8
                $queryBuilder->setIndexName($index->name);
729
            }
730
        }
731
732 90
        if ($this->index) {
733
            // If user specifies the index manually, respect that
734 1
            $queryBuilder->setIndexName($this->index);
735
        }
736
737 90
        if ($limit !== static::MAX_LIMIT) {
738 31
            $queryBuilder->setLimit($limit);
739
        }
740
741 90
        if (!empty($columns)) {
742
            // Either we try to get the count or specific columns
743 8
            if ($columns == ['count(*)']) {
744 6
                $queryBuilder->setSelect('COUNT');
745
            } else {
746 2
                $queryBuilder->setProjectionExpression($this->projectionExpression->parse($columns));
747
            }
748
        }
749
750 90
        if (!empty($this->lastEvaluatedKey)) {
751 15
            $queryBuilder->setExclusiveStartKey($this->lastEvaluatedKey);
752
        }
753
754
        $queryBuilder
755 90
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
756 90
            ->setExpressionAttributeValues($this->expressionAttributeValues->all());
757
758 90
        $raw = new RawDynamoDbQuery($op, $queryBuilder->prepare($this->client)->query);
759
760 90
        if ($this->decorator) {
761 4
            call_user_func($this->decorator, $raw);
762
        }
763
764 90
        return $raw;
765
    }
766
767
    /**
768
     * @return Analyzer
769
     */
770 95
    protected function getConditionAnalyzer()
771
    {
772 95
        return with(new Analyzer)
773 95
            ->on($this->model)
774 95
            ->withIndex($this->index)
775 95
            ->analyze($this->wheres);
776
    }
777
778
    /**
779
     * Return key for DynamoDb query.
780
     *
781
     * @param array|null $modelKeys
782
     * @return array
783
     *
784
     * e.g.
785
     * [
786
     *   'id' => ['S' => 'foo'],
787
     * ]
788
     *
789
     * or
790
     *
791
     * [
792
     *   'id' => ['S' => 'foo'],
793
     *   'id2' => ['S' => 'bar'],
794
     * ]
795
     */
796 36
    protected function getDynamoDbKey($modelKeys = null)
797
    {
798 36
        $modelKeys = $modelKeys ?: $this->model->getKeys();
799
800 36
        $keys = [];
801
802 36
        foreach ($modelKeys as $key => $value) {
803 36
            if (is_null($value)) {
804 4
                continue;
805
            }
806 36
            $keys[$key] = DynamoDb::marshalValue($value);
807
        }
808
809 36
        return $keys;
810
    }
811
812 34
    protected function isMultipleIds($id)
813
    {
814 34
        $keys = collect($this->model->getKeyNames());
815
816
        // could be ['id' => 'foo'], ['id1' => 'foo', 'id2' => 'bar']
817
        $single = $keys->first(function ($name) use ($id) {
818 34
            return !isset($id[$name]);
819 34
        }) === null;
820
821 34
        if ($single) {
822 19
            return false;
823
        }
824
825
        // could be ['foo', 'bar'], [['id1' => 'foo', 'id2' => 'bar'], ...]
826 15
        return $this->model->hasCompositeKey() ? is_array(array_first($id)) : is_array($id);
827
    }
828
829
    /**
830
     * @return DynamoDbModel
831
     */
832 86
    public function getModel()
833
    {
834 86
        return $this->model;
835
    }
836
837
    /**
838
     * @return \Aws\DynamoDb\DynamoDbClient
839
     */
840
    public function getClient()
841
    {
842
        return $this->client;
843
    }
844
845
    /**
846
     * Register a new global scope.
847
     *
848
     * @param  string  $identifier
849
     * @param  \Illuminate\Database\Eloquent\Scope|\Closure  $scope
850
     * @return $this
851
     */
852 7
    public function withGlobalScope($identifier, $scope)
853
    {
854 7
        $this->scopes[$identifier] = $scope;
855
856 7
        if (method_exists($scope, 'extend')) {
857
            $scope->extend($this);
858
        }
859
860 7
        return $this;
861
    }
862
863
    /**
864
     * Remove a registered global scope.
865
     *
866
     * @param  \Illuminate\Database\Eloquent\Scope|string  $scope
867
     * @return $this
868
     */
869 3
    public function withoutGlobalScope($scope)
870
    {
871 3
        if (! is_string($scope)) {
872
            $scope = get_class($scope);
873
        }
874
875 3
        unset($this->scopes[$scope]);
876
877 3
        $this->removedScopes[] = $scope;
878
879 3
        return $this;
880
    }
881
882
    /**
883
     * Remove all or passed registered global scopes.
884
     *
885
     * @param  array|null  $scopes
886
     * @return $this
887
     */
888 5
    public function withoutGlobalScopes(array $scopes = null)
889
    {
890 5
        if (is_array($scopes)) {
891
            foreach ($scopes as $scope) {
892
                $this->withoutGlobalScope($scope);
893
            }
894
        } else {
895 5
            $this->scopes = [];
896
        }
897
898 5
        return $this;
899
    }
900
901
    /**
902
     * Get an array of global scopes that were removed from the query.
903
     *
904
     * @return array
905
     */
906
    public function removedScopes()
907
    {
908
        return $this->removedScopes;
909
    }
910
911
    /**
912
     * Apply the scopes to the Eloquent builder instance and return it.
913
     *
914
     * @return DynamoDbQueryBuilder
915
     */
916 90
    public function applyScopes()
917
    {
918 90
        if (! $this->scopes) {
919 89
            return $this;
920
        }
921
922 3
        $builder = $this;
923
924 3
        foreach ($builder->scopes as $identifier => $scope) {
925 3
            if (! isset($builder->scopes[$identifier])) {
926
                continue;
927
            }
928
929
            $builder->callScope(function (DynamoDbQueryBuilder $builder) use ($scope) {
930
                // If the scope is a Closure we will just go ahead and call the scope with the
931
                // builder instance. The "callScope" method will properly group the clauses
932
                // that are added to this query so "where" clauses maintain proper logic.
933 3
                if ($scope instanceof Closure) {
934 3
                    $scope($builder);
935
                }
936
937
                // If the scope is a scope object, we will call the apply method on this scope
938
                // passing in the builder and the model instance. After we run all of these
939
                // scopes we will return back the builder instance to the outside caller.
940 3
                if ($scope instanceof Scope) {
941
                    throw new NotSupportedException('Scope object is not yet supported');
942
                }
943 3
            });
944
945 3
            $builder->withoutGlobalScope($identifier);
946
        }
947
948 3
        return $builder;
949
    }
950
951
    /**
952
     * Apply the given scope on the current builder instance.
953
     *
954
     * @param  callable  $scope
955
     * @param  array  $parameters
956
     * @return mixed
957
     */
958 7
    protected function callScope(callable $scope, $parameters = [])
959
    {
960 7
        array_unshift($parameters, $this);
961
962
        // $query = $this->getQuery();
963
964
        // // We will keep track of how many wheres are on the query before running the
965
        // // scope so that we can properly group the added scope constraints in the
966
        // // query as their own isolated nested where statement and avoid issues.
967
        // $originalWhereCount = is_null($query->wheres)
968
        //             ? 0 : count($query->wheres);
969
970 7
        $result = $scope(...array_values($parameters)) ?: $this;
971
972
        // if (count((array) $query->wheres) > $originalWhereCount) {
973
        //     $this->addNewWheresWithinGroup($query, $originalWhereCount);
974
        // }
975
976 7
        return $result;
977
    }
978
979
    /**
980
     * Dynamically handle calls into the query instance.
981
     *
982
     * @param  string  $method
983
     * @param  array  $parameters
984
     * @return mixed
985
     */
986 7
    public function __call($method, $parameters)
987
    {
988 7
        if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
989 5
            return $this->callScope([$this->model, $scope], $parameters);
990
        }
991
992 2
        return $this;
993
    }
994
}
995