Completed
Push — master ( 814918...02f15d )
by Song
02:23
created

Column::define()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin\Grid;
4
5
use Closure;
6
use Encore\Admin\Admin;
7
use Encore\Admin\Grid;
8
use Encore\Admin\Grid\Displayers\AbstractDisplayer;
9
use Illuminate\Contracts\Support\Arrayable;
10
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...
11
use Illuminate\Support\Arr;
12
use Illuminate\Support\Collection;
13
use Illuminate\Support\Str;
14
15
/**
16
 * Class Column
17
 *
18
 * @method Displayers\Editable      editable()
19
 * @method Displayers\SwitchDisplay switch ($states = [])
20
 * @method Displayers\SwitchGroup   switchGroup($columns = [], $states = [])
21
 * @method Displayers\Select        select($options = [])
22
 * @method Displayers\Image         image($server = '', $width = 200, $height = 200)
23
 * @method Displayers\Label         label($style = 'success')
24
 * @method Displayers\Button        button($style = null)
25
 * @method Displayers\Link          link($href = '', $target = '_blank')
26
 * @method Displayers\Badge         badge($style = 'red')
27
 * @method Displayers\ProgressBar   progressBar($style = 'primary', $size = 'sm', $max = 100)
28
 * @method Displayers\Radio         radio($options = [])
29
 * @method Displayers\Checkbox      checkbox($options = [])
30
 * @method Displayers\Orderable     orderable($column, $label = '')
31
 * @method Displayers\Table         table($titles = [])
32
 * @method Displayers\Expand        expand($callback = null)
33
 * @method Displayers\Modal         modal($callback = null)
34
 * @method Displayers\Gravatar      gravatar($size = 30)
35
 * @method Displayers\Carousel      carousel(int $width = 300, int $height = 200, $server = '')
36
 */
