Completed
Push — master ( c6a049...7f7ede )
by Song
03:29
created

Column::totalRow()   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\Arr;
11
use Illuminate\Support\Collection;
12
use Illuminate\Support\Str;
13
14
class Column
15
{
16
    /**
17
     * @var Grid
18
     */
19
    protected $grid;
20
21
    /**
22
     * Name of column.
23
     *
24
     * @var string
25
     */
26
    protected $name;
27
28
    /**
29
     * Label of column.
30
     *
31
     * @var string
32
     */
33
    protected $label;
34
35
    /**
36
     * Original value of column.
37
     *
38
     * @var mixed
39
     */
40
    protected $original;
41
42
    /**
43
     * Is column sortable.
44
     *
45
     * @var bool
46
     */
47
    protected $sortable = false;
48
49
    /**
50
     * Sort arguments.
51
     *
52
     * @var array
53
     */
54
    protected $sort;
55
56
    /**
57
     * Cast Name.
58
     *
59
     * @var array
60
     */
61
    protected $cast;
62
63
    /**
64
     * Attributes of column.
65
     *
66
     * @var array
67
     */
68
    protected $attributes = [];
69
70
    /**
71
     * Relation name.
72
     *
73
     * @var bool
74
     */
75
    protected $relation = false;
76
77
    /**
78
     * Relation column.
79
     *
80
     * @var string
81
     */
82
    protected $relationColumn;
83
84
    /**
85
     * Original grid data.
86
     *
87
     * @var Collection
88
     */
89
    protected static $originalGridModels;
90
91
    /**
92
     * @var []Closure
93
     */
94
    protected $displayCallbacks = [];
95
96
    /**
97
     * Displayers for grid column.
98
     *
99
     * @var array
100
     */
101
    public static $displayers = [];
102
103
    /**
104
     * Defined columns.
105
     *
106
     * @var array
107
     */
108
    public static $defined = [];
109
110
    /**
111
     * @var array
112
     */
113
    protected static $htmlAttributes = [];
114
115
    /**
116
     * @var Model
117
     */
118
    protected static $model;
119
120
    const SELECT_COLUMN_NAME = '__row_selector__';
121
122
    /**
123
     * @param string $name
124
     * @param string $label
125
     */
126
    public function __construct($name, $label)
127
    {
128
        $this->name = $name;
129
130
        $this->label = $this->formatLabel($label);
131
    }
132
133
    /**
134
     * Extend column displayer.
135
     *
136
     * @param $name
137
     * @param $displayer
138
     */
139
    public static function extend($name, $displayer)
140
    {
141
        static::$displayers[$name] = $displayer;
142
    }
143
144
    /**
145
     * Define a column globally.
146
     *
147
     * @param string $name
148
     * @param mixed  $definition
149
     */
150
    public static function define($name, $definition)
151
    {
152
        static::$defined[$name] = $definition;
153
    }
154
155
    /**
156
     * Set grid instance for column.
157
     *
158
     * @param Grid $grid
159
     */
160
    public function setGrid(Grid $grid)
161
    {
162
        $this->grid = $grid;
163
164
        $this->setModel($grid->model()->eloquent());
165
    }
166
167
    /**
168
     * Set model for column.
169
     *
170
     * @param $model
171
     */
172
    public function setModel($model)
173
    {
174
        if (is_null(static::$model) && ($model instanceof Model)) {
175
            static::$model = $model->newInstance();
176
        }
177
    }
178
179
    /**
180
     * Set original data for column.
181
     *
182
     * @param Collection $collection
183
     */
184
    public static function setOriginalGridModels(Collection $collection)
185
    {
186
        static::$originalGridModels = $collection;
187
    }
188
189
    /**
190
     * Set column attributes.
191
     *
192
     * @param array $attributes
193
     *
194
     * @return $this
195
     */
196
    public function setAttributes($attributes = [])
197
    {
198
        static::$htmlAttributes[$this->name] = $attributes;
199
200
        return $this;
201
    }
202
203
    /**
204
     * Get column attributes.
205
     *
206
     * @param string $name
207
     *
208
     * @return mixed
209
     */
210
    public static function getAttributes($name)
211
    {
212
        return Arr::get(static::$htmlAttributes, $name, '');
213
    }
214
215
    /**
216
     * Set style of this column.
217
     *
218
     * @param string $style
219
     *
220
     * @return Column
221
     */
222
    public function style($style)
223
    {
224
        return $this->setAttributes(compact('style'));
225
    }
226
227
    /**
228
     * Get name of this column.
229
     *
230
     * @return mixed
231
     */
232
    public function getName()
233
    {
234
        return $this->name;
235
    }
236
237
    /**
238
     * Format label.
239
     *
240
     * @param $label
241
     *
242
     * @return mixed
243
     */
244
    protected function formatLabel($label)
245
    {
246
        $label = $label ?: ucfirst($this->name);
247
248
        return str_replace(['.', '_'], ' ', $label);
249
    }
250
251
    /**
252
     * Get label of the column.
253
     *
254
     * @return mixed
255
     */
256
    public function getLabel()
257
    {
258
        return $this->label;
259
    }
260
261
    /**
262
     * Set relation.
263
     *
264
     * @param string $relation
265
     * @param string $relationColumn
266
     *
267
     * @return $this
268
     */
269
    public function setRelation($relation, $relationColumn = null)
270
    {
271
        $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...
272
        $this->relationColumn = $relationColumn;
273
274
        return $this;
275
    }
276
277
    /**
278
     * If this column is relation column.
279
     *
280
     * @return bool
281
     */
282
    protected function isRelation()
283
    {
284
        return (bool) $this->relation;
285
    }
286
287
    /**
288
     * Set sort value.
289
     *
290
     * @param bool $sort
291
     *
292
     * @return Column
293
     */
294
    public function sort($sort)
295
    {
296
        $this->sortable = $sort;
297
298
        return $this;
299
    }
300
301
    /**
302
     * Mark this column as sortable.
303
     *
304
     * @return Column
305
     */
306
    public function sortable()
307
    {
308
        return $this->sort(true);
309
    }
310
311
    /**
312
     * Set cast name for sortable.
313
     *
314
     * @return Column
315
     */
316
    public function cast($cast)
317
    {
318
        $this->cast = $cast;
319
320
        return $this;
321
    }
322
323
    /**
324
     * Add a display callback.
325
     *
326
     * @param Closure $callback
327
     *
328
     * @return $this
329
     */
330
    public function display(Closure $callback)
331
    {
332
        $this->displayCallbacks[] = $callback;
333
334
        return $this;
335
    }
336
337
    /**
338
     * Display using display abstract.
339
     *
340
     * @param string $abstract
341
     * @param array  $arguments
342
     *
343
     * @return Column
344
     */
345
    public function displayUsing($abstract, $arguments = [])
346
    {
347
        $grid = $this->grid;
348
349
        $column = $this;
350
351 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...
352
            /** @var AbstractDisplayer $displayer */
353
            $displayer = new $abstract($value, $grid, $column, $this);
354
355
            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...
356
        });
