Completed
Push — master ( 88ab05...dae06d )
by Song
02:49
created

Column::repeat()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 2
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin\Grid;
4
5
use Carbon\Carbon;
6
use Closure;
7
use Encore\Admin\Actions\RowAction;
8
use Encore\Admin\Grid;
9
use Encore\Admin\Grid\Displayers\AbstractDisplayer;
10
use Illuminate\Contracts\Support\Arrayable;
11
use Illuminate\Database\Eloquent\Model as BaseModel;
12
use Illuminate\Support\Arr;
13
use Illuminate\Support\Collection;
14
use Illuminate\Support\Str;
15
16
class Column
17
{
18
    use Column\HasHeader,
19
        Column\InlineEditing,
20
        Column\ExtendDisplay;
21
22
    const SELECT_COLUMN_NAME = '__row_selector__';
23
24
    const ACTION_COLUMN_NAME = '__actions__';
25
26
    /**
27
     * @var Grid
28
     */
29
    protected $grid;
30
31
    /**
32
     * Name of column.
33
     *
34
     * @var string
35
     */
36
    protected $name;
37
38
    /**
39
     * Label of column.
40
     *
41
     * @var string
42
     */
43
    protected $label;
44
45
    /**
46
     * Original value of column.
47
     *
48
     * @var mixed
49
     */
50
    protected $original;
51
52
    /**
53
     * Attributes of column.
54
     *
55
     * @var array
56
     */
57
    protected $attributes = [];
58
59
    /**
60
     * Relation name.
61
     *
62
     * @var bool
63
     */
64
    protected $relation = false;
65
66
    /**
67
     * Relation column.
68
     *
69
     * @var string
70
     */
71
    protected $relationColumn;
72
73
    /**
74
     * Original grid data.
75
     *
76
     * @var Collection
77
     */
78
    protected static $originalGridModels;
79
80
    /**
81
     * @var []Closure
82
     */
83
    protected $displayCallbacks = [];
84
85
    /**
86
     * Defined columns.
87
     *
88
     * @var array
89
     */
90
    public static $defined = [];
91
92
    /**
93
     * @var array
94
     */
95
    protected static $htmlAttributes = [];
96
97
    /**
98
     * @var array
99
     */
100
    protected static $rowAttributes = [];
101
102
    /**
103
     * @var Model
104
     */
105
    protected static $model;
106
107
    /**
108
     * @param string $name
109
     * @param string $label
110
     */
111
    public function __construct($name, $label)
112
    {
113
        $this->name = $name;
114
115
        $this->label = $this->formatLabel($label);
116
117
        $this->initAttributes();
118
    }
119
120
    /**
121
     * Initialize column attributes.
122
     */
123
    protected function initAttributes()
124
    {
125
        $name = str_replace('.', '-', $this->name);
126
127
        $this->setAttributes(['class' => "column-{$name}"]);
128
    }
129
130
    /**
131
     * Define a column globally.
132
     *
133
     * @param string $name
134
     * @param mixed  $definition
135
     */
136
    public static function define($name, $definition)
137
    {
138
        static::$defined[$name] = $definition;
139
    }
140
141
    /**
142
     * Set grid instance for column.
143
     *
144
     * @param Grid $grid
145
     */
146
    public function setGrid(Grid $grid)
147
    {
148
        $this->grid = $grid;
149
150
        $this->setModel($grid->model()->eloquent());
0 ignored issues
show
Bug introduced by
The method eloquent does only exist in Encore\Admin\Grid\Model, but not in Illuminate\Database\Eloquent\Builder.

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...
151
    }
152
153
    /**
154
     * Set model for column.
155
     *
156
     * @param $model
157
     */
158
    public function setModel($model)
159
    {
160
        if (is_null(static::$model) && ($model instanceof BaseModel)) {
161
            static::$model = $model->newInstance();
162
        }
163
    }
164
165
    /**
166
     * Set original data for column.
167
     *
168
     * @param Collection $collection
169
     */
170
    public static function setOriginalGridModels(Collection $collection)
171
    {
172
        static::$originalGridModels = $collection;
173
    }
174
175
    /**
176
     * Set column attributes.
177
     *
178
     * @param array $attributes
179
     *
180
     * @return $this
181
     */
182
    public function setAttributes($attributes = [], $key = null)
183
    {
184
        if ($key) {
185
            static::$rowAttributes[$this->name][$key] = array_merge(
186
                Arr::get(static::$rowAttributes, "{$this->name}.{$key}", []),
187
                $attributes
188
            );
189
190
            return $this;
191
        }
192
193
        static::$htmlAttributes[$this->name] = array_merge(
194
            Arr::get(static::$htmlAttributes, $this->name, []),
195
            $attributes
196
        );
197
198
        return $this;
199
    }
200
201
    /**
202
     * Get column attributes.
203
     *
204
     * @param string $name
205
     *
206
     * @return mixed
207
     */
208
    public static function getAttributes($name, $key = null)
209
    {
210
        $rowAttributes = [];
211
212
        if ($key && Arr::has(static::$rowAttributes, "{$name}.{$key}")) {
213
            $rowAttributes = Arr::get(static::$rowAttributes, "{$name}.{$key}", []);
214
        }
215
216
        $columnAttributes = Arr::get(static::$htmlAttributes, $name, []);
217
218
        return array_merge($rowAttributes, $columnAttributes);
219
    }
220
221
    /**
222
     * Format attributes to html.
223
     *
224
     * @return string
225
     */
226
    public function formatHtmlAttributes()
227
    {
228
        $attrArr = [];
229
        foreach (static::getAttributes($this->name) as $name => $val) {
230
            $attrArr[] = "$name=\"$val\"";
231
        }
232
233
        return implode(' ', $attrArr);
234
    }
235
236
    /**
237
     * Set style of this column.
238
     *
239
     * @param string $style
240
     *
241
     * @return $this
242
     */
243
    public function style($style)
244
    {
245
        return $this->setAttributes(compact('style'));
246
    }
247
248
    /**
249
     * Set the width of column.
250
     *
251
     * @param int $width
252
     *
253
     * @return $this
254
     */
255
    public function width(int $width)
256
    {
257
        return $this->style("width: {$width}px;max-width: {$width}px;word-wrap: break-word;word-break: normal;");
258
    }
259
260
    /**
261
     * Set the color of column.
262
     *
263
     * @param string $color
264
     *
265
     * @return $this
266
     */
267
    public function color($color)
268
    {
269
        return $this->style("color:$color;");
270
    }
271
272
    /**
273
     * Get original column value.
274
     *
275
     * @return mixed
276
     */
277
    public function getOriginal()
278
    {
279
        return $this->original;
280
    }
281
282
    /**
283
     * Get name of this column.
284
     *
285
     * @return mixed
286
     */
287
    public function getName()
288
    {
289
        return $this->name;
290
    }
291
292
    /**
293
     * @return string
294
     */
295
    public function getClassName()
296
    {
297
        $name = str_replace('.', '-', $this->getName());
298
299
        return "column-{$name}";
300
    }
301
302
    /**
303
     * Format label.
304
     *
305
     * @param $label
306
     *
307
     * @return mixed
308
     */
309
    protected function formatLabel($label)
310
    {
311
        if ($label) {
312
            return $label;
313
        }
314
315
        $label = ucfirst($this->name);
316
317
        return __(str_replace(['.', '_'], ' ', $label));
318
    }
319
320
    /**
321
     * Get label of the column.
322
     *
323
     * @return mixed
324
     */
325
    public function getLabel()
326
    {
327
        return $this->label;
328
    }
329
330
    /**
331
     * Set relation.
332
     *
333
     * @param string $relation
334
     * @param string $relationColumn
335
     *
336
     * @return $this
337
     */
338
    public function setRelation($relation, $relationColumn = null)
339
    {
340
        $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...
341
        $this->relationColumn = $relationColumn;
342
343
        return $this;
344
    }
345
346
    /**
347
     * If this column is relation column.
348
     *
349
     * @return bool
350
     */
351
    protected function isRelation()
352
    {
353
        return (bool) $this->relation;
354
    }
355
356
    /**
357
     * Mark this column as sortable.
358
     *
359
     * @param null|string $cast
360
     *
361
     * @return Column|string
362
     */
363
    public function sortable($cast = null)
364
    {
365
        return $this->addSorter($cast);
366
    }
367
368
    /**
369
     * Set cast name for sortable.
370
     *
371
     * @return $this
372
     *
373
     * @deprecated Use `$column->sortable($cast)` instead.
374
     */
375
    public function cast($cast)
376
    {
377
        $this->cast = $cast;
0 ignored issues
show
Bug introduced by
The property cast does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
378
379
        return $this;
380
    }
381
382
    /**
383
     * Set help message for column.
384
     *
385
     * @param string $help
386
     *
387
     * @return $this|string
388
     */
389
    public function help($help = '')
390
    {
391
        return $this->addHelp($help);
392
    }
393
394
    /**
395
     * Set column filter.
396
     *
397
     * @param mixed|null $builder
398
     *
399
     * @return $this
400
     */
401
    public function filter($builder = null)
0 ignored issues
show
Unused Code introduced by
The parameter $builder is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
402
    {
403
        return $this->addFilter(...func_get_args());
404
    }
405
406
    /**
407
     * Add a display callback.
408
     *
409
     * @param Closure $callback
410
     *
411
     * @return $this
412
     */
413
    public function display(Closure $callback)
414
    {
415
        $this->displayCallbacks[] = $callback;
416
417
        return $this;
418
    }
419
420
    /**
421
     * Display using display abstract.
422
     *
423
     * @param string $abstract
424
     * @param array  $arguments
425
     *
426
     * @return $this
427
     */
428
    public function displayUsing($abstract, $arguments = [])
429
    {
430
        $grid = $this->grid;
431
432
        $column = $this;
433
434 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...
435
            /** @var AbstractDisplayer $displayer */
436
            $displayer = new $abstract($value, $grid, $column, $this);
437
438
            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...
439
        });
