Completed
Branch raw-query (b7467f)
by Bao
12:24 queued 06:02
created

DynamoDbQueryBuilder::chunk()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
rs 9.9
cc 4
nc 5
nop 2
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\ModelNotFoundException;
12
use Illuminate\Database\Eloquent\Scope;
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 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
    public function __construct(DynamoDbModel $model)
79
    {
80
        $this->model = $model;
81
        $this->client = $model->getClient();
82
        $this->setupExpressions();
83
    }
84
85
    /**
86
     * Alias to set the "limit" value of the query.
87
     *
88
     * @param  int  $value
89
     * @return DynamoDbQueryBuilder\static
0 ignored issues
show
Bug introduced by
The type BaoPham\DynamoDb\DynamoDbQueryBuilder\static was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
90
     */
91
    public function take($value)
92
    {
93
        return $this->limit($value);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->limit($value) returns the type BaoPham\DynamoDb\DynamoDbQueryBuilder which is incompatible with the documented return type BaoPham\DynamoDb\DynamoDbQueryBuilder\static.
Loading history...
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)
1 ignored issue
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

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

126
    public function offset(/** @scrutinizer ignore-unused */ $value)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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 = $this->getDynamoDbKey($afterKey);
168
169
        return $this;
170
    }
171
172
    /**
173
     * Set the index name manually
174
     *
175
     * @param string $index The index name
176
     * @return $this
177
     */
178
    public function withIndex($index)
179
    {
180
        $this->index = $index;
181
        return $this;
182
    }
183
184
    public function where($column, $operator = null, $value = null, $boolean = 'and')
185
    {
186
        // If the column is an array, we will assume it is an array of key-value pairs
187
        // and can add them each as a where clause. We will maintain the boolean we
188
        // received when the method was called and pass it into the nested where.
189
        if (is_array($column)) {
190
            foreach ($column as $key => $value) {
191
                return $this->where($key, '=', $value);
192
            }
193
        }
194
195
        // Here we will make some assumptions about the operator. If only 2 values are
196
        // passed to the method, we will assume that the operator is an equals sign
197
        // and keep going. Otherwise, we'll require the operator to be passed in.
198
        if (func_num_args() == 2) {
199
            list($value, $operator) = [$operator, '='];
200
        }
201
202
        // If the columns is actually a Closure instance, we will assume the developer
203
        // wants to begin a nested where statement which is wrapped in parenthesis.
204
        // We'll add that Closure to the query then return back out immediately.
205
        if ($column instanceof Closure) {
206
            return $this->whereNested($column, $boolean);
207
        }
208
209
        // If the given operator is not found in the list of valid operators we will
210
        // assume that the developer is just short-cutting the '=' operators and
211
        // we will set the operators to '=' and set the values appropriately.
212
        if (!ComparisonOperator::isValidOperator($operator)) {
213
            list($value, $operator) = [$operator, '='];
214
        }
215
216
        // If the value is a Closure, it means the developer is performing an entire
217
        // sub-select within the query and we will need to compile the sub-select
218
        // within the where clause to get the appropriate query record results.
219
        if ($value instanceof Closure) {
220
            throw new NotSupportedException('Closure in where clause is not supported');
221
        }
222
223
        $this->wheres[] = [
224
            'column' => $column,
225
            'type' => ComparisonOperator::getDynamoDbOperator($operator),
226
            'value' => $value,
227
            'boolean' => $boolean,
228
        ];
229
230
        return $this;
231
    }
232
233
    /**
234
     * Add a nested where statement to the query.
235
     *
236
     * @param  \Closure $callback
237
     * @param  string   $boolean
238
     * @return $this
239
     */
240
    public function whereNested(Closure $callback, $boolean = 'and')
241
    {
242
        call_user_func($callback, $query = $this->forNestedWhere());
243
244
        return $this->addNestedWhereQuery($query, $boolean);
245
    }
246
247
    /**
248
     * Create a new query instance for nested where condition.
249
     *
250
     * @return $this
251
     */
252
    public function forNestedWhere()
