Completed
Pull Request — master (#4611)
by xiaoqiang
03:10 queued 36s
created

Model::get()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 7
nop 0
dl 0
loc 33
rs 8.7697
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\Collection;
14
use Illuminate\Support\Facades\Request;
15
use Illuminate\Support\Str;
16
17
class Model
18
{
19
    /**
20
     * Eloquent model instance of the grid model.
21
     *
22
     * @var EloquentModel
23
     */
24
    protected $model;
25
26
    /**
27
     * @var EloquentModel
28
     */
29
    protected $originalModel;
30
31
    /**
32
     * Array of queries of the eloquent model.
33
     *
34
     * @var \Illuminate\Support\Collection
35
     */
36
    protected $queries;
37
38
    /**
39
     * Sort parameters of the model.
40
     *
41
     * @var array
42
     */
43
    protected $sort;
44
45
    /**
46
     * @var array
47
     */
48
    protected $data = [];
49
50
    /*
51
     * 20 items per page as default.
52
     *
53
     * @var int
54
     */
55
    protected $perPage = 20;
56
57
    /**
58
     * If the model use pagination.
59
     *
60
     * @var bool
61
     */
62
    protected $usePaginate = true;
63
64
    /**
65
     * The query string variable used to store the per-page.
66
     *
67
     * @var string
68
     */
69
    protected $perPageName = 'per_page';
70
71
    /**
72
     * The query string variable used to store the sort.
73
     *
74
     * @var string
75
     */
76
    protected $sortName = '_sort';
77
78
    /**
79
     * Collection callback.
80
     *
81
     * @var \Closure
82
     */
83
    protected $collectionCallback;
84
85
    /**
86
     * @var Grid
87
     */
88
    protected $grid;
89
90
    /**
91
     * @var Relation
92
     */
93
    protected $relation;
94
95
    /**
96
     * @var array
97
     */
98
    protected $eagerLoads = [];
99
100
    /**
101
     * Create a new grid model instance.
102
     *
103
     * @param EloquentModel $model
104
     * @param Grid          $grid
105
     */
106
    public function __construct(EloquentModel $model, Grid $grid = null)
107
    {
108
        $this->model = $model;
109
110
        $this->originalModel = $model;
111
112
        $this->grid = $grid;
113
114
        $this->queries = collect();
115
    }
116
117
    /**
118
     * @return EloquentModel
119
     */
120
    public function getOriginalModel()
121
    {
122
        return $this->originalModel;
123
    }
124
125
    /**
126
     * Get the eloquent model of the grid model.
127
     *
128
     * @return EloquentModel
129
     */
130
    public function eloquent()
131
    {
132
        return $this->model;
133
    }
134
135
    /**
136
     * Enable or disable pagination.
137
     *
138
     * @param bool $use
139
     */
140
    public function usePaginate($use = true)
141
    {
142
        $this->usePaginate = $use;
143
    }
144
145
    /**
146
     * Get the query string variable used to store the per-page.
147
     *
148
     * @return string
149
     */
150
    public function getPerPageName()
151
    {
152
        return $this->perPageName;
153
    }
154
155
    /**
156
     * Set the query string variable used to store the per-page.
157
     *
158
     * @param string $name
159
     *
160
     * @return $this
161
     */
162
    public function setPerPageName($name)
163
    {
164
        $this->perPageName = $name;
165
166
        return $this;
167
    }
168
169
    /**
170
     * Get per-page number.
171
     *
172
     * @return int
173
     */
174
    public function getPerPage()
175
    {
176
        return $this->perPage;
177
    }
178
179
    /**
180
     * Set per-page number.
181
     *
182
     * @param int $perPage
183
     *
184
     * @return $this
185
     */
186
    public function setPerPage($perPage)
187
    {
188
        $this->perPage = $perPage;
189
190
        $this->__call('paginate', [$perPage]);
191
192
        return $this;
193
    }
194
195
    /**
196
     * Get the query string variable used to store the sort.
197
     *
198
     * @return string
199
     */
200
    public function getSortName()
201
    {
202
        return $this->sortName;
203
    }
204
205
    /**
206
     * Set the query string variable used to store the sort.
207
     *
208
     * @param string $name
209
     *
210
     * @return $this
211
     */
212
    public function setSortName($name)
213
    {
214
        $this->sortName = $name;
215
216
        return $this;
217
    }
218
219
    /**
220
     * Set parent grid instance.
221
     *
222
     * @param Grid $grid
223
     *
224
     * @return $this
225
     */
226
    public function setGrid(Grid $grid)
227
    {
228
        $this->grid = $grid;
229
230
        return $this;
231
    }
232
233
    /**
234
     * Get parent gird instance.
235
     *
236
     * @return Grid
237
     */
238
    public function getGrid()
239
    {
240
        return $this->grid;
241
    }
242
243
    /**
244
     * @param Relation $relation
245
     *
246
     * @return $this
247
     */
248
    public function setRelation(Relation $relation)
249
    {
250
        $this->relation = $relation;
251
252
        return $this;
253
    }
254
255
    /**
256
     * @return Relation
257
     */
258
    public function getRelation()
259
    {
260
        return $this->relation;
261
    }
262
263
    /**
264
     * Get constraints.
265
     *
266
     * @return array|bool
267
     */
268
    public function getConstraints()
269
    {
270
        if ($this->relation instanceof HasMany) {
271
            return [
272
                $this->relation->getForeignKeyName() => $this->relation->getParentKey(),
273
            ];
274
        }
275
276
        return false;
277
    }
278
279
    /**
280
     * Set collection callback.
281
     *
282
     * @param \Closure $callback
283
     *
284
     * @return $this
285
     */
286
    public function collection(\Closure $callback = null)
287
    {
288
        $this->collectionCallback = $callback;
289
290
        return $this;
291
    }
292
293
    /**
294
     * Build.
295
     *
296
     * @param bool $toArray
297
     *
298
     * @return array|Collection|mixed
299
     */
300
    public function buildData($toArray = true)
301
    {
302
        if (empty($this->data)) {
303
            $collection = $this->get();
304
305
            if ($this->collectionCallback) {
306
                $collection = call_user_func($this->collectionCallback, $collection);
307
            }
308
309
            if ($toArray) {
310
                $this->data = $collection->toArray();
311
            } else {
312
                $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...
313
            }
314
        }
315
316
        return $this->data;
317
    }
318
319
    /**
320
     * @param callable $callback
321
     * @param int      $count
322
     *
323
     * @return bool
324
     */
325
    public function chunk($callback, $count = 100)
326
    {
327
        if ($this->usePaginate) {
328
            return $this->buildData(false)->chunk($count)->each($callback);
329
        }
330
331
        $this->setSort();
332
333
        $this->queries->reject(function ($query) {
334
            return $query['method'] == 'paginate';
335
        })->each(function ($query) {
336
            $this->model = $this->model->{$query['method']}(...$query['arguments']);
337
        });
338
339
        return $this->model->chunk($count, $callback);
340
    }
341
342
    /**
343
     * Add conditions to grid model.
344
     *
345
     * @param array $conditions
346
     *
347
     * @return $this
348
     */
349
    public function addConditions(array $conditions)
350
    {
351
        foreach ($conditions as $condition) {
352
            call_user_func_array([$this, key($condition)], current($condition));
353
        }
354
355
        return $this;
356
    }
357
358
    /**
359
     * Get table of the model.
360
     *
361
     * @return string
362
     */
363
    public function getTable()
364
    {
365
        return $this->model->getTable();
366
    }
367
368
    /**
369
     * @throws \Exception
370
     *
371
     * @return Collection
372
     */
373
    protected function get()
374
    {
375
        if ($this->model instanceof LengthAwarePaginator) {
376
            return $this->model;
377
        }
378
379
        if ($this->relation) {
380
            $this->model = $this->relation->getQuery();
381
        }
382
383
        $this->setSort();
384
        $this->setPaginate();
385
386
        $this->queries->unique()->each(function ($query) {
387
            if ($query['method'] == 'with') {
388
                $this->model = $this->model->with($query['arguments']);
389
            } else {
390
                $this->model = call_user_func_array([$this->model, $query['method']], $query['arguments']);
391
            }
392
        });
393
394
        if ($this->model instanceof Collection) {
395
            return $this->model;
396
        }
397
398
        if ($this->model instanceof LengthAwarePaginator) {
399
            $this->handleInvalidPage($this->model);
400
401
            return $this->model->getCollection();
402
        }
403
404
        throw new \Exception('Grid query error');
405
    }
406
407
    /**
408
     * @return \Illuminate\Database\Eloquent\Builder|EloquentModel
409
     */
410
    public function getQueryBuilder()
411
    {
412
        if ($this->relation) {
413
            return $this->relation->getQuery();
414
        }
415
416
        $this->setSort();
417
418
        $queryBuilder = $this->originalModel;
419
420
        $this->queries->reject(function ($query) {
421
            return in_array($query['method'], ['get', 'paginate']);
422
        })->each(function ($query) use (&$queryBuilder) {
423
            $queryBuilder = $queryBuilder->{$query['method']}(...$query['arguments']);
424
        });
425
426
        return $queryBuilder;
427
    }
428
429
    /**
430
     * If current page is greater than last page, then redirect to last page.
431
     *
432
     * @param LengthAwarePaginator $paginator
433
     *
434
     * @return void
435
     */
436
    protected function handleInvalidPage(LengthAwarePaginator $paginator)
437
    {
438
        if ($paginator->lastPage() && $paginator->currentPage() > $paginator->lastPage()) {
439
            $lastPageUrl = Request::fullUrlWithQuery([
440
                $paginator->getPageName() => $paginator->lastPage(),
441
            ]);
442
443
            Pjax::respond(redirect($lastPageUrl));
444
        }
445
    }
446
447
    /**
448
     * Set the grid paginate.
449
     *
450
     * @return void
451
     */
452
    protected function setPaginate()
453
    {
454
        $paginate = $this->findQueryByMethod('paginate');
455
456
        $this->queries = $this->queries->reject(function ($query) {
457
            return $query['method'] == 'paginate';
458
        });
459
460
        if (!$this->usePaginate) {
461
            $query = [
462
                'method'    => 'get',
463
                'arguments' => [],
464
            ];
465
        } else {
466
            $query = [
467
                'method'    => 'paginate',
468
                '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...
469
            ];
470
        }
471
472
        $this->queries->push($query);
473
    }
474
475
    /**
476
     * Resolve perPage for pagination.
477
     *
478
     * @param array|null $paginate
479
     *
480
     * @return array
481
     */
482
    protected function resolvePerPage($paginate)
483
    {
484
        if ($perPage = request($this->perPageName)) {
485
            if (is_array($paginate)) {
486
                $paginate['arguments'][0] = (int) $perPage;
487
488
                return $paginate['arguments'];
489
            }
490
491
            $this->perPage = (int) $perPage;
492
        }
493
494
        if (isset($paginate['arguments'][0])) {
495
            return $paginate['arguments'];
496
        }
497
498
        if ($name = $this->grid->getName()) {
499
            return [$this->perPage, ['*'], "{$name}_page"];
500
        }
501
502
        return [$this->perPage];
503
    }
504
505
    /**
506
     * Find query by method name.
507
     *
508
     * @param $method
509
     *
510
     * @return static
511
     */
512
    protected function findQueryByMethod($method)
513
    {
514
        return $this->queries->first(function ($query) use ($method) {
515
            return $query['method'] == $method;
516
        });
517
    }
518
519
    /**
520
     * Set the grid sort.
521
     *
522
     * @return void
523
     */
524
    protected function setSort()
525
    {
526
        $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...
527
        if (!is_array($this->sort)) {
528
            return;
529
        }
530
531
        $columnName = $this->sort['column'] ?? null;
532
        if ($columnName === null || empty($this->sort['type'])) {
533
            return;
534
        }
535
536
        $columnNameContainsDots = Str::contains($columnName, '.');
537
        $isRelation = $this->queries->contains(function ($query) use ($columnName) {
538
            return $query['method'] === 'with' && in_array($columnName, $query['arguments'], true);
539
        });
540
        if ($columnNameContainsDots === true && $isRelation) {
541
            $this->setRelationSort($columnName);
542
        } else {
543
            $this->resetOrderBy();
544
545
            if ($columnNameContainsDots === true) {
546
                //json
547
                $this->resetOrderBy();
548
                $explodedCols = explode('.', $this->sort['column']);
549
                $col = array_shift($explodedCols);
550
                $parts = implode('.', $explodedCols);
551
                $columnName = "{$col}->>'$.{$parts}'";
552
            }
553
554
            // get column. if contains "cast", set set column as cast
555
            if (!empty($this->sort['cast'])) {
556
                $column = "CAST({$columnName} AS {$this->sort['cast']}) {$this->sort['type']}";
557
                $method = 'orderByRaw';
558
                $arguments = [$column];
559
            } else {
560
                $column = $columnName;
561
                $method = 'orderBy';
562
                $arguments = [$column, $this->sort['type']];
563
            }
564
565
            $this->queries->push([
566
                'method'    => $method,
567
                'arguments' => $arguments,
568
            ]);
569
        }
570
    }
571
572
    /**
573
     * Set relation sort.
574
     *
575
     * @param string $column
576
     *
577
     * @return void
578
     */
579
    protected function setRelationSort($column)
580
    {
581
        list($relationName, $relationColumn) = explode('.', $column);
582
583
        if ($this->queries->contains(function ($query) use ($relationName) {
584
            return $query['method'] == 'with' && in_array($relationName, $query['arguments']);
585
        })) {
586
            $relation = $this->model->$relationName();
587
588
            $this->queries->push([
589
                'method'    => 'select',
590
                'arguments' => [$this->model->getTable().'.*'],
591
            ]);
592
593
            $this->queries->push([
594
                'method'    => 'join',
595
                'arguments' => $this->joinParameters($relation),
596
            ]);
597
598
            $this->resetOrderBy();
599
600
            $this->queries->push([
601
                'method'    => 'orderBy',
602
                'arguments' => [
603
                    $relation->getRelated()->getTable().'.'.$relationColumn,
604
                    $this->sort['type'],
605
                ],
606
            ]);
607
        }
608
    }
609
610
    /**
611
     * Reset orderBy query.
612
     *
613
     * @return void
614
     */
615
    public function resetOrderBy()
616
    {
617
        $this->queries = $this->queries->reject(function ($query) {
618
            return $query['method'] == 'orderBy' || $query['method'] == 'orderByDesc';
619
        });
620
    }
621
622
    /**
623
     * Build join parameters for related model.
624
     *
625
     * `HasOne` and `BelongsTo` relation has different join parameters.
626
     *
627
     * @param Relation $relation
628
     *
629
     * @throws \Exception
630
     *
631
     * @return array
632
     */
633
    protected function joinParameters(Relation $relation)
634
    {
635
        $relatedTable = $relation->getRelated()->getTable();
636
637
        if ($relation instanceof BelongsTo) {
638
            $foreignKeyMethod = version_compare(app()->version(), '5.8.0', '<') ? 'getForeignKey' : 'getForeignKeyName';
639
640
            return [
641
                $relatedTable,
642
                $relation->{$foreignKeyMethod}(),
643
                '=',
644
                $relatedTable.'.'.$relation->getRelated()->getKeyName(),
645
            ];
646
        }
647
648
        if ($relation instanceof HasOne) {
649
            return [
650
                $relatedTable,
651
                $relation->getQualifiedParentKeyName(),
652
                '=',
653
                $relation->getQualifiedForeignKeyName(),
654
            ];
655
        }
656
657
        throw new \Exception('Related sortable only support `HasOne` and `BelongsTo` relation.');
658
    }
659
660
    /**
661
     * @param string $method
662
     * @param array  $arguments
663
     *
664
     * @return $this
665
     */
666
    public function __call($method, $arguments)
667
    {
668
        $this->queries->push([
669
            'method'    => $method,
670
            'arguments' => $arguments,
671
        ]);
672
673
        return $this;
674
    }
675
676
    /**
677
     * Set the relationships that should be eager loaded.
678
     *
679
     * @param mixed $relations
680
     *
681
     * @return $this|Model
682
     */
683
    public function with($relations)
684
    {
685
        if (is_array($relations)) {
686
            $this->eagerLoads = array_merge($this->eagerLoads, $relations);
687
        }
688
689
        if (is_string($relations)) {
690
            if (Str::contains($relations, '.')) {
691
                $relations = explode('.', $relations)[0];
692
            }
693
694
            if (Str::contains($relations, ':')) {
695
                $relations = explode(':', $relations)[0];
696
            }
697
698
            if (in_array($relations, $this->eagerLoads)) {
699
                return $this;
700
            }
701
702
            $this->eagerLoads[] = $relations;
703
        }
704
705
        return $this->__call('with', (array) $relations);
706
    }
707
708
    /**
709
     * @param $key
710
     *
711
     * @return mixed
712
     */
713
    public function __get($key)
714
    {
715
        $data = $this->buildData();
716
717
        if (array_key_exists($key, $data)) {
718
            return $data[$key];
719
        }
720
    }
721
}
722