440
    }
441
442
    /**
443
     * Hide this column by default.
444
     *
445
     * @return $this
446
     */
447
    public function hide()
448
    {
449
        $this->grid->hideColumns($this->getName());
450
451
        return $this;
452
    }
453
454
    /**
455
     * Add column to total-row.
456
     *
457
     * @param null $display
458
     *
459
     * @return $this
460
     */
461
    public function totalRow($display = null)
462
    {
463
        $this->grid->addTotalRow($this->name, $display);
464
465
        return $this;
466
    }
467
468
    /**
469
     * Display column using a grid row action.
470
     *
471
     * @param string $action
472
     *
473
     * @return $this
474
     */
475
    public function action($action)
476
    {
477
        if (!is_subclass_of($action, RowAction::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\Actions\RowAction::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
478
            throw new \InvalidArgumentException("Action class [$action] must be sub-class of [Encore\Admin\Actions\GridAction]");
479
        }
480
481
        $grid = $this->grid;
482
483
        return $this->display(function ($_, $column) use ($action, $grid) {
484
            /** @var RowAction $action */
485
            $action = new $action();
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $action, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
486
487
            return $action
488
                ->asColumn()
489
                ->setGrid($grid)
490
                ->setColumn($column)
491
                ->setRow($this);
0 ignored issues
show
Compatibility introduced by
$this of type object<Encore\Admin\Grid\Column> is not a sub-type of object<Illuminate\Database\Eloquent\Model>. It seems like you assume a child class of the class Encore\Admin\Grid\Column to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
492
        });
493
    }
494
495
    /**
496
     * If has display callbacks.
497
     *
498
     * @return bool
499
     */
500
    protected function hasDisplayCallbacks()
501
    {
502
        return !empty($this->displayCallbacks);
503
    }
504
505
    /**
506
     * Call all of the "display" callbacks column.
507
     *
508
     * @param mixed $value
509
     * @param int   $key
510
     *
511
     * @return mixed
512
     */
513
    protected function callDisplayCallbacks($value, $key)
514
    {
515
        foreach ($this->displayCallbacks as $callback) {
516
            $previous = $value;
517
518
            $callback = $this->bindOriginalRowModel($callback, $key);
519
            $value = call_user_func_array($callback, [$value, $this]);
520
521
            if (($value instanceof static) &&
522
                ($last = array_pop($this->displayCallbacks))
523
            ) {
524
                $last = $this->bindOriginalRowModel($last, $key);
525
                $value = call_user_func($last, $previous);
526
            }
527
        }
528
529
        return $value;
530
    }
531
532
    /**
533
     * Set original grid data to column.
534
     *
535
     * @param Closure $callback
536
     * @param int     $key
537
     *
538
     * @return Closure
539
     */
540
    protected function bindOriginalRowModel(Closure $callback, $key)
541
    {
542
        $rowModel = static::$originalGridModels[$key];
543
544
        return $callback->bindTo($rowModel);
545
    }
546
547
    /**
548
     * Fill all data to every column.
549
     *
550
     * @param array $data
551
     *
552
     * @return mixed
553
     */
554
    public function fill(array $data)
555
    {
556
        foreach ($data as $key => &$row) {
557
            $this->original = $value = Arr::get($row, $this->name);
558
559
            $value = $this->htmlEntityEncode($value);
560
561
            Arr::set($row, $this->name, $value);
562
563
            if ($this->isDefinedColumn()) {
564
                $this->useDefinedColumn();
565
            }
566
567
            if ($this->hasDisplayCallbacks()) {
568
                $value = $this->callDisplayCallbacks($this->original, $key);
569
                Arr::set($row, $this->name, $value);
570
            }
571
        }
572
573
        return $data;
574
    }
575
576
    /**
577
     * If current column is a defined column.
578
     *
579
     * @return bool
580
     */
581
    protected function isDefinedColumn()
582
    {
583
        return array_key_exists($this->name, static::$defined);
584
    }
585
586
    /**
587
     * Use a defined column.
588
     *
589
     * @throws \Exception
590
     */
591
    protected function useDefinedColumn()
592
    {
593
        // clear all display callbacks.
594
        $this->displayCallbacks = [];
595
596
        $class = static::$defined[$this->name];
597
598
        if ($class instanceof Closure) {
599
            $this->display($class);
600
601
            return;
602
        }
603
604
        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...
605
            throw new \Exception("Invalid column definition [$class]");
606
        }
607
608
        $grid = $this->grid;
609
        $column = $this;
610
611
        $this->display(function ($value) use ($grid, $column, $class) {
612
            /** @var AbstractDisplayer $definition */
613
            $definition = new $class($value, $grid, $column, $this);
614
615
            return $definition->display();
616
        });
617
    }