357
    }
358
359
    /**
360
     * Display column using array value map.
361
     *
362
     * @param array $values
363
     * @param null  $default
364
     *
365
     * @return $this
366
     */
367 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...
368
    {
369
        return $this->display(function ($value) use ($values, $default) {
370
            if (is_null($value)) {
371
                return $default;
372
            }
373
374
            return Arr::get($values, $value, $default);
375
        });
376
    }
377
378
    /**
379
     * Render this column with the given view.
380
     *
381
     * @param string $view
382
     *
383
     * @return $this
384
     */
385
    public function view($view)
386
    {
387
        return $this->display(function ($value) use ($view) {
388
            $model = $this;
389
390
            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...
391
        });
392
    }
393
394
    /**
395
     * Add column to total-row.
396
     *
397
     * @param null $display
398
     *
399
     * @return $this
400
     */
401
    public function totalRow($display = null)
402
    {
403
        $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...
404
405
        return $this;
406
    }
407
408
    /**
409
     * If has display callbacks.
410
     *
411
     * @return bool
412
     */
413
    protected function hasDisplayCallbacks()
414
    {
415
        return !empty($this->displayCallbacks);
416
    }
417
418
    /**
419
     * Call all of the "display" callbacks column.
420
     *
421
     * @param mixed $value
422
     * @param int   $key
423
     *
424
     * @return mixed
425
     */
