Completed
Pull Request — master (#4611)
by xiaoqiang
02:28
created

Model::setPerPageName()   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
            if ($query['method'] == 'with') {
389
                $this->model = $this->model->with($query['arguments']);
390
            } else {
391
                $this->model = call_user_func_array([$this->model, $query['method']], $query['arguments']);
392
            }
393
        });
394
395
        if ($this->model instanceof Collection) {
396
            return $this->model;
397
        }
398
399
        if ($this->model instanceof LengthAwarePaginator) {
400
            $this->handleInvalidPage($this->model);
401
402
            return $this->model->getCollection();
403
        }
404
405
        throw new \Exception('Grid query error');
406
    }
407
408
    /**
409
     * @return \Illuminate\Database\Eloquent\Builder|EloquentModel
410
     */
411
    public function getQueryBuilder()
412
    {
413
        if ($this->relation) {
414
            return $this->relation->getQuery();
415
        }
416
417
        $this->setSort();
418
419
        $queryBuilder = $this->originalModel;
420
421
        $this->queries->reject(function ($query) {
422
            return in_array($query['method'], ['get', 'paginate']);
423
        })->each(function ($query) use (&$queryBuilder) {
424
            $queryBuilder = $queryBuilder->{$query['method']}(...$query['arguments']);
425
        });
426
427
        return $queryBuilder;
428
    }
429
430
    /**
431
     * If current page is greater than last page, then redirect to last page.
432
     *
433
     * @param LengthAwarePaginator $paginator
434
     *
435
     * @return void
436
     */
437
    protected function handleInvalidPage(LengthAwarePaginator $paginator)
438
    {
439
        if ($paginator->lastPage() && $paginator->currentPage() > $paginator->lastPage()) {
440
            $lastPageUrl = Request::fullUrlWithQuery([
441
                $paginator->getPageName() => $paginator->lastPage(),
442
            ]);
443
444
            Pjax::respond(redirect($lastPageUrl));
445
        }
446
    }
447
448
    /**
449
     * Set the grid paginate.
450
     *
451
     * @return void
452
     */
453
    protected function setPaginate()
454
    {
455
        $paginate = $this->findQueryByMethod('paginate');
456
457
        $this->queries = $this->queries->reject(function ($query) {
458
            return $query['method'] == 'paginate';
459
        });
460
461
        if (!$this->usePaginate) {
462
            $query = [
463
                'method'    => 'get',
464
                'arguments' => [],
465
            ];
466
        } else {
467
            $query = [
468
                'method'    => 'paginate',
469
                '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...
470
            ];
471
        }
472
473
        $this->queries->push($query);
474
    }
475
476
    /**
477
     * Resolve perPage for pagination.
478
     *
479
     * @param array|null $paginate
480
     *
481
     * @return array
482
     */
483
    protected function resolvePerPage($paginate)
484
    {
485
        if ($perPage = request($this->perPageName)) {
486
            if (is_array($paginate)) {
487
                $paginate['arguments'][0] = (int) $perPage;
488
489
                return $paginate['arguments'];
490
            }
491
492
            $this->perPage = (int) $perPage;
493
        }
494
495
        if (isset($paginate['arguments'][0])) {
496
            return $paginate['arguments'];
497
        }
498
499
        if ($name = $this->grid->getName()) {
500
            return [$this->perPage, ['*'], "{$name}_page"];
501
        }
502
503
        return [$this->perPage];
504
    }
505
506
    /**
507
     * Find query by method name.
508
     *
509
     * @param $method
510
     *
511
     * @return static
512
     */
513
    protected function findQueryByMethod($method)
514
    {
515
        return $this->queries->first(function ($query) use ($method) {
516
            return $query['method'] == $method;
517
        });
518
    }
519
520
    /**
521
     * Set the grid sort.
522
     *
523
     * @return void
524
     */
525
    protected function setSort()
526
    {
527
        $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...
528
        if (!is_array($this->sort)) {
529
            return;
530
        }
531
532
        $columnName = $this->sort['column'] ?? null;
533
        if ($columnName === null || empty($this->sort['type'])) {
534
            return;
535
        }
536
537
        $columnNameContainsDots = Str::contains($columnName, '.');
538
        $isRelation = $this->queries->contains(function ($query) use ($columnName) {
539
            return $query['method'] === 'with' && in_array($columnName, $query['arguments'], true);
540
        });
541
        if ($columnNameContainsDots === true && $isRelation) {
542
            $this->setRelationSort($columnName);
543
        } else {
544
            $this->resetOrderBy();
545
546
            if ($columnNameContainsDots === true) {
547
                //json
548
                $this->resetOrderBy();
549
                $explodedCols = explode('.', $this->sort['column']);
550
                $col = array_shift($explodedCols);
551
                $parts = implode('.', $explodedCols);
552
                $columnName = "{$col}->>'$.{$parts}'";
553
            }
554
555
            // get column. if contains "cast", set set column as cast
556
            if (!empty($this->sort['cast'])) {
557
                $column = "CAST({$columnName} AS {$this->sort['cast']}) {$this->sort['type']}";
558
                $method = 'orderByRaw';
559
                $arguments = [$column];
560
            } else {
561
                $column = $columnName;
562
                $method = 'orderBy';
563
                $arguments = [$column, $this->sort['type']];
564
            }
565
566
            $this->queries->push([
567
                'method'    => $method,
568
                'arguments' => $arguments,
569
            ]);
570
        }
571
    }
