Completed
Push — master ( f82786...e63f05 )
by Song
04:44 queued 01:41
created

Column::sort()   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 Closure;
6
use Encore\Admin\Grid;
7
use Encore\Admin\Grid\Displayers\AbstractDisplayer;
8
use Illuminate\Contracts\Support\Arrayable;
9
use Illuminate\Database\Eloquent\Model;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Encore\Admin\Grid\Model.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
10
use Illuminate\Support\Collection;
11
use Illuminate\Support\Str;
12
13
class Column
14
{
15
    /**
16
     * @var Grid
17
     */
18
    protected $grid;
19
20
    /**
21
     * Name of column.
22
     *
23
     * @var string
24
     */
25
    protected $name;
26
27
    /**
28
     * Label of column.
29
     *
30
     * @var string
31
     */
32
    protected $label;
33
34
    /**
35
     * Original value of column.
36
     *
37
     * @var mixed
38
     */
39
    protected $original;
40
41
    /**
42
     * Is column sortable.
43
     *
44
     * @var bool
45
     */
46
    protected $sortable = false;
47
48
    /**
49
     * Sort arguments.
50
     *
51
     * @var array
52
     */
53
    protected $sort;
54
55
    /**
56
     * Attributes of column.
57
     *
58
     * @var array
59
     */
60
    protected $attributes = [];
61
62
    /**
63
     * Relation name.
64
     *
65
     * @var bool
66
     */
67
    protected $relation = false;
68
69
    /**
70
     * Relation column.
71
     *
72
     * @var string
73
     */
74
    protected $relationColumn;
75
76
    /**
77
     * Original grid data.
78
     *
79
     * @var Collection
80
     */
81
    protected static $originalGridModels;
82
83
    /**
84
     * @var []Closure
85
     */
86
    protected $displayCallbacks = [];
87
88
    /**
89
     * Displayers for grid column.
90
     *
91
     * @var array
92
     */
93
    public static $displayers = [];
94
95
    /**
96
     * Defined columns.
97
     *
98
     * @var array
99
     */
100
    public static $defined = [];
101
102
    /**
103
     * @var array
104
     */
105
    protected static $htmlAttributes = [];
106
107
    /**
108
     * @var Model
109
     */
110
    protected static $model;
111
112
    const SELECT_COLUMN_NAME = '__row_selector__';
113
114
    /**
115
     * @param string $name
116
     * @param string $label
117
     */
118
    public function __construct($name, $label)
119
    {
120
        $this->name = $name;
121
122
        $this->label = $this->formatLabel($label);
123
    }
124
125
    /**
126
     * Extend column displayer.
127
     *
128
     * @param $name
129
     * @param $displayer
130
     */
131
    public static function extend($name, $displayer)
132
    {
133
        static::$displayers[$name] = $displayer;
134
    }
135
136
    /**
137
     * Define a column globally.
138
     *
139
     * @param string $name
140
     * @param mixed  $definition
141
     */
142
    public static function define($name, $definition)
143
    {
144
        static::$defined[$name] = $definition;
145
    }
146
147
    /**
148
     * Set grid instance for column.
149
     *
150
     * @param Grid $grid
151
     */
152
    public function setGrid(Grid $grid)
153
    {
154
        $this->grid = $grid;
155
156
        $this->setModel($grid->model()->eloquent());
157
    }
158
159
    /**
160
     * Set model for column.
161
     *
162
     * @param $model
163
     */
164
    public function setModel($model)
165
    {
166
        if (is_null(static::$model) && ($model instanceof Model)) {
167
            static::$model = $model->newInstance();
168
        }
169
    }
170
171
    /**
172
     * Set original data for column.
173
     *
174
     * @param Collection $collection
175
     */
176
    public static function setOriginalGridModels(Collection $collection)
177
    {
178
        static::$originalGridModels = $collection;
179
    }
180
181
    /**
182
     * Set column attributes.
183
     *
184
     * @param array $attributes
185
     *
186
     * @return $this
187
     */
188
    public function setAttributes($attributes = [])
189
    {
190
        static::$htmlAttributes[$this->name] = $attributes;
191
192
        return $this;
193
    }
194
195
    /**
196
     * Get column attributes.
197
     *
198
     * @param string $name
199
     *
200
     * @return mixed
201
     */
202
    public static function getAttributes($name)
203
    {
204
        return array_get(static::$htmlAttributes, $name, '');
205
    }
206
207
    /**
208
     * Set style of this column.
209
     *
210
     * @param string $style
211
     *
212
     * @return Column
213
     */
214
    public function style($style)
215
    {
216
        return $this->setAttributes(compact('style'));
217
    }
218
219
    /**
220
     * Get name of this column.
221
     *
222
     * @return mixed
223
     */
224
    public function getName()
225
    {
226
        return $this->name;
227
    }
228
229
    /**
230
     * Format label.
231
     *
232
     * @param $label
233
     *
234
     * @return mixed
235
     */
236
    protected function formatLabel($label)
237
    {
238
        $label = $label ?: ucfirst($this->name);
239
240
        return str_replace(['.', '_'], ' ', $label);
241
    }
242
243
    /**
244
     * Get label of the column.
245
     *
246
     * @return mixed
247
     */
248
    public function getLabel()
249
    {
250
        return $this->label;
251
    }
252
253
    /**
254
     * Set relation.
255
     *
256
     * @param string $relation
257
     * @param string $relationColumn
258
     *
259
     * @return $this
260
     */
261
    public function setRelation($relation, $relationColumn = null)
262
    {
263
        $this->relation = $relation;
0 ignored issues
show
Documentation Bug introduced by
The property $relation was declared of type boolean, but $relation is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
264
        $this->relationColumn = $relationColumn;
265
266
        return $this;
267
    }
268
269
    /**
270
     * If this column is relation column.
271
     *
272
     * @return bool
273
     */
274
    protected function isRelation()
275
    {
276
        return (bool) $this->relation;
277
    }
278
279
    /**
280
     * Set sort value.
281
     *
282
     * @param bool $sort
283
     *
284
     * @return Column
285
     */
286
    public function sort($sort)
287
    {
288
        $this->sortable = $sort;
289
290
        return $this;
291
    }
292
293
    /**
294
     * Mark this column as sortable.
295
     *
296
     * @return Column
297
     */
298
    public function sortable()
299
    {
300
        return $this->sort(true);
301
    }
302
303
    /**
304
     * Add a display callback.
305
     *
306
     * @param Closure $callback
307
     *
308
     * @return $this
309
     */
310
    public function display(Closure $callback)
311
    {
312
        $this->displayCallbacks[] = $callback;
313
314
        return $this;
315
    }
316
317
    /**
318
     * Display using display abstract.
319
     *
320
     * @param string $abstract
321
     * @param array  $arguments
322
     *
323
     * @return Column
324
     */
325
    public function displayUsing($abstract, $arguments = [])
326
    {
327
        $grid = $this->grid;
328
329
        $column = $this;
330
331 View Code Duplication
        return $this->display(function ($value) use ($grid, $column, $abstract, $arguments) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
332
            /** @var AbstractDisplayer $displayer */
333
            $displayer = new $abstract($value, $grid, $column, $this);
334
335
            return $displayer->display(...$arguments);
0 ignored issues
show
Unused Code introduced by
The call to AbstractDisplayer::display() has too many arguments starting with $arguments.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
336
        });
337
    }