426
    protected function callDisplayCallbacks($value, $key)
427
    {
428
        foreach ($this->displayCallbacks as $callback) {
429
            $previous = $value;
430
431
            $callback = $this->bindOriginalRowModel($callback, $key);
432
            $value = call_user_func_array($callback, [$value, $this]);
433
434
            if (($value instanceof static) &&
435
                ($last = array_pop($this->displayCallbacks))
436
            ) {
437
                $last = $this->bindOriginalRowModel($last, $key);
438
                $value = call_user_func($last, $previous);
439
            }
440
        }
441
442
        return $value;
443
    }
444
445
    /**
446
     * Set original grid data to column.
447
     *
448
     * @param Closure $callback
449
     * @param int     $key
450
     *
451
     * @return Closure
452
     */
453
    protected function bindOriginalRowModel(Closure $callback, $key)
454
    {
455
        $rowModel = static::$originalGridModels[$key];
456
457
        return $callback->bindTo($rowModel);
458
    }
459
460
    /**
461
     * Fill all data to every column.
462
     *
463
     * @param array $data
464
     *
465
     * @return mixed
466
     */
467
    public function fill(array $data)
468
    {
469
        foreach ($data as $key => &$row) {
470
            $this->original = $value = Arr::get($row, $this->name);
471
472
            $value = $this->htmlEntityEncode($value);
473
474
            Arr::set($row, $this->name, $value);
475
476
            if ($this->isDefinedColumn()) {
477
                $this->useDefinedColumn();
478
            }
479
480
            if ($this->hasDisplayCallbacks()) {
481
                $value = $this->callDisplayCallbacks($this->original, $key);
482
                Arr::set($row, $this->name, $value);
483
            }
484
        }
485
486
        return $data;
487
    }
488
489
    /**
490
     * If current column is a defined column.
491
     *
492
     * @return bool
493
     */
494
    protected function isDefinedColumn()
495
    {
496
        return array_key_exists($this->name, static::$defined);
497
    }
498
499
    /**
500
     * Use a defined column.
501
     *
502
     * @throws \Exception
503
     */
504
    protected function useDefinedColumn()
505
    {
506
        // clear all display callbacks.
507
        $this->displayCallbacks = [];
508
509
        $class = static::$defined[$this->name];
510
511
        if ($class instanceof Closure) {
512
            $this->display($class);
513
514
            return;
515
        }
516
517
        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...
518
            throw new \Exception("Invalid column definition [$class]");
519
        }
520
521
        $grid = $this->grid;
522
        $column = $this;
523
524
        $this->display(function ($value) use ($grid, $column, $class) {
525
            /** @var AbstractDisplayer $definition */
526
            $definition = new $class($value, $grid, $column, $this);
527
528
            return $definition->display();
529
        });
530
    }
531
532
    /**
533
     * Convert characters to HTML entities recursively.
534
     *
535
     * @param array|string $item
536
     *
537
     * @return mixed
538
     */
539
    protected function htmlEntityEncode($item)
540
    {
541
        if (is_array($item)) {
542
            array_walk_recursive($item, function (&$value) {
543
                $value = htmlentities($value);
544
            });
545
        } else {
546
            $item = htmlentities($item);
547
        }
548
549
        return $item;
550
    }
551
552
    /**
553
     * Create the column sorter.
554
     *
555
     * @return string
556
     */
557
    public function sorter()