572
573
    /**
574
     * Set relation sort.
575
     *
576
     * @param string $column
577
     *
578
     * @return void
579
     */
580
    protected function setRelationSort($column)
581
    {
582
        list($relationName, $relationColumn) = explode('.', $column);
583
584
        if ($this->queries->contains(function ($query) use ($relationName) {
585
            return $query['method'] == 'with' && in_array($relationName, $query['arguments']);
586
        })) {
587
            $relation = $this->model->$relationName();
588
589
            $this->queries->push([
590
                'method'    => 'select',
591
                'arguments' => [$this->model->getTable().'.*'],
592
            ]);
593
594
            $this->queries->push([
595
                'method'    => 'join',
596
                'arguments' => $this->joinParameters($relation),
597
            ]);
598
599
            $this->resetOrderBy();
600
601
            $this->queries->push([
602
                'method'    => 'orderBy',
603
                'arguments' => [
604
                    $relation->getRelated()->getTable().'.'.$relationColumn,
605
                    $this->sort['type'],
606
                ],
607
            ]);
608
        }
609
    }
610
611
    /**
612
     * Reset orderBy query.
613
     *
614
     * @return void
615
     */
616
    public function resetOrderBy()
617
    {
618
        $this->queries = $this->queries->reject(function ($query) {
619
            return $query['method'] == 'orderBy' || $query['method'] == 'orderByDesc';
620
        });
621
    }
622
623
    /**
624
     * Build join parameters for related model.
625
     *
626
     * `HasOne` and `BelongsTo` relation has different join parameters.
627
     *
628
     * @param Relation $relation
629
     *
630
     * @throws \Exception
631
     *
632
     * @return array
633
     */
634
    protected function joinParameters(Relation $relation)
635
    {
636
        $relatedTable = $relation->getRelated()->getTable();
637
638
        if ($relation instanceof BelongsTo) {
639
            $foreignKeyMethod = version_compare(app()->version(), '5.8.0', '<') ? 'getForeignKey' : 'getForeignKeyName';
640
641
            return [
642
                $relatedTable,
643
                $relation->{$foreignKeyMethod}(),
644
                '=',
645
                $relatedTable.'.'.$relation->getRelated()->getKeyName(),
646
            ];
647
        }
648
649
        if ($relation instanceof HasOne) {
650
            return [
651
                $relatedTable,
652
                $relation->getQualifiedParentKeyName(),
653
                '=',
654
                $relation->getQualifiedForeignKeyName(),
655
            ];
656
        }
657
658
        throw new \Exception('Related sortable only support `HasOne` and `BelongsTo` relation.');
659
    }
660
661
    /**
662
     * @param string $method
663
     * @param array  $arguments
664
     *
665
     * @return $this
666
     */
667
    public function __call($method, $arguments)
668
    {
669
        $this->queries->push([
670
            'method'    => $method,
671
            'arguments' => $arguments,
672
        ]);
673
674
        return $this;
675
    }
676
677
    /**
678
     * Set the relationships that should be eager loaded.
679
     *
680
     * @param mixed $relations
681
     *
682
     * @return $this|Model
683
     */
684
    public function with($relations)
685
    {
686
        if (is_array($relations)) {
687
            $this->eagerLoads = array_merge($this->eagerLoads, $relations);
688
        }
689
690
        if (is_string($relations)) {
691
            if (Str::contains($relations, '.')) {
692
                $relations = explode('.', $relations)[0];
693
            }
694
695
            if (Str::contains($relations, ':')) {
696
                $relations = explode(':', $relations)[0];
697
            }
698
699
            if (in_array($relations, $this->eagerLoads)) {
700
                return $this;
701
            }
702
703
            $this->eagerLoads[] = $relations;
704
        }
705
706
        return $this->__call('with', (array) $relations);
707
    }
708
709
    /**
710
     * @param $key
711
     *
712
     * @return mixed
713
     */
714
    public function __get($key)
715
    {
716
        $data = $this->buildData();
717
718
        if (array_key_exists($key, $data)) {
719
            return $data[$key];
720
        }
721
    }
722
}
723