Completed
Pull Request — master (#2695)
by
unknown
23:03 queued 17s
created

Model::joinParameters()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 24
rs 9.536
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\Input;
16
use Illuminate\Support\Facades\Request;
17
use Illuminate\Support\Str;
18
19
class Model
20
{
21
    /**
22
     * Eloquent model instance of the grid model.
23
     *
24
     * @var EloquentModel
25
     */
26
    protected $model;
27
28
    /**
29
     * @var EloquentModel
30
     */
31
    protected $originalModel;
32
33
    /**
34
     * Array of queries of the eloquent model.
35
     *
36
     * @var \Illuminate\Support\Collection
37
     */
38
    protected $queries;
39
40
    /**
41
     * Sort parameters of the model.
42
     *
43
     * @var array
44
     */
45
    protected $sort;
46
47
    /**
48
     * @var array
49
     */
50
    protected $data = [];
51
52
    /*
53
     * 20 items per page as default.
54
     *
55
     * @var int
56
     */
57
    protected $perPage = 20;
58
59
    /**
60
     * If the model use pagination.
61
     *
62
     * @var bool
63
     */
64
    protected $usePaginate = true;
65
66
    /**
67
     * The query string variable used to store the per-page.
68
     *
69
     * @var string
70
     */
71
    protected $perPageName = 'per_page';
72
73
    /**
74
     * The query string variable used to store the sort.
75
     *
76
     * @var string
77
     */
78
    protected $sortName = '_sort';
79
80
    /**
81
     * Collection callback.
82
     *
83
     * @var \Closure
84
     */
85
    protected $collectionCallback;
86
87
    /**
88
     * @var Grid
89
     */
90
    protected $grid;
91
92
    /**
93
     * @var Relation
94
     */
95
    protected $relation;
96
97
    /**
98
     * @var array
99
     */
100
    protected $eagerLoads = [];
101
102
    /**
103
     * Create a new grid model instance.
104
     *
105
     * @param EloquentModel $model
106
     */
107
    public function __construct(EloquentModel $model)
108
    {
109
        $this->model = $model;
110
111
        $this->originalModel = $model;
112
113
        $this->queries = collect();
114
115
//        static::doNotSnakeAttributes($this->model);
116
    }
117
118
    /**
119
     * Don't snake case attributes.
120
     *
121
     * @param EloquentModel $model
122
     *
123
     * @return void
124
     */
125
    protected static function doNotSnakeAttributes(EloquentModel $model)
126
    {
127
        $class = get_class($model);
128
129
        $class::$snakeAttributes = false;
130
    }
131
132
    /**
133
     * Get the eloquent model of the grid model.
134
     *
135
     * @return EloquentModel
136
     */
137
    public function eloquent()
138
    {
139
        return $this->model;
140
    }
141
142
    /**
143
     * Enable or disable pagination.
144
     *
145
     * @param bool $use
146
     */
147
    public function usePaginate($use = true)
148
    {
149
        $this->usePaginate = $use;
150
    }
151
152
    /**
153
     * Get the query string variable used to store the per-page.
154
     *
155
     * @return string
156
     */
157
    public function getPerPageName()
158
    {
159
        return $this->perPageName;
160
    }
161
162
    /**
163
     * Set the query string variable used to store the per-page.
164
     *
165
     * @param string $name
166
     *
167
     * @return $this
168
     */
169
    public function setPerPageName($name)
170
    {
171
        $this->perPageName = $name;
172
173
        return $this;
174
    }
175
176
    /**
177
     * Get the query string variable used to store the sort.
178
     *
179
     * @return string
180
     */
181
    public function getSortName()
182
    {
183
        return $this->sortName;
184
    }
185
186
    /**
187
     * Set the query string variable used to store the sort.
188
     *
189
     * @param string $name
190
     *
191
     * @return $this
192
     */
193
    public function setSortName($name)
194
    {
195
        $this->sortName = $name;
196
197
        return $this;
198
    }
199
200
    /**
201
     * Set parent grid instance.
202
     *
203
     * @param Grid $grid
204
     *
205
     * @return $this
206
     */
207
    public function setGrid(Grid $grid)
208
    {
209
        $this->grid = $grid;
210
211
        return $this;
212
    }
213
214
    /**
215
     * Get parent gird instance.
216
     *
217
     * @return Grid
218
     */
219
    public function getGrid()
220
    {
221
        return $this->grid;
222
    }
223
224
    /**
225
     * @param Relation $relation
226
     *
227
     * @return $this
228
     */
229
    public function setRelation(Relation $relation)
230
    {
231
        $this->relation = $relation;
232
233
        return $this;
234
    }
235
236
    /**
237
     * @return Relation
238
     */
239
    public function getRelation()
240
    {
241
        return $this->relation;
242
    }
243
244
    /**
245
     * Get constraints.
246
     *
247
     * @return array|bool
248
     */
249
    public function getConstraints()
250
    {
251
        if ($this->relation instanceof HasMany) {
252
            return [
253
                $this->relation->getForeignKeyName() => $this->relation->getParentKey(),
254
            ];
255
        }
256
257
        return false;
258
    }
259
260
    /**
261
     * Set collection callback.
262
     *
263
     * @param \Closure $callback
264
     *
265
     * @return $this
266
     */
267
    public function collection(\Closure $callback = null)
268
    {
269
        $this->collectionCallback = $callback;
270
271
        return $this;
272
    }
273
274
    /**
275
     * Build.
276
     *
277
     * @param bool $toArray
278
     *
279
     * @return array|Collection|mixed
280
     */
281
    public function buildData($toArray = true)
282
    {
283
        if (empty($this->data)) {
284
            $collection = $this->get();
285
286
            if ($this->collectionCallback) {
287
                $collection = call_user_func($this->collectionCallback, $collection);
288
            }
289
290
            if ($toArray) {
291
                $this->data = $collection->toArray();
292
            } else {
293
                $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...
294
            }
295
        }
296
297
        return $this->data;
298
    }
299
300
    /**
301
     * @param callable $callback
302
     * @param int      $count
303
     *
304
     * @return bool
305
     */
306
    public function chunk($callback, $count = 100)
307
    {
308
        if ($this->usePaginate) {
309
            return $this->buildData(false)->chunk($count)->each($callback);
310
        }
311
312
        $this->setSort();
313
314
        $this->queries->reject(function ($query) {
315
            return $query['method'] == 'paginate';
316
        })->each(function ($query) {
317
            $this->model = $this->model->{$query['method']}(...$query['arguments']);
318
        });
319
320
        return $this->model->chunk($count, $callback);
321
    }
322
323
    /**
324
     * Add conditions to grid model.
325
     *
326
     * @param array $conditions
327
     *
328
     * @return $this
329
     */
330
    public function addConditions(array $conditions)
331
    {
332
        foreach ($conditions as $condition) {
333
            call_user_func_array([$this, key($condition)], current($condition));
334
        }
335
336
        return $this;
337
    }
338
339
    /**
340
     * Get table of the model.
341
     *
342
     * @return string
343
     */
344
    public function getTable()
345
    {
346
        return $this->model->getTable();
347
    }
348
349
    /**
350
     * @throws \Exception
351
     *
352
     * @return Collection
353
     */
354
    protected function get()
355
    {
356
        if ($this->model instanceof LengthAwarePaginator) {
357
            return $this->model;
358
        }
359
360
        if ($this->relation) {
361
            $this->model = $this->relation->getQuery();
362
        }
363
364
        $this->setSort();
365
        $this->setPaginate();
366
367
        foreach ($this->queries->unique()->toArray() as $query) {
368
            $tmp_model = call_user_func_array([$this->model, $query['method']], $query['arguments']);
369
        }
370
        $this->model = $tmp_model;
0 ignored issues
show
Bug introduced by
The variable $tmp_model does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
371
372
        if ($this->model instanceof Collection) {
373
            return $this->model;
374
        }
375
376
        if ($this->model instanceof LengthAwarePaginator) {
377
            $this->handleInvalidPage($this->model);
378
379
            return $this->model->getCollection();
380
        }
381
382
        throw new \Exception('Grid query error');
383
    }
384
385
    /**
386
     * @return \Illuminate\Database\Eloquent\Builder|EloquentModel
387
     */
388
    public function getQueryBuilder()
389
    {
390
        if ($this->relation) {
391
            return $this->relation->getQuery();
392
        }
393
394
        $this->setSort();
395
396
        $queryBuilder = $this->originalModel;
397
398
        $this->queries->reject(function ($query) {
399
            return in_array($query['method'], ['get', 'paginate']);
400
        })->each(function ($query) use (&$queryBuilder) {
401
            $queryBuilder = $queryBuilder->{$query['method']}(...$query['arguments']);
402
        });
403
404
        return $queryBuilder;
405
    }
406
407
    /**
408
     * If current page is greater than last page, then redirect to last page.
409
     *
410
     * @param LengthAwarePaginator $paginator
411
     *
412
     * @return void
413
     */
414
    protected function handleInvalidPage(LengthAwarePaginator $paginator)
415
    {
416
        if ($paginator->lastPage() && $paginator->currentPage() > $paginator->lastPage()) {
417
            $lastPageUrl = Request::fullUrlWithQuery([
418
                $paginator->getPageName() => $paginator->lastPage(),
419
            ]);
420
421
            Pjax::respond(redirect($lastPageUrl));
422
        }
423
    }
424
425
    /**
426
     * Set the grid paginate.
427
     *
428
     * @return void
429
     */
430
    protected function setPaginate()
431
    {
432
        $paginate = $this->findQueryByMethod('paginate');
433
434
        $this->queries = $this->queries->reject(function ($query) {
435
            return $query['method'] == 'paginate';
436
        });
437
438
        if (!$this->usePaginate) {
439
            $query = [
440
                'method'    => 'get',
441
                'arguments' => [],
442
            ];
443
        } else {
444
            $query = [
445
                'method'    => 'paginate',
446
                '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...
447
            ];
448
        }
449
450
        $this->queries->push($query);
451
    }
452
453
    /**
454
     * Resolve perPage for pagination.
455
     *
456
     * @param array|null $paginate
457
     *
458
     * @return array
459
     */
460
    protected function resolvePerPage($paginate)
461
    {
462
        if ($perPage = app('request')->input($this->perPageName)) {
463
            if (is_array($paginate)) {
464
                $paginate['arguments'][0] = (int) $perPage;
465
466
                return $paginate['arguments'];
467
            }
468
469
            $this->perPage = (int) $perPage;
470
        }
471
472
        if (isset($paginate['arguments'][0])) {
473
            return $paginate['arguments'];
474
        }
475
476
        if ($name = $this->grid->getName()) {
477
            return [$this->perPage, null, "{$name}_page"];
478
        }
479
480
        return [$this->perPage];
481
    }
482
483
    /**
484
     * Find query by method name.
485
     *
486
     * @param $method
487
     *
488
     * @return static
489
     */
490
    protected function findQueryByMethod($method)
491
    {
492
        return $this->queries->first(function ($query) use ($method) {
493
            return $query['method'] == $method;
494
        });
495
    }
496
497
    /**
498
     * Set the grid sort.
499
     *
500
     * @return void
501
     */
502
    protected function setSort()
503
    {
504
        $this->sort = Input::get($this->sortName, []);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Illuminate\Support\Faca...his->sortName, array()) of type * is incompatible with the declared type array of property $sort.

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...
505
        if (!is_array($this->sort)) {
506
            return;
507
        }
508
509
        if (empty($this->sort['column']) || empty($this->sort['type'])) {
510
            return;
511
        }
512
513
        if (str_contains($this->sort['column'], '.')) {
514
            $this->setRelationSort($this->sort['column']);
515
        } else {
516
            $this->resetOrderBy();
517
518
            $this->queries->push([
519
                'method'    => 'orderBy',
520
                'arguments' => [$this->sort['column'], $this->sort['type']],
521
            ]);
522
        }
523
    }
524
525
    /**
526
     * Set relation sort.
527
     *
528
     * @param string $column
529
     *
530
     * @return void
531
     */
532
    protected function setRelationSort($column)
533
    {
534
        list($relationName, $relationColumn) = explode('.', $column);
535
536
        if ($this->queries->contains(function ($query) use ($relationName) {
537
            return $query['method'] == 'with' && in_array($relationName, $query['arguments']);
538
        })) {
539
            $relation = $this->model->$relationName();
540
541
            $this->queries->push([
542
                'method'    => 'select',
543
                'arguments' => [$this->model->getTable().'.*'],
544
            ]);
545
546
            $this->queries->push([
547
                'method'    => 'join',
548
                'arguments' => $this->joinParameters($relation),
549
            ]);
550
551
            $this->resetOrderBy();
552
553
            $this->queries->push([
554
                'method'    => 'orderBy',
555
                'arguments' => [
556
                    $relation->getRelated()->getTable().'.'.$relationColumn,
557
                    $this->sort['type'],
558
                ],
559
            ]);
560
        }
561
    }
562
563
    /**
564
     * Reset orderBy query.
565
     *
566
     * @return void
567
     */
568
    public function resetOrderBy()
569
    {
570
        $this->queries = $this->queries->reject(function ($query) {
571
            return $query['method'] == 'orderBy' || $query['method'] == 'orderByDesc';
572
        });
573
    }
574
575
    /**
576
     * Build join parameters for related model.
577
     *
578
     * `HasOne` and `BelongsTo` relation has different join parameters.
579
     *
580
     * @param Relation $relation
581
     *
582
     * @throws \Exception
583
     *
584
     * @return array
585
     */
586
    protected function joinParameters(Relation $relation)
587
    {
588
        $relatedTable = $relation->getRelated()->getTable();
589
590
        if ($relation instanceof BelongsTo) {
591
            return [
592
                $relatedTable,
593
                $relation->getForeignKey(),
594
                '=',
595
                $relatedTable.'.'.$relation->getRelated()->getKeyName(),
596
            ];
597
        }
598
599
        if ($relation instanceof HasOne) {
600
            return [
601
                $relatedTable,
602
                $relation->getQualifiedParentKeyName(),
603
                '=',
604
                $relation->getQualifiedForeignKeyName(),
605
            ];
606
        }
607
608
        throw new \Exception('Related sortable only support `HasOne` and `BelongsTo` relation.');
609
    }
610
611
    /**
612
     * @param string $method
613
     * @param array  $arguments
614
     *
615
     * @return $this
616
     */
617
    public function __call($method, $arguments)
618
    {
619
        $this->queries->push([
620
            'method'    => $method,
621
            'arguments' => $arguments,
622
        ]);
623
624
        return $this;
625
    }
626
627
    /**
628
     * Set the relationships that should be eager loaded.
629
     *
630
     * @param mixed $relations
631
     *
632
     * @return $this|Model
633
     */
634
    public function with($relations)
635
    {
636
        if (is_array($relations)) {
637
            if (Arr::isAssoc($relations)) {
638
                $relations = array_keys($relations);
639
            }
640
641
            $this->eagerLoads = array_merge($this->eagerLoads, $relations);
642
        }
643
644
        if (is_string($relations)) {
645
            if (Str::contains($relations, '.')) {
646
                $relations = explode('.', $relations)[0];
647
            }
648
649
            if (Str::contains($relations, ':')) {
650
                $relations = explode(':', $relations)[0];
651
            }
652
653
            if (in_array($relations, $this->eagerLoads)) {
654
                return $this;
655
            }
656
657
            $this->eagerLoads[] = $relations;
658
        }
659
660
        return $this->__call('with', (array) $relations);
661
    }
662
663
    /**
664
     * @param $key
665
     *
666
     * @return mixed
667
     */
668
    public function __get($key)
669
    {
670
        $data = $this->buildData();
671
672
        if (array_key_exists($key, $data)) {
673
            return $data[$key];
674
        }
675
    }
676
}
677