Completed
Pull Request — master (#142)
by Bao
09:53
created

DynamoDbQueryBuilder::findMany()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 4.0009

Importance

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