Completed
Pull Request — master (#212)
by Alexandru
43:58 queued 01:14
created

DynamoDbQueryBuilder   F

Complexity

Total Complexity 112

Size/Duplication

Total Lines 981
Duplicated Lines 0 %

Test Coverage

Coverage 27.66%

Importance

Changes 0
Metric Value
wmc 112
eloc 301
dl 0
loc 981
ccs 91
cts 329
cp 0.2766
rs 2
c 0
b 0
f 0

48 Methods

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

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 Rennokki\DynamoDb;
4
5
use Closure;
6
use Illuminate\Contracts\Support\Arrayable;
7
use Illuminate\Database\Eloquent\ModelNotFoundException;
8
use Illuminate\Database\Eloquent\Scope;
9
use Illuminate\Support\Arr;
10
use Rennokki\DynamoDb\Concerns\HasParsers;
11
use Rennokki\DynamoDb\ConditionAnalyzer\Analyzer;
12
use Rennokki\DynamoDb\Facades\DynamoDb;
13
14
class DynamoDbQueryBuilder
15
{
16
    use HasParsers;
17
18
    const MAX_LIMIT = -1;
19
    const DEFAULT_TO_ITERATOR = true;
20
21
    /**
22
     * The maximum number of records to return.
23
     *
24
     * @var int
25
     */
26
    public $limit;
27
28
    /**
29
     * @var array
30
     */
31
    public $wheres = [];
32
33
    /**
34
     * @var DynamoDbModel
35
     */
36
    protected $model;
37
38
    /**
39
     * @var \Aws\DynamoDb\DynamoDbClient
40
     */
41
    protected $client;
42
43
    /**
44
     * @var Closure
45
     */
46
    protected $decorator;
47
48
    /**
49
     * Applied global scopes.
50
     *
51
     * @var array
52
     */
53
    protected $scopes = [];
54
55
    /**
56
     * Removed global scopes.
57
     *
58
     * @var array
59
     */
60
    protected $removedScopes = [];
61
62
    /**
63
     * When not using the iterator, you can store the lastEvaluatedKey to
64
     * paginate through the results. The getAll method will take this into account
65
     * when used with $use_iterator = false.
66
     *
67
     * @var mixed
68
     */
69
    protected $lastEvaluatedKey;
70
71
    /**
72
     * Specified index name for the query.
73
     *
74
     * @var string
75
     */
76
    protected $index;
77
78 11
    public function __construct(DynamoDbModel $model)
79
    {
80 11
        $this->model = $model;
81 11
        $this->client = $model->getClient();
82 11
        $this->setupExpressions();
83 11
    }
84
85
    /**
86
     * Alias to set the "limit" value of the query.
87
     *
88
     * @param  int  $value
89
     * @return DynamoDbQueryBuilder
90
     */
91
    public function take($value)
92
    {
93
        return $this->limit($value);
94
    }
95
96
    /**
97
     * Set the "limit" value of the query.
98
     *
99
     * @param  int  $value
100
     * @return $this
101
     */
102
    public function limit($value)
103
    {
104
        $this->limit = $value;
105
106
        return $this;
107
    }
108
109
    /**
110
     * Alias to set the "offset" value of the query.
111
     *
112
     * @param  int $value
113
     * @throws NotSupportedException
114
     */
115
    public function skip($value)
116
    {
117
        return $this->offset($value);
118
    }
119
120
    /**
121
     * Set the "offset" value of the query.
122
     *
123
     * @param  int $value
124
     * @throws NotSupportedException
125
     */
126
    public function offset($value)
127
    {
128
        throw new NotSupportedException('Skip/Offset is not supported. Consider using after() instead');
129
    }
130
131
    /**
132
     * Determine the starting point (exclusively) of the query.
133
     * Unfortunately, offset of how many records to skip does not make sense for DynamoDb.
134
     * Instead, provide the last result of the previous query as the starting point for the next query.
135
     *
136
     * @param  DynamoDbModel|null  $after
137
     *   Examples:
138
     *
139
     *   For query such as
140
     *       $query = $model->where('count', 10)->limit(2);
141
     *       $last = $query->all()->last();
142
     *   Take the last item of this query result as the next "offset":
143
     *       $nextPage = $query->after($last)->limit(2)->all();
144
     *
145
     *   Alternatively, pass in nothing to reset the starting point.
146
     *
147
     * @return $this
148
     */
149
    public function after(DynamoDbModel $after = null)
150
    {
151
        if (empty($after)) {
152
            $this->lastEvaluatedKey = null;
153
154
            return $this;
155
        }
156
157
        $afterKey = $after->getKeys();
158
159
        $analyzer = $this->getConditionAnalyzer();
160
161
        if ($index = $analyzer->index()) {
162
            foreach ($index->columns() as $column) {
163
                $afterKey[$column] = $after->getAttribute($column);
164
            }
165
        }
166
167
        $this->lastEvaluatedKey = DynamoDb::marshalItem($afterKey);
168
169
        return $this;
170
    }
171
172
    /**
173
     * Similar to after(), but instead of using the model instance, the model's keys are used.
174
     * Use $collection->lastKey() or $model->getKeys() to retrieve the value.
175
     *
176
     * @param  array  $key
177
     *   Examples:
178
     *
179
     *   For query such as
180
     *       $query = $model->where('count', 10)->limit(2);
181
     *       $items = $query->all();
182
     *   Take the last item of this query result as the next "offset":
183
     *       $nextPage = $query->afterKey($items->lastKey())->limit(2)->all();
184
     *
185
     *   Alternatively, pass in nothing to reset the starting point.
186
     *
187
     * @return $this
188
     */
189
    public function afterKey($key = null)
190
    {
191
        $this->lastEvaluatedKey = empty($key) ? null : DynamoDb::marshalItem($key);
192
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 1
    public function withIndex($index)
203
    {
204 1
        $this->index = $index;
205
206 1
        return $this;
207
    }
208
209 7
    public function where($column, $operator = null, $value = null, $boolean = 'and')
210
    {
211
        // If the column is an array, we will assume it is an array of key-value pairs
212
        // and can add them each as a where clause. We will maintain the boolean we
213
        // received when the method was called and pass it into the nested where.
214 7
        if (is_array($column)) {
215 2
            foreach ($column as $key => $value) {
216 2
                $this->where($key, '=', $value, $boolean);
217
            }
218
219 2
            return $this;
220
        }
221
222
        // Here we will make some assumptions about the operator. If only 2 values are
223
        // passed to the method, we will assume that the operator is an equals sign
224
        // and keep going. Otherwise, we'll require the operator to be passed in.
225 7
        if (func_num_args() == 2) {
226 5
            [$value, $operator] = [$operator, '='];
227
        }
228
229
        // If the columns is actually a Closure instance, we will assume the developer
230
        // wants to begin a nested where statement which is wrapped in parenthesis.
231
        // We'll add that Closure to the query then return back out immediately.
232 7
        if ($column instanceof Closure) {
233
            return $this->whereNested($column, $boolean);
234
        }
235
236
        // If the given operator is not found in the list of valid operators we will
237
        // assume that the developer is just short-cutting the '=' operators and
238
        // we will set the operators to '=' and set the values appropriately.
239 7
        if (! ComparisonOperator::isValidOperator($operator)) {
240
            [$value, $operator] = [$operator, '='];
241
        }
242
243
        // If the value is a Closure, it means the developer is performing an entire
244
        // sub-select within the query and we will need to compile the sub-select
245
        // within the where clause to get the appropriate query record results.
246 7
        if ($value instanceof Closure) {
247
            throw new NotSupportedException('Closure in where clause is not supported');
248
        }
249
250 7
        $this->wheres[] = [
251 7
            'column' => $column,
252 7
            'type' => ComparisonOperator::getDynamoDbOperator($operator),
253 7
            'value' => $value,
254 7
            'boolean' => $boolean,
255
        ];
256
257 7
        return $this;
258
    }
259
260
    /**
261
     * Add a nested where statement to the query.
262
     *
263
     * @param  \Closure $callback
264
     * @param  string   $boolean
265
     * @return $this
266
     */
267
    public function whereNested(Closure $callback, $boolean = 'and')
268
    {
269
        call_user_func($callback, $query = $this->forNestedWhere());
270
271
        return $this->addNestedWhereQuery($query, $boolean);
272
    }
273
274
    /**
275
     * Create a new query instance for nested where condition.
276
     *
277
     * @return $this
278
     */
279
    public function forNestedWhere()
280
    {
281
        return $this->newQuery();
282
    }
283
284
    /**
285
     * Add another query builder as a nested where to the query builder.
286
     *
287
     * @param  DynamoDbQueryBuilder $query
288
     * @param  string  $boolean
289
     * @return $this
290
     */
291
    public function addNestedWhereQuery($query, $boolean = 'and')
292
    {
293
        if (count($query->wheres)) {
294
            $type = 'Nested';
295
            $column = null;
296
            $value = $query->wheres;
297
            $this->wheres[] = compact('column', 'type', 'value', 'boolean');
298
        }
299
300
        return $this;
301
    }
302
303
    /**
304
     * Add an "or where" clause to the query.
305
     *
306
     * @param  string  $column
307
     * @param  string  $operator
308
     * @param  mixed   $value
309
     * @return $this
310
     */
311
    public function orWhere($column, $operator = null, $value = null)
312
    {
313
        return $this->where($column, $operator, $value, 'or');
314
    }
315
316
    /**
317
     * Add a "where in" clause to the query.
318
     *
319
     * @param  string  $column
320
     * @param  mixed   $values
321
     * @param  string  $boolean
322
     * @param  bool    $not
323
     * @return $this
324
     * @throws NotSupportedException
325
     */
326
    public function whereIn($column, $values, $boolean = 'and', $not = false)
327
    {
328
        if ($not) {
329
            throw new NotSupportedException('"not in" is not a valid DynamoDB comparison operator');
330
        }
331
332
        // If the value is a query builder instance, not supported
333
        if ($values instanceof static) {
334
            throw new NotSupportedException('Value is a query builder instance');
335
        }
336
337
        // If the value of the where in clause is actually a Closure, not supported
338
        if ($values instanceof Closure) {
339
            throw new NotSupportedException('Value is a Closure');
340
        }
341
342
        // Next, if the value is Arrayable we need to cast it to its raw array form
343
        if ($values instanceof Arrayable) {
344
            $values = $values->toArray();
345
        }
346
347
        return $this->where($column, ComparisonOperator::IN, $values, $boolean);
348
    }
349
350
    /**
351
     * Add an "or where in" clause to the query.
352
     *
353
     * @param  string  $column
354
     * @param  mixed   $values
355
     * @return $this
356
     */
357
    public function orWhereIn($column, $values)
358
    {
359
        return $this->whereIn($column, $values, 'or');
360
    }
361
362
    /**
363
     * Add a "where null" clause to the query.
364
     *
365
     * @param  string  $column
366
     * @param  string  $boolean
367
     * @param  bool    $not
368
     * @return $this
369
     */
370
    public function whereNull($column, $boolean = 'and', $not = false)
371
    {
372
        $type = $not ? ComparisonOperator::NOT_NULL : ComparisonOperator::NULL;
373
374
        $this->wheres[] = compact('column', 'type', 'boolean');
375
376
        return $this;
377
    }
378
379
    /**
380
     * Add an "or where null" clause to the query.
381
     *
382
     * @param  string  $column
383
     * @return $this
384
     */
385
    public function orWhereNull($column)
386
    {
387
        return $this->whereNull($column, 'or');
388
    }
389
390
    /**
391
     * Add an "or where not null" clause to the query.
392
     *
393
     * @param  string  $column
394
     * @return $this
395
     */
396
    public function orWhereNotNull($column)
397
    {
398
        return $this->whereNotNull($column, 'or');
399
    }
400
401
    /**
402
     * Add a "where not null" clause to the query.
403
     *
404
     * @param  string  $column
405
     * @param  string  $boolean
406
     * @return $this
407
     */
408
    public function whereNotNull($column, $boolean = 'and')
409
    {
410
        return $this->whereNull($column, $boolean, true);
411
    }
412
413
    /**
414
     * Get a new instance of the query builder.
415
     *
416
     * @return DynamoDbQueryBuilder
417
     */
418
    public function newQuery()
419
    {
420
        return new static($this->getModel());
421
    }
422
423
    /**
424
     * Implements the Query Chunk method.
425
     *
426
     * @param int $chunkSize
427
     * @param callable $callback
428
     */
429
    public function chunk($chunkSize, callable $callback)
430
    {
431
        while (true) {
432
            $results = $this->getAll([], $chunkSize, false);
433
434
            if (! $results->isEmpty()) {
435
                if (call_user_func($callback, $results) === false) {
436
                    return false;
437
                }
438
            }
439
440
            if (empty($this->lastEvaluatedKey)) {
441
                break;
442
            }
443
        }
444
445
        return true;
446
    }
447
448
    /**
449
     * @param $id
450
     * @param array $columns
451
     * @return DynamoDbModel|\Illuminate\Database\Eloquent\Collection|null
452
     */
453 4
    public function find($id, array $columns = [])
454
    {
455 4
        if ($this->isMultipleIds($id)) {
456
            return $this->findMany($id, $columns);
457
        }
458
459 4
        $this->resetExpressions();
460
461 4
        $this->model->setId($id);
462
463 4
        $query = DynamoDb::table($this->model->getTable())
464 4
            ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
465 4
            ->setConsistentRead(true);
466
467 4
        if (! empty($columns)) {
468
            $query
469
                ->setProjectionExpression($this->projectionExpression->parse($columns))
470
                ->setExpressionAttributeNames($this->expressionAttributeNames->all());
471
        }
472
473 4
        $item = $query->prepare($this->client)->getItem();
474
475
        $item = Arr::get($item->toArray(), 'Item');
476
477
        if (empty($item)) {
478
            return;
479
        }
480
481
        $item = DynamoDb::unmarshalItem($item);
482
483
        $model = $this->model->newInstance([], true);
484
485
        $model->setRawAttributes($item, true);
486
487
        return $model;
488
    }
489
490
    /**
491
     * @param $ids
492
     * @param array $columns
493
     * @return \Illuminate\Database\Eloquent\Collection
494
     */
495
    public function findMany($ids, array $columns = [])
496
    {
497
        $collection = $this->model->newCollection();
498
499
        if (empty($ids)) {
500
            return $collection;
501
        }
502
503
        $this->resetExpressions();
504
505
        $table = $this->model->getTable();
506
507
        $keys = collect($ids)->map(function ($id) {
508
            if (! is_array($id)) {
509
                $id = [$this->model->getKeyName() => $id];
510
            }
511
512
            return DynamoDb::marshalItem($id);
513
        });
514
515
        $subQuery = DynamoDb::newQuery()
516
            ->setKeys($keys->toArray())
517
            ->setProjectionExpression($this->projectionExpression->parse($columns))
518
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
519
            ->prepare($this->client)
520
            ->query;
521
522
        $results = DynamoDb::newQuery()
523
            ->setRequestItems([$table => $subQuery])
524
            ->prepare($this->client)
525
            ->batchGetItem();
526
527
        foreach ($results['Responses'][$table] as $item) {
528
            $item = DynamoDb::unmarshalItem($item);
529
            $model = $this->model->newInstance([], true);
530
            $model->setRawAttributes($item, true);
531
            $collection->add($model);
532
        }
533
534
        return $collection;
535
    }
536
537 2
    public function findOrFail($id, $columns = [])
538
    {
539 2
        $result = $this->find($id, $columns);
540
541
        if ($this->isMultipleIds($id)) {
542
            if (count($result) == count(array_unique($id))) {
0 ignored issues
show
Bug introduced by
$result of type Rennokki\DynamoDb\DynamoDbModel is incompatible with the type Countable|array expected by parameter $var of count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

542
            if (count(/** @scrutinizer ignore-type */ $result) == count(array_unique($id))) {
Loading history...
543
                return $result;
544
            }
545
        } elseif (! is_null($result)) {
546
            return $result;
547
        }
548
549
        throw (new ModelNotFoundException)->setModel(
550
            get_class($this->model),
551
            $id
552
        );
553
    }
554
555 2
    public function first($columns = [])
556
    {
557 2
        $items = $this->getAll($columns, 1);
558
559
        return $items->first();
560
    }
561
562 2
    public function firstOrFail($columns = [])
563
    {
564 2
        if (! is_null($model = $this->first($columns))) {
565
            return $model;
566
        }
567
568
        throw (new ModelNotFoundException)->setModel(get_class($this->model));
569
    }
570
571
    /**
572
     * Remove attributes from an existing item.
573
     *
574
     * @param array ...$attributes
575
     * @return bool
576
     * @throws InvalidQuery
577
     */
578
    public function removeAttribute(...$attributes)
579
    {
580
        $keySet = ! empty(array_filter($this->model->getKeys()));
581
582
        if (! $keySet) {
583
            $analyzer = $this->getConditionAnalyzer();
584
585
            if (! $analyzer->isExactSearch()) {
586
                throw new InvalidQuery('Need to provide the key in your query');
587
            }
588
589
            $id = $analyzer->identifierConditionValues();
590
            $this->model->setId($id);
591
        }
592
593
        $key = DynamoDb::marshalItem($this->model->getKeys());
594
595
        $this->resetExpressions();
596
597
        /** @var \Aws\Result $result */
598
        $result = DynamoDb::table($this->model->getTable())
599
            ->setKey($key)
600
            ->setUpdateExpression($this->updateExpression->remove($attributes))
601
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
602
            ->setReturnValues('ALL_NEW')
603
            ->prepare($this->client)
604
            ->updateItem();
605
606
        $success = Arr::get($result, '@metadata.statusCode') === 200;
607
608
        if ($success) {
609
            $this->model->setRawAttributes(DynamoDb::unmarshalItem($result->get('Attributes')));
610
            $this->model->syncOriginal();
611
        }
612
613
        return $success;
614
    }
615
616
    public function delete()
617
    {
618
        $result = DynamoDb::table($this->model->getTable())
619
            ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
620
            ->prepare($this->client)
621
            ->deleteItem();
622
623
        return Arr::get($result->toArray(), '@metadata.statusCode') === 200;
624
    }
625
626
    public function deleteAsync()
627
    {
628
        $promise = DynamoDb::table($this->model->getTable())
629
            ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
630
            ->prepare($this->client)
631
            ->deleteItemAsync();
632
633
        return $promise;
634
    }
635
636
    public function save()
637
    {
638
        $result = DynamoDb::table($this->model->getTable())
639
            ->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
640
            ->prepare($this->client)
641
            ->putItem();
642
643
        return Arr::get($result, '@metadata.statusCode') === 200;
644
    }
645
646
    public function saveAsync()
647
    {
648
        $promise = DynamoDb::table($this->model->getTable())
649
            ->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
650
            ->prepare($this->client)
651
            ->putItemAsync();
652
653
        return $promise;
654
    }
655
656
    public function get($columns = [])
657
    {
658
        return $this->all($columns);
659
    }
660
661
    public function all($columns = [])
662
    {
663
        $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
664
665
        return $this->getAll($columns, $limit, ! isset($this->limit));
666
    }
667
668
    public function count()
669
    {
670
        $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
671
        $raw = $this->toDynamoDbQuery(['count(*)'], $limit);
672
673
        if ($raw->op === 'Scan') {
674
            $res = $this->client->scan($raw->query);
675
        } else {
676
            $res = $this->client->query($raw->query);
677
        }
678
679
        return $res['Count'];
680
    }
681
682 2
    public function decorate(Closure $closure)
683
    {
684 2
        $this->decorator = $closure;
685
686 2
        return $this;
687
    }
688
689 2
    protected function getAll(
690
        $columns = [],
691
        $limit = self::MAX_LIMIT,
692
        $useIterator = self::DEFAULT_TO_ITERATOR
693
    ) {
694 2
        $analyzer = $this->getConditionAnalyzer();
695
696 2
        if ($analyzer->isExactSearch()) {
697 2
            $item = $this->find($analyzer->identifierConditionValues(), $columns);
698
699
            return $this->getModel()->newCollection([$item]);
700
        }
701
702
        $raw = $this->toDynamoDbQuery($columns, $limit);
703
704
        if ($useIterator) {
705
            $iterator = $this->client->getIterator($raw->op, $raw->query);
706
707
            if (isset($raw->query['Limit'])) {
708
                $iterator = new \LimitIterator($iterator, 0, $raw->query['Limit']);
709
            }
710
        } else {
711
            if ($raw->op === 'Scan') {
712
                $res = $this->client->scan($raw->query);
713
            } else {
714
                $res = $this->client->query($raw->query);
715
            }
716
717
            $this->lastEvaluatedKey = Arr::get($res, 'LastEvaluatedKey');
718
            $iterator = $res['Items'];
719
        }
720
721
        $results = [];
722
723
        foreach ($iterator as $item) {
724
            $item = DynamoDb::unmarshalItem($item);
725
            $model = $this->model->newInstance([], true);
726
            $model->setRawAttributes($item, true);
727
            $results[] = $model;
728
        }
729
730
        return $this->getModel()->newCollection($results, $analyzer->index());
731
    }
732
733
    /**
734
     * Return the raw DynamoDb query.
735
     *
736
     * @param array $columns
737
     * @param int $limit
738
     * @return RawDynamoDbQuery
739
     */
740 5
    public function toDynamoDbQuery(
741
        $columns = [],
742
        $limit = self::MAX_LIMIT
743
    ) {
744 5
        $this->applyScopes();
745
746 5
        $this->resetExpressions();
747
748 5
        $op = 'Scan';
749 5
        $queryBuilder = DynamoDb::table($this->model->getTable());
750
751 5
        if (! empty($this->wheres)) {
752 3
            $analyzer = $this->getConditionAnalyzer();
753
754 3
            if ($keyConditions = $analyzer->keyConditions()) {
755 1
                $op = 'Query';
756 1
                $queryBuilder->setKeyConditionExpression($this->keyConditionExpression->parse($keyConditions));
757
            }
758
759 3
            if ($filterConditions = $analyzer->filterConditions()) {
760 2
                $queryBuilder->setFilterExpression($this->filterExpression->parse($filterConditions));
761
            }
762
763 3
            if ($index = $analyzer->index()) {
764 1
                $queryBuilder->setIndexName($index->name);
765
            }
766
        }
767
768 5
        if ($this->index) {
769
            // If user specifies the index manually, respect that
770 1
            $queryBuilder->setIndexName($this->index);
771
        }
772
773 5
        if ($limit !== static::MAX_LIMIT) {
774
            $queryBuilder->setLimit($limit);
775
        }
776
777 5
        if (! empty($columns)) {
778
            // Either we try to get the count or specific columns
779 2
            if ($columns == ['count(*)']) {
780 2
                $queryBuilder->setSelect('COUNT');
781
            } else {
782
                $queryBuilder->setProjectionExpression($this->projectionExpression->parse($columns));
783
            }
784
        }
785
786 5
        if (! empty($this->lastEvaluatedKey)) {
787
            $queryBuilder->setExclusiveStartKey($this->lastEvaluatedKey);
788
        }
789
790
        $queryBuilder
791 5
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
792 5
            ->setExpressionAttributeValues($this->expressionAttributeValues->all());
793
794 5
        $raw = new RawDynamoDbQuery($op, $queryBuilder->prepare($this->client)->query);
795
796 5
        if ($this->decorator) {
797 2
            call_user_func($this->decorator, $raw);
798
        }
799
800 5
        return $raw;
801
    }
802
803
    /**
804
     * @return Analyzer
805
     */
806 5
    protected function getConditionAnalyzer()
807
    {
808 5
        return with(new Analyzer)
809 5
            ->on($this->model)
810 5
            ->withIndex($this->index)
811 5
            ->analyze($this->wheres);
812
    }
813
814 4
    protected function isMultipleIds($id)
815
    {
816 4
        $keys = collect($this->model->getKeyNames());
817
818
        // could be ['id' => 'foo'], ['id1' => 'foo', 'id2' => 'bar']
819
        $single = $keys->first(function ($name) use ($id) {
820 4
            return ! isset($id[$name]);
821 4
        }) === null;
822
823 4
        if ($single) {
824 3
            return false;
825
        }
826
827
        // could be ['foo', 'bar'], [['id1' => 'foo', 'id2' => 'bar'], ...]
828 1
        return $this->model->hasCompositeKey() ? is_array(H::array_first($id)) : is_array($id);
829
    }
830
831
    /**
832
     * @return DynamoDbModel
833
     */
834
    public function getModel()
835
    {
836
        return $this->model;
837
    }
838
839
    /**
840
     * @return \Aws\DynamoDb\DynamoDbClient
841
     */
842
    public function getClient()
843
    {
844
        return $this->client;
845
    }
846
847
    /**
848
     * Register a new global scope.
849
     *
850
     * @param  string  $identifier
851
     * @param  \Illuminate\Database\Eloquent\Scope|\Closure  $scope
852
     * @return $this
853
     */
854
    public function withGlobalScope($identifier, $scope)
855
    {
856
        $this->scopes[$identifier] = $scope;
857
858
        if (method_exists($scope, 'extend')) {
859
            $scope->extend($this);
860
        }
861
862
        return $this;
863
    }
864
865
    /**
866
     * Remove a registered global scope.
867
     *
868
     * @param  \Illuminate\Database\Eloquent\Scope|string  $scope
869
     * @return $this
870
     */
871
    public function withoutGlobalScope($scope)
872
    {
873
        if (! is_string($scope)) {
874
            $scope = get_class($scope);
875
        }
876
877
        unset($this->scopes[$scope]);
878
879
        $this->removedScopes[] = $scope;
880
881
        return $this;
882
    }
883
884
    /**
885
     * Remove all or passed registered global scopes.
886
     *
887
     * @param  array|null  $scopes
888
     * @return $this
889
     */
890
    public function withoutGlobalScopes(array $scopes = null)
891
    {
892
        if (is_array($scopes)) {
893
            foreach ($scopes as $scope) {
894
                $this->withoutGlobalScope($scope);
895
            }
896
        } else {
897
            $this->scopes = [];
898
        }
899
900
        return $this;
901
    }
902
903
    /**
904
     * Get an array of global scopes that were removed from the query.
905
     *
906
     * @return array
907
     */
908
    public function removedScopes()
909
    {
910
        return $this->removedScopes;
911
    }
912
913
    /**
914
     * Apply the scopes to the Eloquent builder instance and return it.
915
     *
916
     * @return DynamoDbQueryBuilder
917
     */
918 5
    public function applyScopes()
919
    {
920 5
        if (! $this->scopes) {
921 5
            return $this;
922
        }
923
924
        $builder = $this;
925
926
        foreach ($builder->scopes as $identifier => $scope) {
927
            if (! isset($builder->scopes[$identifier])) {
928
                continue;
929
            }
930
931
            $builder->callScope(function (self $builder) use ($scope) {
932
                // If the scope is a Closure we will just go ahead and call the scope with the
933
                // builder instance. The "callScope" method will properly group the clauses
934
                // that are added to this query so "where" clauses maintain proper logic.
935
                if ($scope instanceof Closure) {
936
                    $scope($builder);
937
                }
938
939
                // If the scope is a scope object, we will call the apply method on this scope
940
                // passing in the builder and the model instance. After we run all of these
941
                // scopes we will return back the builder instance to the outside caller.
942
                if ($scope instanceof Scope) {
943
                    throw new NotSupportedException('Scope object is not yet supported');
944
                }
945
            });
946
947
            $builder->withoutGlobalScope($identifier);
948
        }
949
950
        return $builder;
951
    }
952
953
    /**
954
     * Apply the given scope on the current builder instance.
955
     *
956
     * @param  callable  $scope
957
     * @param  array  $parameters
958
     * @return mixed
959
     */
960
    protected function callScope(callable $scope, $parameters = [])
961
    {
962
        array_unshift($parameters, $this);
963
964
        // $query = $this->getQuery();
965
966
        // // We will keep track of how many wheres are on the query before running the
967
        // // scope so that we can properly group the added scope constraints in the
968
        // // query as their own isolated nested where statement and avoid issues.
969
        // $originalWhereCount = is_null($query->wheres)
970
        //             ? 0 : count($query->wheres);
971
972
        $result = $scope(...array_values($parameters)) ?: $this;
973
974
        // if (count((array) $query->wheres) > $originalWhereCount) {
975
        //     $this->addNewWheresWithinGroup($query, $originalWhereCount);
976
        // }
977
978
        return $result;
979
    }
980
981
    /**
982
     * Dynamically handle calls into the query instance.
983
     *
984
     * @param  string  $method
985
     * @param  array  $parameters
986
     * @return mixed
987
     */
988 2
    public function __call($method, $parameters)
989
    {
990 2
        if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
991
            return $this->callScope([$this->model, $scope], $parameters);
992
        }
993
994 2
        return $this;
995
    }
996
}
997