37
class Column
38
{
39
    const SELECT_COLUMN_NAME = '__row_selector__';
40
41
    /**
42
     * @var Grid
43
     */
44
    protected $grid;
45
46
    /**
47
     * Name of column.
48
     *
49
     * @var string
50
     */
51
    protected $name;
52
53
    /**
54
     * Label of column.
55
     *
56
     * @var string
57
     */
58
    protected $label;
59
60
    /**
61
     * Original value of column.
62
     *
63
     * @var mixed
64
     */
65
    protected $original;
66
67
    /**
68
     * Is column sortable.
69
     *
70
     * @var bool
71
     */
72
    protected $sortable = false;
73
74
    /**
75
     * Sort arguments.
76
     *
77
     * @var array
78
     */
79
    protected $sort;
80
81
    /**
82
     * Help message.
83
     *
84
     * @var string
85
     */
86
    protected $help = '';
87
88
    /**
89
     * Cast Name.
90
     *
91
     * @var array
92
     */
93
    protected $cast;
94
95
    /**
96
     * Attributes of column.
97
     *
98
     * @var array
99
     */
100
    protected $attributes = [];
101
102
    /**
103
     * Relation name.
104
     *
105
     * @var bool
106
     */
107
    protected $relation = false;
108
109
    /**
110
     * Relation column.
111
     *
112
     * @var string
113
     */
114
    protected $relationColumn;
115
116
    /**
117
     * Original grid data.
118
     *
119
     * @var Collection
120
     */
121
    protected static $originalGridModels;
122
123
    /**
124
     * @var []Closure
125
     */
126
    protected $displayCallbacks = [];
127
128
    /**
129
     * Displayers for grid column.
130
     *
131
     * @var array
132
     */
133
    public static $displayers = [];
134
135
    /**
136
     * Defined columns.
137
     *
138
     * @var array
139
     */
140
    public static $defined = [];
141
142
    /**
143
     * @var array
144
     */
145
    protected static $htmlAttributes = [];
146
147
    /**
148
     * @var Model
149
     */
150
    protected static $model;
151
152
    /**
153
     * @param string $name
154
     * @param string $label
155
     */
156
    public function __construct($name, $label)
157
    {
158
        $this->name = $name;
159
160
        $this->label = $this->formatLabel($label);
161
    }
162
163
    /**
164
     * Extend column displayer.
165
     *
166
     * @param $name
167
     * @param $displayer
168
     */
169
    public static function extend($name, $displayer)
170
    {
171
        static::$displayers[$name] = $displayer;
172
    }
173
174
    /**
175
     * Define a column globally.
176
     *
177
     * @param string $name
178
     * @param mixed  $definition
179
     */
180
    public static function define($name, $definition)
181
    {
182
        static::$defined[$name] = $definition;
183
    }
184
185
    /**
186
     * Set grid instance for column.
187
     *
188
     * @param Grid $grid
189
     */
190
    public function setGrid(Grid $grid)
191
    {
192
        $this->grid = $grid;
193
194
        $this->setModel($grid->model()->eloquent());
195
    }
196
197
    /**
198
     * Set model for column.
199
     *
200
     * @param $model
201
     */
202
    public function setModel($model)
203
    {
204
        if (is_null(static::$model) && ($model instanceof Model)) {
205
            static::$model = $model->newInstance();
206
        }
207
    }
208
209
    /**
210
     * Set original data for column.
211
     *
212
     * @param Collection $collection
213
     */
214
    public static function setOriginalGridModels(Collection $collection)
215
    {
216
        static::$originalGridModels = $collection;
217
    }
218
219
    /**
220
     * Set column attributes.
221
     *
222
     * @param array $attributes
223
     *
224
     * @return $this
225
     */
226
    public function setAttributes($attributes = [])
227
    {
228
        static::$htmlAttributes[$this->name] = $attributes;
229
230
        return $this;
231
    }
232
233
    /**
234
     * Get column attributes.
235
     *
236
     * @param string $name
237
     *
238
     * @return mixed
239
     */
240
    public static function getAttributes($name)
241
    {
242
        return Arr::get(static::$htmlAttributes, $name, '');
243
    }
244
245
    /**
246
     * Set style of this column.
247
     *
248
     * @param string $style
249
     *
250
     * @return Column
251
     */
252
    public function style($style)
253
    {
254
        return $this->setAttributes(compact('style'));
255
    }
256
257
    /**
258
     * Set the width of column.
259
     *
260
     * @param int $width
261
     *
262
     * @return Column
263
     */
264
    public function width(int $width)
265
    {
266
        return $this->style("width: {$width}px;");
267
    }
268
269
    /**
270
     * Get name of this column.
271
     *
272
     * @return mixed
273
     */
274
    public function getName()
275
    {
276
        return $this->name;
277
    }
278
279
    /**
280
     * Format label.
281
     *
282
     * @param $label
283
     *
284
     * @return mixed
285
     */
286
    protected function formatLabel($label)
287
    {
288
        $label = $label ?: ucfirst($this->name);
289
290
        return str_replace(['.', '_'], ' ', $label);
291
    }
292
293
    /**
294
     * Get label of the column.
295
     *
296
     * @return mixed
297
     */
298
    public function getLabel()
299
    {
300
        return $this->label;
301
    }
302
303
    /**
304
     * Set relation.
305
     *
306
     * @param string $relation
307
     * @param string $relationColumn
308
     *
309
     * @return $this
310
     */
311
    public function setRelation($relation, $relationColumn = null)
312
    {
313
        $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...
314
        $this->relationColumn = $relationColumn;
315
316
        return $this;
317
    }
318
319
    /**
320
     * If this column is relation column.
321
     *
322
     * @return bool
323
     */
324
    protected function isRelation()
325
    {
326
        return (bool) $this->relation;
327
    }
328
329
    /**
330
     * Set sort value.
331
     *
332
     * @param bool $sort
333
     *
334
     * @return Column
335
     */
336
    public function sort($sort)
337
    {
338
        $this->sortable = $sort;
339
340
        return $this;
341
    }
342
343
    /**
344
     * Mark this column as sortable.
345
     *
346
     * @return Column
347
     */
348
    public function sortable()
349
    {
350
        return $this->sort(true);
351
    }
352
353
    /**
354
     * Set cast name for sortable.
355
     *
356
     * @return Column
357
     */
358
    public function cast($cast)
359
    {
360
        $this->cast = $cast;
361
362
        return $this;
363
    }
364
365
    /**
366
     * Add a display callback.
367
     *
368
     * @param Closure $callback
369
     *
370
     * @return $this
371
     */
372
    public function display(Closure $callback)
373
    {
374
        $this->displayCallbacks[] = $callback;
375
376
        return $this;
377
    }
378
379
    /**
380
     * Display using display abstract.
381
     *
382
     * @param string $abstract
383
     * @param array  $arguments
384
     *
385
     * @return Column
386
     */
387
    public function displayUsing($abstract, $arguments = [])
388
    {
389
        $grid = $this->grid;
390
391
        $column = $this;
392
393 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...
394
            /** @var AbstractDisplayer $displayer */
395
            $displayer = new $abstract($value, $grid, $column, $this);
396
397
            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...
398
        });
399
    }
400
401
    /**
402
     * Display column using array value map.
403
     *
404
     * @param array $values
405
     * @param null  $default
406
     *
407
     * @return $this
408
     */