338
339
    /**
340
     * Display column using array value map.
341
     *
342
     * @param array $values
343
     * @param null  $default
344
     *
345
     * @return $this
346
     */
347 View Code Duplication
    public function using(array $values, $default = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
348
    {
349
        return $this->display(function ($value) use ($values, $default) {
350
            if (is_null($value)) {
351
                return $default;
352
            }
353
354
            return array_get($values, $value, $default);
355
        });
356
    }
357
358
    /**
359
     * Render this column with the given view.
360
     *
361
     * @param string $view
362
     *
363
     * @return $this
364
     */
365
    public function view($view)
366
    {
367
        return $this->display(function ($value) use ($view) {
368
            $model = $this;
369
370
            return view($view, compact('model', 'value'))->render();
0 ignored issues
show
Bug introduced by
The method render does only exist in Illuminate\View\View, but not in Illuminate\Contracts\View\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
371
        });
372
    }
373
374
    /**
375
     * If has display callbacks.
376
     *
377
     * @return bool
378
     */
379
    protected function hasDisplayCallbacks()
380
    {
381
        return !empty($this->displayCallbacks);
382
    }
383
384
    /**
385
     * Call all of the "display" callbacks column.
386
     *
387
     * @param mixed $value
388
     * @param int   $key
389
     *
390
     * @return mixed
391
     */
392
    protected function callDisplayCallbacks($value, $key)
393
    {
394
        foreach ($this->displayCallbacks as $callback) {
395
            $previous = $value;
396
397
            $callback = $this->bindOriginalRowModel($callback, $key);
398
            $value = call_user_func_array($callback, [$value, $this]);
399
400
            if (($value instanceof static) &&
401
                ($last = array_pop($this->displayCallbacks))
402
            ) {
403
                $last = $this->bindOriginalRowModel($last, $key);
404
                $value = call_user_func($last, $previous);
405
            }
406
        }
407
408
        return $value;
409
    }
410
411
    /**
412
     * Set original grid data to column.
413
     *
414
     * @param Closure $callback
415
     * @param int     $key
416
     *
417
     * @return Closure
418
     */
419
    protected function bindOriginalRowModel(Closure $callback, $key)
420
    {
421
        $rowModel = static::$originalGridModels[$key];
422
423
        return $callback->bindTo($rowModel);
424
    }
425
426
    /**
427
     * Fill all data to every column.
428
     *
429
     * @param array $data
430
     *
431
     * @return mixed
432
     */
433
    public function fill(array $data)
434
    {
435
        foreach ($data as $key => &$row) {
436
            $this->original = $value = array_get($row, $this->name);
437
438
            $value = $this->htmlEntityEncode($value);
439
440
            array_set($row, $this->name, $value);
441
442
            if ($this->isDefinedColumn()) {
443
                $this->useDefinedColumn();
444
            }
445
446
            if ($this->hasDisplayCallbacks()) {
447
                $value = $this->callDisplayCallbacks($this->original, $key);
448
                array_set($row, $this->name, $value);
449
            }
450
        }
451
452
        return $data;
453
    }
454
455
    /**
456
     * If current column is a defined column.
457
     *
458
     * @return bool
459
     */
460
    protected function isDefinedColumn()
461
    {
462
        return array_key_exists($this->name, static::$defined);
463
    }
464
465
    /**
466
     * Use a defined column.
467
     *
468
     * @throws \Exception
469
     */
470
    protected function useDefinedColumn()
471
    {
472
        // clear all display callbacks.
473
        $this->displayCallbacks = [];
474
475
        $class = static::$defined[$this->name];
476
477
        if ($class instanceof Closure) {
478
            $this->display($class);
479
480
            return;
481
        }
482
483
        if (!class_exists($class) || !is_subclass_of($class, AbstractDisplayer::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Encore\Admin\Grid\Displ...bstractDisplayer::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
484
            throw new \Exception("Invalid column definition [$class]");
485
        }
486
487
        $grid = $this->grid;
488
        $column = $this;
489
490
        $this->display(function ($value) use ($grid, $column, $class) {
491
            /** @var AbstractDisplayer $definition */
492
            $definition = new $class($value, $grid, $column, $this);
493
494
            return $definition->display();
495
        });
496
    }
497
498
    /**
499
     * Convert characters to HTML entities recursively.
500
     *
501
     * @param array|string $item
502
     *
503
     * @return mixed
504
     */
505
    protected function htmlEntityEncode($item)
506
    {
507
        if (is_array($item)) {
508
            array_walk_recursive($item, function (&$value) {
509
                $value = htmlentities($value);
510
            });
511
        } else {
512
            $item = htmlentities($item);
513
        }
514
515
        return $item;
516
    }
517
518
    /**
519
     * Create the column sorter.
520
     *
521
     * @return string
522
     */
523
    public function sorter()
524
    {
525
        if (!$this->sortable) {
526
            return '';
527
        }
528
529
        $icon = 'fa-sort';
530
        $type = 'desc';
531
532
        if ($this->isSorted()) {
533
            $type = $this->sort['type'] == 'desc' ? 'asc' : 'desc';
534
            $icon .= "-amount-{$this->sort['type']}";
535
        }
536
537
        $query = app('request')->all();
538
        $query = array_merge($query, [$this->grid->model()->getSortName() => ['column' => $this->name, 'type' => $type]]);
539
540
        $url = url()->current().'?'.http_build_query($query);
541
542
        return "<a class=\"fa fa-fw $icon\" href=\"$url\"></a>";
543
    }
544
545
    /**
546
     * Determine if this column is currently sorted.
547
     *
548
     * @return bool
549
     */
550
    protected function isSorted()
551
    {
552
        $this->sort = app('request')->get($this->grid->model()->getSortName());
553
554
        if (empty($this->sort)) {
555
            return false;
556
        }
557
558
        return isset($this->sort['column']) && $this->sort['column'] == $this->name;
559
    }
560
561
    /**
562
     * Find a displayer to display column.
563
     *
564
     * @param string $abstract
565
     * @param array  $arguments
566
     *
567
     * @return Column
568
     */
569
    protected function resolveDisplayer($abstract, $arguments)
570
    {
571
        if (array_key_exists($abstract, static::$displayers)) {
572
            return $this->callBuiltinDisplayer(static::$displayers[$abstract], $arguments);
573
        }
574
575
        return $this->callSupportDisplayer($abstract, $arguments);
576
    }
577
578
    /**
579
     * Call Illuminate/Support displayer.
580
     *
581
     * @param string $abstract
582
     * @param array  $arguments
583
     *
584
     * @return Column
585
     */
586
    protected function callSupportDisplayer($abstract, $arguments)
587
    {
588
        return $this->display(function ($value) use ($abstract, $arguments) {
589
            if (is_array($value) || $value instanceof Arrayable) {
590
                return call_user_func_array([collect($value), $abstract], $arguments);
591
            }
592
593
            if (is_string($value)) {
594
                return call_user_func_array([Str::class, $abstract], array_merge([$value], $arguments));
595
            }
596
597
            return $value;
598
        });
599
    }
600
601
    /**
602
     * Call Builtin displayer.
603
     *
604
     * @param string $abstract
605
     * @param array  $arguments
606
     *
607
     * @return Column
608
     */
609
    protected function callBuiltinDisplayer($abstract, $arguments)
610
    {
611
        if ($abstract instanceof Closure) {
612
            return $this->display(function ($value) use ($abstract, $arguments) {
613
                return $abstract->call($this, ...array_merge([$value], $arguments));
614
            });
615
        }
616
617
        if (class_exists($abstract) && is_subclass_of($abstract, AbstractDisplayer::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Encore\Admin\Grid\Displ...bstractDisplayer::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
618
            $grid = $this->grid;
619
            $column = $this;
620
621 View Code Duplication
            return $this->display(function ($value) use ($abstract, $grid, $column, $arguments) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
622
                /** @var AbstractDisplayer $displayer */
623
                $displayer = new $abstract($value, $grid, $column, $this);
624
625
                return $displayer->display(...$arguments);
0 ignored issues
show
Unused Code introduced by
The call to AbstractDisplayer::display() has too many arguments starting with $arguments.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
626
            });
627
        }
628
629
        return $this;
630
    }
631
632
    /**
633
     * Passes through all unknown calls to builtin displayer or supported displayer.
634
     *
635
     * Allow fluent calls on the Column object.
636
     *
637
     * @param string $method
638
     * @param array  $arguments
639
     *
640
     * @return $this
641
     */
642
    public function __call($method, $arguments)
643
    {
644
        if ($this->isRelation() && !$this->relationColumn) {
645
            $this->name = "{$this->relation}.$method";
646
            $this->label = isset($arguments[0]) ? $arguments[0] : ucfirst($method);
647
648
            $this->relationColumn = $method;
649
650
            return $this;
651
        }
652
653
        return $this->resolveDisplayer($method, $arguments);
654
    }
655
}
656