253
    {
254
        return $this->newQuery();
255
    }
256
257
    /**
258
     * Add another query builder as a nested where to the query builder.
259
     *
260
     * @param  DynamoDbQueryBuilder|static $query
261
     * @param  string  $boolean
262
     * @return $this
263
     */
264
    public function addNestedWhereQuery($query, $boolean = 'and')
265
    {
266
        if (count($query->wheres)) {
267
            $type = 'Nested';
268
            $column = null;
269
            $value = $query->wheres;
270
            $this->wheres[] = compact('column', 'type', 'value', 'boolean');
271
        }
272
273
        return $this;
274
    }
275
276
    /**
277
     * Add an "or where" clause to the query.
278
     *
279
     * @param  string  $column
280
     * @param  string  $operator
281
     * @param  mixed   $value
282
     * @return $this
283
     */
284
    public function orWhere($column, $operator = null, $value = null)
285
    {
286
        return $this->where($column, $operator, $value, 'or');
287
    }
288
289
    /**
290
     * Add a "where in" clause to the query.
291
     *
292
     * @param  string  $column
293
     * @param  mixed   $values
294
     * @param  string  $boolean
295
     * @param  bool    $not
296
     * @return $this
297
     * @throws NotSupportedException
298
     */
299
    public function whereIn($column, $values, $boolean = 'and', $not = false)
300
    {
301
        if ($not) {
302
            throw new NotSupportedException('"not in" is not a valid DynamoDB comparison operator');
303
        }
304
305
        // If the value is a query builder instance, not supported
306
        if ($values instanceof static) {
307
            throw new NotSupportedException('Value is a query builder instance');
308
        }
309
310
        // If the value of the where in clause is actually a Closure, not supported
311
        if ($values instanceof Closure) {
312
            throw new NotSupportedException('Value is a Closure');
313
        }
314
315
        // Next, if the value is Arrayable we need to cast it to its raw array form
316
        if ($values instanceof Arrayable) {
317
            $values = $values->toArray();
318
        }
319
320
        return $this->where($column, ComparisonOperator::IN, $values, $boolean);
321
    }
322
323
    /**
324
     * Add an "or where in" clause to the query.
325
     *
326
     * @param  string  $column
327
     * @param  mixed   $values
328
     * @return $this
329
     */
330
    public function orWhereIn($column, $values)
331
    {
332
        return $this->whereIn($column, $values, 'or');
333
    }
334
335
    /**
336
     * Add a "where null" clause to the query.
337
     *
338
     * @param  string  $column
339
     * @param  string  $boolean
340
     * @param  bool    $not
341
     * @return $this
342
     */
343
    public function whereNull($column, $boolean = 'and', $not = false)
344
    {
345
        $type = $not ? ComparisonOperator::NOT_NULL : ComparisonOperator::NULL;
346
347
        $this->wheres[] = compact('column', 'type', 'boolean');
348
349
        return $this;
350
    }
351
352
    /**
353
     * Add an "or where null" clause to the query.
354
     *
355
     * @param  string  $column
356
     * @return $this
357
     */
358
    public function orWhereNull($column)
359
    {
360
        return $this->whereNull($column, 'or');
361
    }
362
363
    /**
364
     * Add an "or where not null" clause to the query.
365
     *
366
     * @param  string  $column
367
     * @return $this
368
     */
369
    public function orWhereNotNull($column)
370
    {
371
        return $this->whereNotNull($column, 'or');
372
    }
373
374
    /**
375
     * Add a "where not null" clause to the query.
376
     *
377
     * @param  string  $column
378
     * @param  string  $boolean
379
     * @return $this
380
     */
381
    public function whereNotNull($column, $boolean = 'and')
382
    {
383
        return $this->whereNull($column, $boolean, true);
384
    }
385
386
    /**
387
     * Get a new instance of the query builder.
388
     *
389
     * @return DynamoDbQueryBuilder
390
     */
391
    public function newQuery()
392
    {
393
        return new static($this->getModel());
394
    }