409 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...
410
    {
411
        return $this->display(function ($value) use ($values, $default) {
412
            if (is_null($value)) {
413
                return $default;
414
            }
415
416
            return Arr::get($values, $value, $default);
417
        });
418
    }
419
420
    /**
421
     * Render this column with the given view.
422
     *
423
     * @param string $view
424
     *
425
     * @return $this
426
     */
427
    public function view($view)
428
    {
429
        return $this->display(function ($value) use ($view) {
430
            $model = $this;
431
432
            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...
433
        });
434
    }
435
436
    /**
437
     * Add column to total-row.
438
     *
439
     * @param null $display
440
     *
441
     * @return $this
442
     */
443
    public function totalRow($display = null)
444
    {
445
        $this->grid->addTotalRow($this->name, $display);
0 ignored issues
show
Documentation introduced by
$display is of type null, but the function expects a object<Closure>.

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...
446
447
        return $this;
448
    }
449
450
    /**
451
     * If has display callbacks.
452
     *
453
     * @return bool
454
     */
455
    protected function hasDisplayCallbacks()
456
    {
457
        return !empty($this->displayCallbacks);
458
    }
459
460
    /**
461
     * Call all of the "display" callbacks column.
462
     *
463
     * @param mixed $value
464
     * @param int   $key
465
     *
466
     * @return mixed
467
     */
468
    protected function callDisplayCallbacks($value, $key)
469
    {
470
        foreach ($this->displayCallbacks as $callback) {
471
            $previous = $value;
472
473
            $callback = $this->bindOriginalRowModel($callback, $key);
474
            $value = call_user_func_array($callback, [$value, $this]);
475
476
            if (($value instanceof static) &&
477
                ($last = array_pop($this->displayCallbacks))
478
            ) {
479
                $last = $this->bindOriginalRowModel($last, $key);
480
                $value = call_user_func($last, $previous);
481
            }
482
        }
483
484
        return $value;
485
    }
486
487
    /**
488
     * Set original grid data to column.
489
     *
490
     * @param Closure $callback
491
     * @param int     $key
492
     *
493
     * @return Closure
494
     */
495
    protected function bindOriginalRowModel(Closure $callback, $key)
496
    {
497
        $rowModel = static::$originalGridModels[$key];
498
499
        return $callback->bindTo($rowModel);
500
    }
501
502
    /**
503
     * Fill all data to every column.
504
     *
505
     * @param array $data
506
     *
507
     * @return mixed
508
     */
509
    public function fill(array $data)
510
    {
511
        foreach ($data as $key => &$row) {
512
            $this->original = $value = Arr::get($row, $this->name);
513
514
            $value = $this->htmlEntityEncode($value);
515
516
            Arr::set($row, $this->name, $value);
517
518
            if ($this->isDefinedColumn()) {
519
                $this->useDefinedColumn();
520
            }
521
522
            if ($this->hasDisplayCallbacks()) {
523
                $value = $this->callDisplayCallbacks($this->original, $key);
524
                Arr::set($row, $this->name, $value);
525
            }
526
        }
527
528
        return $data;
529
    }
530
531
    /**
532
     * If current column is a defined column.
533
     *
534
     * @return bool
535
     */
536
    protected function isDefinedColumn()
537
    {
538
        return array_key_exists($this->name, static::$defined);
539
    }
540
541
    /**
542
     * Use a defined column.
543
     *
544
     * @throws \Exception
545
     */
546
    protected function useDefinedColumn()
547
    {
548
        // clear all display callbacks.
549
        $this->displayCallbacks = [];
550
551
        $class = static::$defined[$this->name];
552
553
        if ($class instanceof Closure) {
554
            $this->display($class);
555
556
            return;
557
        }
558
559
        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...
560
            throw new \Exception("Invalid column definition [$class]");
561
        }
562
563
        $grid = $this->grid;
564
        $column = $this;
565
566
        $this->display(function ($value) use ($grid, $column, $class) {
567
            /** @var AbstractDisplayer $definition */
568
            $definition = new $class($value, $grid, $column, $this);
569
570
            return $definition->display();
571
        });
572
    }
573
574
    /**
575
     * Convert characters to HTML entities recursively.
576
     *
577
     * @param array|string $item
578
     *
579
     * @return mixed
580
     */
581
    protected function htmlEntityEncode($item)
582
    {
583
        if (is_array($item)) {
584
            array_walk_recursive($item, function (&$value) {
585
                $value = htmlentities($value);
586
            });
587
        } else {
588
            $item = htmlentities($item);
589
        }
590
591
        return $item;
592
    }
593
594
    /**
595
     * Create the column sorter.
596
     *
597
     * @return string
598
     */
599
    public function sorter()