618
619
    /**
620
     * Convert characters to HTML entities recursively.
621
     *
622
     * @param array|string $item
623
     *
624
     * @return mixed
625
     */
626
    protected function htmlEntityEncode($item)
627
    {
628
        if (is_array($item)) {
629
            array_walk_recursive($item, function (&$value) {
630
                $value = htmlentities($value);
631
            });
632
        } else {
633
            $item = htmlentities($item);
634
        }
635
636
        return $item;
637
    }
638
639
    /**
640
     * Find a displayer to display column.
641
     *
642
     * @param string $abstract
643
     * @param array  $arguments
644
     *
645
     * @return $this
646
     */
647
    protected function resolveDisplayer($abstract, $arguments)
648
    {
649
        if (array_key_exists($abstract, static::$displayers)) {
650
            return $this->callBuiltinDisplayer(static::$displayers[$abstract], $arguments);
651
        }
652
653
        return $this->callSupportDisplayer($abstract, $arguments);
654
    }
655
656
    /**
657
     * Call Illuminate/Support displayer.
658
     *
659
     * @param string $abstract
660
     * @param array  $arguments
661
     *
662
     * @return $this
663
     */
664
    protected function callSupportDisplayer($abstract, $arguments)
665
    {
666
        return $this->display(function ($value) use ($abstract, $arguments) {
667
            if (is_array($value) || $value instanceof Arrayable) {
668
                return call_user_func_array([collect($value), $abstract], $arguments);
669
            }
670
671
            if (is_string($value)) {
672
                return call_user_func_array([Str::class, $abstract], array_merge([$value], $arguments));
673
            }
674
675
            return $value;
676
        });
677
    }