395
396
    /**
397
     * Implements the Query Chunk method
398
     *
399
     * @param int $chunkSize
400
     * @param callable $callback
401
     */
402
    public function chunk($chunkSize, callable $callback)
403
    {
404
        while (true) {
405
            $results = $this->getAll([], $chunkSize, false);
406
407
            if ($results->isNotEmpty()) {
408
                call_user_func($callback, $results);
409
            }
410
411
            if (empty($this->lastEvaluatedKey)) {
412
                break;
413
            }
414
        }
415
    }
416
417
    public function find($id, array $columns = [])
418
    {
419
        if ($this->isMultipleIds($id)) {
420
            return $this->findMany($id, $columns);
421
        }
422
423
        $this->resetExpressions();
424
425
        $this->model->setId($id);
426
427
        $query = DynamoDb::table($this->model->getTable())
428
            ->setKey($this->getDynamoDbKey())
429
            ->setConsistentRead(true);
0 ignored issues
show
Bug introduced by
The method setConsistentRead() does not exist on BaoPham\DynamoDb\DynamoDb\QueryBuilder. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

429
            ->/** @scrutinizer ignore-call */ setConsistentRead(true);
Loading history...
430
431
        if (!empty($columns)) {
432
            $query
433
                ->setProjectionExpression($this->projectionExpression->parse($columns))
434
                ->setExpressionAttributeNames($this->expressionAttributeNames->all());
435
        }
436
437
        $item = $query->prepare($this->client)->getItem();
438
439
        $item = array_get($item->toArray(), 'Item');
440
441
        if (empty($item)) {
442
            return;
443
        }
444
445
        $item = DynamoDb::unmarshalItem($item);
446
447
        $model = $this->model->newInstance([], true);
448
449
        $model->setRawAttributes($item, true);
450
451
        return $model;
452
    }
453
454
    public function findMany($ids, array $columns = [])
0 ignored issues
show
Unused Code introduced by
The parameter $ids is not used and could be removed. ( Ignorable by Annotation )

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