600
    {
601
        if (!$this->sortable) {
602
            return '';
603
        }
604
605
        $icon = 'fa-sort';
606
        $type = 'desc';
607
608
        if ($this->isSorted()) {
609
            $type = $this->sort['type'] == 'desc' ? 'asc' : 'desc';
610
            $icon .= "-amount-{$this->sort['type']}";
611
        }
612
613
        // set sort value
614
        $sort = ['column' => $this->name, 'type' => $type];
615
        if (isset($this->cast)) {
616
            $sort['cast'] = $this->cast;
617
        }
618
619
        $query = app('request')->all();
620
        $query = array_merge($query, [$this->grid->model()->getSortName() => $sort]);
621
622
        $url = url()->current().'?'.http_build_query($query);
623
624
        return "<a class=\"fa fa-fw $icon\" href=\"$url\"></a>";
625
    }
626
627
    /**
628
     * Determine if this column is currently sorted.
629
     *
630
     * @return bool
631
     */
632
    protected function isSorted()
633
    {
634
        $this->sort = app('request')->get($this->grid->model()->getSortName());
635
636
        if (empty($this->sort)) {
637
            return false;
638
        }
639
640
        return isset($this->sort['column']) && $this->sort['column'] == $this->name;
641
    }
642
643
    /**
644
     * Set help message for column.
645
     *
646
     * @param string $help
647
     *
648
     * @return $this|string
649
     */
650
    public function help($help = '')
651
    {
652
        if (!empty($help)) {
653
            $this->help = $help;
654
655
            return $this;
656
        }
657
658
        if (empty($this->help)) {
659
            return '';
660
        }
661
662
        Admin::script("$('.column-help').popover();");
663
664
        return <<<HELP
665
<a href="javascript:void(0);" class="column-help" data-container="body" data-toggle="popover" data-trigger="hover" data-placement="bottom" data-content="{$this->help}">
666
    <i class="fa fa-question-circle"></i>
667
</a>
668
HELP;
669
    }
670
671
    /**
672
     * Find a displayer to display column.
673
     *
674
     * @param string $abstract
675
     * @param array  $arguments
676
     *
677
     * @return Column
678
     */
679
    protected function resolveDisplayer($abstract, $arguments)
680
    {
681
        if (array_key_exists($abstract, static::$displayers)) {
682
            return $this->callBuiltinDisplayer(static::$displayers[$abstract], $arguments);
683
        }
684
685
        return $this->callSupportDisplayer($abstract, $arguments);
686
    }
687
688
    /**
689
     * Call Illuminate/Support displayer.
690
     *
691
     * @param string $abstract
692
     * @param array  $arguments
693
     *
694
     * @return Column
695
     */
696
    protected function callSupportDisplayer($abstract, $arguments)
697
    {
698
        return $this->display(function ($value) use ($abstract, $arguments) {
699
            if (is_array($value) || $value instanceof Arrayable) {
700
                return call_user_func_array([collect($value), $abstract], $arguments);
701
            }
702
703
            if (is_string($value)) {
704
                return call_user_func_array([Str::class, $abstract], array_merge([$value], $arguments));
705
            }
706
707
            return $value;
708
        });
709
    }
710
711
    /**
712
     * Call Builtin displayer.
713
     *
714
     * @param string $abstract
715
     * @param array  $arguments
716
     *
717
     * @return Column
718
     */
719
    protected function callBuiltinDisplayer($abstract, $arguments)
720
    {
721
        if ($abstract instanceof Closure) {
722
            return $this->display(function ($value) use ($abstract, $arguments) {
723
                return $abstract->call($this, ...array_merge([$value], $arguments));
724
            });
725
        }
726
727
        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...
728
            $grid = $this->grid;
729
            $column = $this;
730
731 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...
732
                /** @var AbstractDisplayer $displayer */
733
                $displayer = new $abstract($value, $grid, $column, $this);
734
735
                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...
736
            });
737
        }
738
739
        return $this;
740
    }
741
742
    /**
743
     * Passes through all unknown calls to builtin displayer or supported displayer.
744
     *
745
     * Allow fluent calls on the Column object.
746
     *
747
     * @param string $method
748
     * @param array  $arguments
749
     *
750
     * @return $this
751
     */
752
    public function __call($method, $arguments)
753
    {
754
        if ($this->isRelation() && !$this->relationColumn) {
755
            $this->name = "{$this->relation}.$method";
756
            $this->label = isset($arguments[0]) ? $arguments[0] : ucfirst($method);
757
758
            $this->relationColumn = $method;
759
760
            return $this;
761
        }
762
763
        return $this->resolveDisplayer($method, $arguments);
764
    }
765
}
766