558
    {
559
        if (!$this->sortable) {
560
            return '';
561
        }
562
563
        $icon = 'fa-sort';
564
        $type = 'desc';
565
566
        if ($this->isSorted()) {
567
            $type = $this->sort['type'] == 'desc' ? 'asc' : 'desc';
568
            $icon .= "-amount-{$this->sort['type']}";
569
        }
570
571
        // set sort value
572
        $sort = ['column' => $this->name, 'type' => $type];
573
        if (isset($this->cast)) {
574
            $sort['cast'] = $this->cast;
575
        }
576
577
        $query = app('request')->all();
578
        $query = array_merge($query, [$this->grid->model()->getSortName() => $sort]);
579
580
        $url = url()->current().'?'.http_build_query($query);
581
582
        return "<a class=\"fa fa-fw $icon\" href=\"$url\"></a>";
583
    }
584
585
    /**
586
     * Determine if this column is currently sorted.
587
     *
588
     * @return bool
589
     */
590
    protected function isSorted()
591
    {
592
        $this->sort = app('request')->get($this->grid->model()->getSortName());
593
594
        if (empty($this->sort)) {
595
            return false;
596
        }
597
598
        return isset($this->sort['column']) && $this->sort['column'] == $this->name;
599
    }
600
601
    /**
602
     * Find a displayer to display column.
603
     *
604
     * @param string $abstract
605
     * @param array  $arguments
606
     *
607
     * @return Column
608
     */
609
    protected function resolveDisplayer($abstract, $arguments)
610
    {
611
        if (array_key_exists($abstract, static::$displayers)) {
612
            return $this->callBuiltinDisplayer(static::$displayers[$abstract], $arguments);
613
        }
614
615
        return $this->callSupportDisplayer($abstract, $arguments);
616
    }
617
618
    /**
619
     * Call Illuminate/Support displayer.
620
     *
621
     * @param string $abstract
622
     * @param array  $arguments
623
     *
624
     * @return Column
625
     */
626
    protected function callSupportDisplayer($abstract, $arguments)
627
    {
628
        return $this->display(function ($value) use ($abstract, $arguments) {
629
            if (is_array($value) || $value instanceof Arrayable) {
630
                return call_user_func_array([collect($value), $abstract], $arguments);
631
            }
632
633
            if (is_string($value)) {
634
                return call_user_func_array([Str::class, $abstract], array_merge([$value], $arguments));
635
            }
636
637
            return $value;
638
        });
639
    }
640
641
    /**
642
     * Call Builtin displayer.
643
     *
644
     * @param string $abstract
645
     * @param array  $arguments
646
     *
647
     * @return Column
648
     */
649
    protected function callBuiltinDisplayer($abstract, $arguments)
650
    {
651
        if ($abstract instanceof Closure) {
652
            return $this->display(function ($value) use ($abstract, $arguments) {
653
                return $abstract->call($this, ...array_merge([$value], $arguments));
654
            });
655
        }
656
657
        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...
658
            $grid = $this->grid;
659
            $column = $this;
660
661 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...
662
                /** @var AbstractDisplayer $displayer */
663
                $displayer = new $abstract($value, $grid, $column, $this);
664
665
                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...
666
            });
667
        }
668
669
        return $this;
670
    }
671
672
    /**
673
     * Passes through all unknown calls to builtin displayer or supported displayer.
674
     *
675
     * Allow fluent calls on the Column object.
676
     *
677
     * @param string $method
678
     * @param array  $arguments
679
     *
680
     * @return $this
681
     */
682
    public function __call($method, $arguments)
683
    {
684
        if ($this->isRelation() && !$this->relationColumn) {
685
            $this->name = "{$this->relation}.$method";
686
            $this->label = isset($arguments[0]) ? $arguments[0] : ucfirst($method);
687
688
            $this->relationColumn = $method;
689
690
            return $this;
691
        }
692
693
        return $this->resolveDisplayer($method, $arguments);
694
    }
695
}
696