Completed
Push — master ( 1c9dad...840a3e )
by Song
03:08
created

Model::with()   B

Complexity

Conditions 7
Paths 27

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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