Completed
Push — master ( addbc6...6731dd )
by Song
02:29
created

Model::collection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin\Grid;
4
5
use Encore\Admin\Grid;
6
use Encore\Admin\Middleware\Pjax;
7
use Illuminate\Database\Eloquent\Model as EloquentModel;
8
use Illuminate\Database\Eloquent\Relations\BelongsTo;
9
use Illuminate\Database\Eloquent\Relations\HasMany;
10
use Illuminate\Database\Eloquent\Relations\HasOne;
11
use Illuminate\Database\Eloquent\Relations\Relation;
12
use Illuminate\Pagination\LengthAwarePaginator;
13
use Illuminate\Support\Arr;
14
use Illuminate\Support\Collection;
15
use Illuminate\Support\Facades\Request;
16
use Illuminate\Support\Str;
17
18
class Model
19
{
20
    /**
21
     * Eloquent model instance of the grid model.
22
     *
23
     * @var EloquentModel
24
     */
25
    protected $model;
26
27
    /**
28
     * @var EloquentModel
29
     */
30
    protected $originalModel;
31
32
    /**
33
     * Array of queries of the eloquent model.
34
     *
35
     * @var \Illuminate\Support\Collection
36
     */
37
    protected $queries;
38
39
    /**
40
     * Sort parameters of the model.
41
     *
42
     * @var array
43
     */
44
    protected $sort;
45
46
    /**
47
     * @var array
48
     */
49
    protected $data = [];
50
51
    /*
52
     * 20 items per page as default.
53
     *
54
     * @var int
55
     */
56
    protected $perPage = 20;
57
58
    /**
59
     * If the model use pagination.
60
     *
61
     * @var bool
62
     */
63
    protected $usePaginate = true;
64
65
    /**
66
     * The query string variable used to store the per-page.
67
     *
68
     * @var string
69
     */
70
    protected $perPageName = 'per_page';
71
72
    /**
73
     * The query string variable used to store the sort.
74
     *
75
     * @var string
76
     */
77
    protected $sortName = '_sort';
78
79
    /**
80
     * Collection callback.
81
     *
82
     * @var \Closure
83
     */
84
    protected $collectionCallback;
85
86
    /**
87
     * @var Grid
88
     */
89
    protected $grid;
90
91
    /**
92
     * @var Relation
93
     */
94
    protected $relation;
95
96
    /**
97
     * @var array
98
     */
99
    protected $eagerLoads = [];
100
101
    /**
102
     * Create a new grid model instance.
103
     *
104
     * @param EloquentModel $model
105
     * @param Grid          $grid
106
     */
107
    public function __construct(EloquentModel $model, Grid $grid = null)
108
    {
109
        $this->model = $model;
110
111
        $this->originalModel = $model;
112
113
        $this->grid = $grid;
114
115
        $this->queries = collect();
116
    }
117
118
    /**
119
     * @return EloquentModel
120
     */
121
    public function getOriginalModel()
122
    {
123
        return $this->originalModel;
124
    }
125
126
    /**
127
     * Get the eloquent model of the grid model.
128
     *
129
     * @return EloquentModel
130
     */
131
    public function eloquent()
132
    {
133
        return $this->model;
134
    }
135
136
    /**
137
     * Enable or disable pagination.
138
     *
139
     * @param bool $use
140
     */
141
    public function usePaginate($use = true)
142
    {
143
        $this->usePaginate = $use;
144
    }
145
146
    /**
147
     * Get the query string variable used to store the per-page.
148
     *
149
     * @return string
150
     */
151
    public function getPerPageName()
152
    {
153
        return $this->perPageName;
154
    }
155
156
    /**
157
     * Set the query string variable used to store the per-page.
158
     *
159
     * @param string $name
160
     *
161
     * @return $this
162
     */
163
    public function setPerPageName($name)
164
    {
165
        $this->perPageName = $name;
166
167
        return $this;
168
    }
169
170
    /**
171
     * Get per-page number.
172
     *
173
     * @return int
174
     */
175
    public function getPerPage()
176
    {
177
        return $this->perPage;
178
    }
179
180
    /**
181
     * Set per-page number.
182
     *
183
     * @param int $perPage
184
     *
185
     * @return $this
186
     */
187
    public function setPerPage($perPage)
188
    {
189
        $this->perPage = $perPage;
190
191
        $this->__call('paginate', [$perPage]);
192
193
        return $this;
194
    }
195
196
    /**
197
     * Get the query string variable used to store the sort.
198
     *
199
     * @return string
200
     */
201
    public function getSortName()
202
    {
203
        return $this->sortName;
204
    }
205
206
    /**
207
     * Set the query string variable used to store the sort.
208
     *
209
     * @param string $name
210
     *
211
     * @return $this
212
     */
213
    public function setSortName($name)
214
    {
215
        $this->sortName = $name;
216
217
        return $this;
218
    }
219
220
    /**
221
     * Set parent grid instance.
222
     *
223
     * @param Grid $grid
224
     *
225
     * @return $this
226
     */
227
    public function setGrid(Grid $grid)
228
    {
229
        $this->grid = $grid;
230
231
        return $this;
232
    }
233
234
    /**
235
     * Get parent gird instance.
236
     *
237
     * @return Grid
238
     */
239
    public function getGrid()
240
    {
241
        return $this->grid;
242
    }
243
244
    /**
245
     * @param Relation $relation
246
     *
247
     * @return $this
248
     */
249
    public function setRelation(Relation $relation)
250
    {
251
        $this->relation = $relation;
252
253
        return $this;
254
    }
255
256
    /**
257
     * @return Relation
258
     */
259
    public function getRelation()
260
    {
261
        return $this->relation;
262
    }
263
264
    /**
265
     * Get constraints.
266
     *
267
     * @return array|bool
268
     */
269
    public function getConstraints()
270
    {
271
        if ($this->relation instanceof HasMany) {
272
            return [
273
                $this->relation->getForeignKeyName() => $this->relation->getParentKey(),
274
            ];
275
        }
276
277
        return false;
278
    }
279
280
    /**
281
     * Set collection callback.
282
     *
283
     * @param \Closure $callback
284
     *
285
     * @return $this
286
     */
287
    public function collection(\Closure $callback = null)
288
    {
289
        $this->collectionCallback = $callback;
290
291
        return $this;
292
    }
293
294
    /**
295
     * Build.
296
     *
297
     * @param bool $toArray
298
     *
299
     * @return array|Collection|mixed
300
     */
301
    public function buildData($toArray = true)
302
    {
303
        if (empty($this->data)) {
304
            $collection = $this->get();
305
306
            if ($this->collectionCallback) {
307
                $collection = call_user_func($this->collectionCallback, $collection);
308
            }
309
310
            if ($toArray) {
311
                $this->data = $collection->toArray();
312
            } else {
313
                $this->data = $collection;
0 ignored issues
show
Documentation Bug introduced by
It seems like $collection of type * is incompatible with the declared type array of property $data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
314
            }
315
        }
316
317
        return $this->data;
318
    }
319
320
    /**
321
     * @param callable $callback
322
     * @param int      $count
323
     *
324
     * @return bool
325
     */
326
    public function chunk($callback, $count = 100)
327
    {
328
        if ($this->usePaginate) {
329
            return $this->buildData(false)->chunk($count)->each($callback);
330
        }
331
332
        $this->setSort();
333
334
        $this->queries->reject(function ($query) {
335
            return $query['method'] == 'paginate';
336
        })->each(function ($query) {
337
            $this->model = $this->model->{$query['method']}(...$query['arguments']);
338
        });
339
340
        return $this->model->chunk($count, $callback);
341
    }
342
343
    /**
344
     * Add conditions to grid model.
345
     *
346
     * @param array $conditions
347
     *
348
     * @return $this
349
     */
350
    public function addConditions(array $conditions)
351
    {
352
        foreach ($conditions as $condition) {
353
            call_user_func_array([$this, key($condition)], current($condition));
354
        }
355
356
        return $this;
357
    }
358
359
    /**
360
     * Get table of the model.
361
     *
362
     * @return string
363
     */
364
    public function getTable()
365
    {
366
        return $this->model->getTable();
367
    }
368
369
    /**
370
     * @throws \Exception
371
     *
372
     * @return Collection
373
     */
374
    protected function get()
375
    {
376
        if ($this->model instanceof LengthAwarePaginator) {
377
            return $this->model;
378
        }
379
380
        if ($this->relation) {
381
            $this->model = $this->relation->getQuery();
382
        }
383
384
        $this->setSort();
385
        $this->setPaginate();
386
387
        $this->queries->unique()->each(function ($query) {
388
            $this->model = call_user_func_array([$this->model, $query['method']], $query['arguments']);
389
        });
390
391
        if ($this->model instanceof Collection) {
392
            return $this->model;
393
        }
394
395
        if ($this->model instanceof LengthAwarePaginator) {
396
            $this->handleInvalidPage($this->model);
397
398
            return $this->model->getCollection();
399
        }
400
401
        throw new \Exception('Grid query error');
402
    }
403
404
    /**
405
     * @return \Illuminate\Database\Eloquent\Builder|EloquentModel
406
     */
407
    public function getQueryBuilder()
408
    {
409
        if ($this->relation) {
410
            return $this->relation->getQuery();
411
        }
412
413
        $this->setSort();
414
415
        $queryBuilder = $this->originalModel;
416
417
        $this->queries->reject(function ($query) {
418
            return in_array($query['method'], ['get', 'paginate']);
419
        })->each(function ($query) use (&$queryBuilder) {
420
            $queryBuilder = $queryBuilder->{$query['method']}(...$query['arguments']);
421
        });
422
423
        return $queryBuilder;
424
    }
425
426
    /**
427
     * If current page is greater than last page, then redirect to last page.
428
     *
429
     * @param LengthAwarePaginator $paginator
430
     *
431
     * @return void
432
     */
433
    protected function handleInvalidPage(LengthAwarePaginator $paginator)
434
    {
435
        if ($paginator->lastPage() && $paginator->currentPage() > $paginator->lastPage()) {
436
            $lastPageUrl = Request::fullUrlWithQuery([
437
                $paginator->getPageName() => $paginator->lastPage(),
438
            ]);
439
440
            Pjax::respond(redirect($lastPageUrl));
441
        }
442
    }
443
444
    /**
445
     * Set the grid paginate.
446
     *
447
     * @return void
448
     */
449
    protected function setPaginate()
450
    {
451
        $paginate = $this->findQueryByMethod('paginate');
452
453
        $this->queries = $this->queries->reject(function ($query) {
454
            return $query['method'] == 'paginate';
455
        });
456
457
        if (!$this->usePaginate) {
458
            $query = [
459
                'method'    => 'get',
460
                'arguments' => [],
461
            ];
462
        } else {
463
            $query = [
464
                'method'    => 'paginate',
465
                'arguments' => $this->resolvePerPage($paginate),
0 ignored issues
show
Documentation introduced by
$paginate is of type this<Encore\Admin\Grid\Model>, but the function expects a array|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
466
            ];
467
        }
468
469
        $this->queries->push($query);
470
    }
471
472
    /**
473
     * Resolve perPage for pagination.
474
     *
475
     * @param array|null $paginate
476
     *
477
     * @return array
478
     */
479
    protected function resolvePerPage($paginate)
480
    {
481
        if ($perPage = request($this->perPageName)) {
482
            if (is_array($paginate)) {
483
                $paginate['arguments'][0] = (int) $perPage;
484
485
                return $paginate['arguments'];
486
            }
487
488
            $this->perPage = (int) $perPage;
489
        }
490
491
        if (isset($paginate['arguments'][0])) {
492
            return $paginate['arguments'];
493
        }
494
495
        if ($name = $this->grid->getName()) {
496
            return [$this->perPage, ['*'], "{$name}_page"];
497
        }
498
499
        return [$this->perPage];
500
    }
501
502
    /**
503
     * Find query by method name.
504
     *
505
     * @param $method
506
     *
507
     * @return static
508
     */
509
    protected function findQueryByMethod($method)
510
    {
511
        return $this->queries->first(function ($query) use ($method) {
512
            return $query['method'] == $method;
513
        });
514
    }
515
516
    /**
517
     * Set the grid sort.
518
     *
519
     * @return void
520
     */
521
    protected function setSort()
522
    {
523
        $this->sort = \request($this->sortName, []);
0 ignored issues
show
Documentation Bug introduced by
It seems like \request($this->sortName, array()) can also be of type object<Illuminate\Http\Request> or string. However, the property $sort is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
524
        if (!is_array($this->sort)) {
525
            return;
526
        }
527
528
        if (empty($this->sort['column']) || empty($this->sort['type'])) {
529
            return;
530
        }
531
532
        if (Str::contains($this->sort['column'], '.')) {
533
            $this->setRelationSort($this->sort['column']);
534
        } else {
535
            $this->resetOrderBy();
536
537
            // get column. if contains "cast", set set column as cast
538
            if (!empty($this->sort['cast'])) {
539
                $column = "CAST({$this->sort['column']} AS {$this->sort['cast']}) {$this->sort['type']}";
540
                $method = 'orderByRaw';
541
                $arguments = [$column];
542
            } else {
543
                $column = $this->sort['column'];
544
                $method = 'orderBy';
545
                $arguments = [$column, $this->sort['type']];
546
            }
547
548
            $this->queries->push([
549
                'method'    => $method,
550
                'arguments' => $arguments,
551
            ]);
552
        }
553
    }
554
555
    /**
556
     * Set relation sort.
557
     *
558
     * @param string $column
559
     *
560
     * @return void
561
     */
562
    protected function setRelationSort($column)
563
    {
564
        list($relationName, $relationColumn) = explode('.', $column);
565
566
        if ($this->queries->contains(function ($query) use ($relationName) {
567
            return $query['method'] == 'with' && in_array($relationName, $query['arguments']);
568
        })) {
569
            $relation = $this->model->$relationName();
570
571
            $this->queries->push([
572
                'method'    => 'select',
573
                'arguments' => [$this->model->getTable().'.*'],
574
            ]);
575
576
            $this->queries->push([
577
                'method'    => 'join',
578
                'arguments' => $this->joinParameters($relation),
579
            ]);
580
581
            $this->resetOrderBy();
582
583
            $this->queries->push([
584
                'method'    => 'orderBy',
585
                'arguments' => [
586
                    $relation->getRelated()->getTable().'.'.$relationColumn,
587
                    $this->sort['type'],
588
                ],
589
            ]);
590
        }
591
    }
592
593
    /**
594
     * Reset orderBy query.
595
     *
596
     * @return void
597
     */
598
    public function resetOrderBy()
599
    {
600
        $this->queries = $this->queries->reject(function ($query) {
601
            return $query['method'] == 'orderBy' || $query['method'] == 'orderByDesc';
602
        });
603
    }
604
605
    /**
606
     * Build join parameters for related model.
607
     *
608
     * `HasOne` and `BelongsTo` relation has different join parameters.
609
     *
610
     * @param Relation $relation
611
     *
612
     * @throws \Exception
613
     *
614
     * @return array
615
     */
616
    protected function joinParameters(Relation $relation)
617
    {
618
        $relatedTable = $relation->getRelated()->getTable();
619
620
        if ($relation instanceof BelongsTo) {
621
            $foreignKeyMethod = version_compare(app()->version(), '5.8.0', '<') ? 'getForeignKey' : 'getForeignKeyName';
622
623
            return [
624
                $relatedTable,
625
                $relation->{$foreignKeyMethod}(),
626
                '=',
627
                $relatedTable.'.'.$relation->getRelated()->getKeyName(),
628
            ];
629
        }
630
631
        if ($relation instanceof HasOne) {
632
            return [
633
                $relatedTable,
634
                $relation->getQualifiedParentKeyName(),
635
                '=',
636
                $relation->getQualifiedForeignKeyName(),
637
            ];
638
        }
639
640
        throw new \Exception('Related sortable only support `HasOne` and `BelongsTo` relation.');
641
    }
642
643
    /**
644
     * @param string $method
645
     * @param array  $arguments
646
     *
647
     * @return $this
648
     */
649
    public function __call($method, $arguments)
650
    {
651
        $this->queries->push([
652
            'method'    => $method,
653
            'arguments' => $arguments,
654
        ]);
655
656
        return $this;
657
    }
658
659
    /**
660
     * Set the relationships that should be eager loaded.
661
     *
662
     * @param mixed $relations
663
     *
664
     * @return $this|Model
665
     */
666
    public function with($relations)
667
    {
668
        if (is_array($relations)) {
669
            if (Arr::isAssoc($relations)) {
670
                $relations = array_keys($relations);
671
            }
672
673
            $this->eagerLoads = array_merge($this->eagerLoads, $relations);
674
        }
675
676
        if (is_string($relations)) {
677
            if (Str::contains($relations, '.')) {
678
                $relations = explode('.', $relations)[0];
679
            }
680
681
            if (Str::contains($relations, ':')) {
682
                $relations = explode(':', $relations)[0];
683
            }
684
685
            if (in_array($relations, $this->eagerLoads)) {
686
                return $this;
687
            }
688
689
            $this->eagerLoads[] = $relations;
690
        }
691
692
        return $this->__call('with', (array) $relations);
693
    }
694
695
    /**
696
     * @param $key
697
     *
698
     * @return mixed
699
     */
700
    public function __get($key)
701
    {
702
        $data = $this->buildData();
703
704
        if (array_key_exists($key, $data)) {
705
            return $data[$key];
706
        }
707
    }
708
}
709