678
679
    /**
680
     * Call Builtin displayer.
681
     *
682
     * @param string $abstract
683
     * @param array  $arguments
684
     *
685
     * @return $this
686
     */
687
    protected function callBuiltinDisplayer($abstract, $arguments)
688
    {
689
        if ($abstract instanceof Closure) {
690
            return $this->display(function ($value) use ($abstract, $arguments) {
691
                return $abstract->call($this, ...array_merge([$value], $arguments));
692
            });
693
        }
694
695
        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...
696
            $grid = $this->grid;
697
            $column = $this;
698
699 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...
700
                /** @var AbstractDisplayer $displayer */
701
                $displayer = new $abstract($value, $grid, $column, $this);
702
703
                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...
704
            });
705
        }
706
707
        return $this;
708
    }
709
710
    /**
711
     * Passes through all unknown calls to builtin displayer or supported displayer.
712
     *
713
     * Allow fluent calls on the Column object.
714
     *
715
     * @param string $method
716
     * @param array  $arguments
717
     *
718
     * @return $this
719
     */
720
    public function __call($method, $arguments)
721
    {
722
        if ($this->isRelation() && !$this->relationColumn) {
723
            $this->name = "{$this->relation}.$method";
724
            $this->label = $this->formatLabel($arguments[0] ?? null);
725
726
            $this->relationColumn = $method;
727
728
            return $this;
729
        }
730
731
        return $this->resolveDisplayer($method, $arguments);
732
    }
733
}
734