454
    public function findMany(/** @scrutinizer ignore-unused */ $ids, array $columns = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $columns is not used and could be removed. ( Ignorable by Annotation )

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

454
    public function findMany($ids, /** @scrutinizer ignore-unused */ array $columns = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
455
    {
456
        throw new NotSupportedException('Finding by multiple ids is not supported');
457
    }
458
459
    public function findOrFail($id, $columns = [])
460
    {
461
        $result = $this->find($id, $columns);
462
463
        if ($this->isMultipleIds($id)) {
464
            if (count($result) == count(array_unique($id))) {
1 ignored issue
show
Bug introduced by
It seems like $result can also be of type BaoPham\DynamoDb\DynamoDbModel; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

464
            if (count(/** @scrutinizer ignore-type */ $result) == count(array_unique($id))) {
Loading history...
465
                return $result;
466
            }
467
        } elseif (! is_null($result)) {
468
            return $result;
469
        }
470
471
        throw (new ModelNotFoundException)->setModel(
472
            get_class($this->model),
473
            $id
474
        );
475
    }
476
477
    public function first($columns = [])
478
    {
479
        $items = $this->getAll($columns, 1);
480
481
        return $items->first();
482
    }
483
484
    public function firstOrFail($columns = [])
485
    {
486
        if (! is_null($model = $this->first($columns))) {
487
            return $model;
488
        }
489
490
        throw (new ModelNotFoundException)->setModel(get_class($this->model));
491
    }
492
493
    /**
494
     * Remove attributes from an existing item
495
     *
496
     * @param array ...$attributes
497
     * @return bool
498
     * @throws InvalidQuery
499
     */
500
    public function removeAttribute(...$attributes)
501
    {
502
        $key = $this->getDynamoDbKey();
503
504
        if (empty($key)) {
505
            $analyzer = $this->getConditionAnalyzer();
506
507
            if (!$analyzer->isExactSearch()) {
508
                throw new InvalidQuery('Need to provide the key in your query');
509
            }
510
511
            $id = $analyzer->identifierConditionValues();
512
            $this->model->setId($id);
513
            $key = $this->getDynamoDbKey();
514
        }
515
516
        $this->resetExpressions();
517
518
        $result = DynamoDb::table($this->model->getTable())
519
            ->setKey($key)
520
            ->setUpdateExpression($this->updateExpression->remove($attributes))
521
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
522
            ->prepare($this->client)
523
            ->updateItem();
524
525
        return array_get($result, '@metadata.statusCode') === 200;
526
    }
527
528
    public function get($columns = [])
529
    {
530
        return $this->all($columns);
531
    }
532
533
    public function delete()
534
    {
535
        $result = DynamoDb::table($this->model->getTable())
536
            ->setKey($this->getDynamoDbKey())
537
            ->prepare($this->client)
538
            ->deleteItem();
539
540
        return array_get($result->toArray(), '@metadata.statusCode') === 200;
541
    }
542
543
    public function save()
544
    {
545
        $result = DynamoDb::table($this->model->getTable())
546
            ->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
547
            ->prepare($this->client)
548
            ->putItem();
549
550
        return array_get($result, '@metadata.statusCode') === 200;
551
    }
552
553
    public function all($columns = [])
554
    {
555
        $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
556
        return $this->getAll($columns, $limit);
557
    }
558
559
    public function count()
560
    {
561
        $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
562
        $raw = $this->toDynamoDbQuery(['count(*)'], $limit);
563
564
        if ($raw->op === 'Scan') {
565
            $res = $this->client->scan($raw->query);
566
        } else {
567
            $res = $this->client->query($raw->query);
568
        }
569
570
        return $res['Count'];
571
    }
572
573
    public function decorate(Closure $closure)
574
    {
575
        $this->decorator = $closure;
576
        return $this;
577
    }
578
579
    protected function getAll(
580
        $columns = [],
581
        $limit = DynamoDbQueryBuilder::MAX_LIMIT,
582
        $useIterator = DynamoDbQueryBuilder::DEFAULT_TO_ITERATOR
583
    ) {
584
        $analyzer = $this->getConditionAnalyzer();
585
586
        if ($analyzer->isExactSearch()) {
587
            $item = $this->find($analyzer->identifierConditionValues(), $columns);
588
589
            return $this->getModel()->newCollection([$item]);
590
        }
591
592
        $raw = $this->toDynamoDbQuery($columns, $limit);
593
594
        if ($useIterator) {
595
            $iterator = $this->client->getIterator($raw->op, $raw->query);
596
597
            if (isset($raw->query['Limit'])) {
598
                $iterator = new \LimitIterator($iterator, 0, $raw->query['Limit']);
599
            }
600
        } else {
601
            if ($raw->op === 'Scan') {
602
                $res = $this->client->scan($raw->query);
603
            } else {
604
                $res = $this->client->query($raw->query);
605
            }
606
607
            $this->lastEvaluatedKey = array_get($res, 'LastEvaluatedKey');
608
            $iterator = $res['Items'];
609
        }
610
611
        $results = [];
612
613
        foreach ($iterator as $item) {
614
            $item = DynamoDb::unmarshalItem($item);
615
            $model = $this->model->newInstance([], true);
616
            $model->setRawAttributes($item, true);
617
            $results[] = $model;
618
        }
619
620
        return $this->getModel()->newCollection($results);
621
    }
622
623
    /**
624
     * Return the raw DynamoDb query
625
     *
626
     * @param array $columns
627
     * @param int $limit
628
     * @return RawDynamoDbQuery
629
     */
630
    public function toDynamoDbQuery(
631
        $columns = [],
632
        $limit = DynamoDbQueryBuilder::MAX_LIMIT
633
    ) {
634
        $this->applyScopes();
635
636
        $this->resetExpressions();
637
638
        $op = 'Scan';
639
        $queryBuilder = DynamoDb::table($this->model->getTable());
640
641
        if (! empty($this->wheres)) {
642
            $analyzer = $this->getConditionAnalyzer();
643
644
            if ($keyConditions = $analyzer->keyConditions()) {
645
                $op = 'Query';
646
                $queryBuilder->setKeyConditionExpression($this->keyConditionExpression->parse($keyConditions));
647
            }
648
649
            if ($filterConditions = $analyzer->filterConditions()) {
650
                $queryBuilder->setFilterExpression($this->filterExpression->parse($filterConditions));
651
            }
652
653
            if ($index = $analyzer->index()) {
654
                $queryBuilder->setIndexName($index->name);
655
            }
656
        }
657
658
        if ($this->index) {
659
            // If user specifies the index manually, respect that
660
            $queryBuilder->setIndexName($this->index);
661
        }
662
663
        if ($limit !== static::MAX_LIMIT) {
664
            $queryBuilder->setLimit($limit);
665
        }
666
667
        if (!empty($columns)) {
668
            // Either we try to get the count or specific columns
669
            if ($columns == ['count(*)']) {
670
                $queryBuilder->setSelect('COUNT');
671
            } else {
672
                $queryBuilder->setProjectionExpression($this->projectionExpression->parse($columns));
673
            }
674
        }
675
676
        if (!empty($this->lastEvaluatedKey)) {
677
            $queryBuilder->setExclusiveStartKey($this->lastEvaluatedKey);
678
        }
679
680
        $queryBuilder
681
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
682
            ->setExpressionAttributeValues($this->expressionAttributeValues->all());
683
684
        $raw = with(new RawDynamoDbQuery($op, $queryBuilder->query))->finalize();
685
686
        if ($this->decorator) {
687
            call_user_func($this->decorator, $raw);
688
        }
689
690
        return $raw;
691
    }
692
693
    /**
694
     * @return Analyzer
695
     */
696
    protected function getConditionAnalyzer()
697
    {
698
        return with(new Analyzer)
699
            ->on($this->model)
700
            ->withIndex($this->index)
701
            ->analyze($this->wheres);
702
    }
703
704
    /**
705
     * Return key for DynamoDb query.
706
     *
707
     * @param array|null $modelKeys
708
     * @return array
709
     *
710
     * e.g.
711
     * [
712
     *   'id' => ['S' => 'foo'],
713
     * ]
714
     *
715
     * or
716
     *
717
     * [
718
     *   'id' => ['S' => 'foo'],
719
     *   'id2' => ['S' => 'bar'],
720
     * ]
721
     */
722
    protected function getDynamoDbKey($modelKeys = null)
723
    {
724
        $modelKeys = $modelKeys ?: $this->model->getKeys();
725
726
        $keys = [];
727
728
        foreach ($modelKeys as $key => $value) {
729
            if (is_null($value)) {
730
                continue;
731
            }
732
            $keys[$key] = DynamoDb::marshalValue($value);
733
        }
734
735
        return $keys;
736
    }
737
738
    protected function isMultipleIds($id)
739
    {
740
        $keys = collect($this->model->getKeyNames());
741
742
        // could be ['id' => 'foo'], ['id1' => 'foo', 'id2' => 'bar']
743
        $single = $keys->first(function ($name) use ($id) {
744
            return !isset($id[$name]);
745
        }) === null;
746
747
        if ($single) {
748
            return false;
749
        }
750
751
        // could be ['foo', 'bar'], [['id1' => 'foo', 'id2' => 'bar'], ...]
752
        return $this->model->hasCompositeKey() ? is_array(array_first($id)) : is_array($id);
753
    }
754
755
    /**
756
     * @return DynamoDbModel
757
     */
758
    public function getModel()
759
    {
760
        return $this->model;
761
    }
762
763
    /**
764
     * @return DynamoDbClient
765
     */
766
    public function getClient()
767
    {
768
        return $this->client;
769
    }
770
771
    /**
772
     * Register a new global scope.
773
     *
774
     * @param  string  $identifier
775
     * @param  \Illuminate\Database\Eloquent\Scope|\Closure  $scope
776
     * @return $this
777
     */
778
    public function withGlobalScope($identifier, $scope)
779
    {
780
        $this->scopes[$identifier] = $scope;
781
782
        if (method_exists($scope, 'extend')) {
783
            $scope->extend($this);
784
        }
785
786
        return $this;
787
    }
788
789
    /**
790
     * Remove a registered global scope.
791
     *
792
     * @param  \Illuminate\Database\Eloquent\Scope|string  $scope
793
     * @return $this
794
     */
795
    public function withoutGlobalScope($scope)
796
    {
797
        if (! is_string($scope)) {
798
            $scope = get_class($scope);
799
        }
800
801
        unset($this->scopes[$scope]);
802
803
        $this->removedScopes[] = $scope;
804
805
        return $this;
806
    }
807
808
    /**
809
     * Remove all or passed registered global scopes.
810
     *
811
     * @param  array|null  $scopes
812
     * @return $this
813
     */
814
    public function withoutGlobalScopes(array $scopes = null)
815
    {
816
        if (is_array($scopes)) {
817
            foreach ($scopes as $scope) {
818
                $this->withoutGlobalScope($scope);
819
            }
820
        } else {
821
            $this->scopes = [];
822
        }
823
824
        return $this;
825
    }
826
827
    /**
828
     * Get an array of global scopes that were removed from the query.
829
     *
830
     * @return array
831
     */
832
    public function removedScopes()
833
    {
834
        return $this->removedScopes;
835
    }
836
837
    /**
838
     * Apply the scopes to the Eloquent builder instance and return it.
839
     *
840
     * @return DynamoDbQueryBuilder
841
     */
842
    public function applyScopes()
843
    {
844
        if (! $this->scopes) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $this->scopes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
845
            return $this;
846
        }
847
848
        $builder = $this;
849
850
        foreach ($builder->scopes as $identifier => $scope) {
851
            if (! isset($builder->scopes[$identifier])) {
852
                continue;
853
            }
854
855
            $builder->callScope(function (DynamoDbQueryBuilder $builder) use ($scope) {
856
                // If the scope is a Closure we will just go ahead and call the scope with the
857
                // builder instance. The "callScope" method will properly group the clauses
858
                // that are added to this query so "where" clauses maintain proper logic.
859
                if ($scope instanceof Closure) {
860
                    $scope($builder);
861
                }
862
863
                // If the scope is a scope object, we will call the apply method on this scope
864
                // passing in the builder and the model instance. After we run all of these
865
                // scopes we will return back the builder instance to the outside caller.
866
                if ($scope instanceof Scope) {
867
                    throw new NotSupportedException('Scope object is not yet supported');
868
                }
869
            });
870
871
            $builder->withoutGlobalScope($identifier);
872
        }
873
874
        return $builder;
875
    }
876
877
    /**
878
     * Apply the given scope on the current builder instance.
879
     *
880
     * @param  callable  $scope
881
     * @param  array  $parameters
882
     * @return mixed
883
     */
884
    protected function callScope(callable $scope, $parameters = [])
885
    {
886
        array_unshift($parameters, $this);
887
888
        // $query = $this->getQuery();
889
890
        // // We will keep track of how many wheres are on the query before running the
891
        // // scope so that we can properly group the added scope constraints in the
892
        // // query as their own isolated nested where statement and avoid issues.
893
        // $originalWhereCount = is_null($query->wheres)
894
        //             ? 0 : count($query->wheres);
895
896
        $result = $scope(...array_values($parameters)) ?: $this;
897
898
        // if (count((array) $query->wheres) > $originalWhereCount) {
899
        //     $this->addNewWheresWithinGroup($query, $originalWhereCount);
900
        // }
901
902
        return $result;
903
    }
904
905
    /**
906
     * Dynamically handle calls into the query instance.
907
     *
908
     * @param  string  $method
909
     * @param  array  $parameters
910
     * @return mixed
911
     */
912
    public function __call($method, $parameters)
913
    {
914
        if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
915
            return $this->callScope([$this->model, $scope], $parameters);
916
        }
917
918
        return $this;
919
    }
920
}
921