Completed
Pull Request — master (#2254)
by
unknown
11:30
created

Column   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 563
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
dl 0
loc 563
rs 6
c 0
b 0
f 0
wmc 55
lcom 2
cbo 4

29 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A extend() 0 4 1
A define() 0 4 1
A setGrid() 0 6 1
A setModel() 0 6 3
A setOriginalGridData() 0 4 1
A setAttributes() 0 6 1
A getAttributes() 0 4 1
A style() 0 4 1
A getName() 0 4 1
A formatLabel() 0 6 2
A getLabel() 0 4 1
A setRelation() 0 7 1
A isRelation() 0 4 1
A sortable() 0 6 1
A display() 0 6 1
A hasDisplayCallbacks() 0 4 1
A callDisplayCallbacks() 0 9 2
A bindOriginalRow() 0 6 1
A fill() 0 21 4
A isDefinedColumn() 0 4 1
A useDefinedColumn() 0 26 4
A htmlEntityEncode() 0 12 2
A sorter() 0 21 4
A isSorted() 0 10 3
A resolveDisplayer() 0 8 2
A callSupportDisplayer() 0 14 4
A callBuiltinDisplayer() 0 21 4
A __call() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like Column often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Column, and based on these observations, apply Extract Interface, too.

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\Facades\URL;
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 array
80
     */
81
    protected static $originalGridData = [];
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
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 array $input
175
     */
176
    public static function setOriginalGridData(array $input)
177
    {
178
        static::$originalGridData = $input;
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
     * Mark this column as sortable.
281
     *
282
     * @return Column
283
     */
284
    public function sortable()
285
    {
286
        $this->sortable = true;
287
288
        return $this;
289
    }
290
291
    /**
292
     * Add a display callback.
293
     *
294
     * @param Closure $callback
295
     *
296
     * @return $this
297
     */
298
    public function display(Closure $callback)
299
    {
300
        $this->displayCallbacks[] = $callback;
301
302
        return $this;
303
    }
304
305
    /**
306
     * If has display callbacks.
307
     *
308
     * @return bool
309
     */
310
    protected function hasDisplayCallbacks()
311
    {
312
        return !empty($this->displayCallbacks);
313
    }
314
315
    /**
316
     * Call all of the "display" callbacks column.
317
     *
318
     * @param mixed $value
319
     * @param int   $key
320
     *
321
     * @return mixed
322
     */
323
    protected function callDisplayCallbacks($value, $key)
324
    {
325
        foreach ($this->displayCallbacks as $callback) {
326
            $callback = $this->bindOriginalRow($callback, $key);
327
            $value = call_user_func($callback, $value);
328
        }
329
330
        return $value;
331
    }
332
333
    /**
334
     * Set original grid data to column.
335
     *
336
     * @param Closure $callback
337
     * @param int     $key
338
     *
339
     * @return Closure
340
     */
341
    protected function bindOriginalRow(Closure $callback, $key)
342
    {
343
        $originalRow = static::$originalGridData[$key];
344
345
        return $callback->bindTo(static::$model->newFromBuilder($originalRow));
346
    }
347
348
    /**
349
     * Fill all data to every column.
350
     *
351
     * @param array $data
352
     *
353
     * @return mixed
354
     */
355
    public function fill(array $data)
356
    {
357
        foreach ($data as $key => &$row) {
358
            $this->original = $value = array_get($row, $this->name);
359
360
            $value = $this->htmlEntityEncode($value);
361
362
            array_set($row, $this->name, $value);
363
364
            if ($this->isDefinedColumn()) {
365
                $this->useDefinedColumn();
366
            }
367
368
            if ($this->hasDisplayCallbacks()) {
369
                $value = $this->callDisplayCallbacks($this->original, $key);
370
                array_set($row, $this->name, $value);
371
            }
372
        }
373
374
        return $data;
375
    }
376
377
    /**
378
     * If current column is a defined column.
379
     *
380
     * @return bool
381
     */
382
    protected function isDefinedColumn()
383
    {
384
        return array_key_exists($this->name, static::$defined);
385
    }
386
387
    /**
388
     * Use a defined column.
389
     *
390
     * @throws \Exception
391
     */
392
    protected function useDefinedColumn()
393
    {
394
        // clear all display callbacks.
395
        $this->displayCallbacks = [];
396
397
        $class = static::$defined[$this->name];
398
399
        if ($class instanceof Closure) {
400
            $this->display($class);
401
402
            return;
403
        }
404
405
        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...
406
            throw new \Exception("Invalid column definition [$class]");
407
        }
408
409
        $grid = $this->grid;
410
        $column = $this;
411
412
        $this->display(function ($value) use ($grid, $column, $class) {
413
            $definition = new $class($value, $grid, $column, $this);
414
415
            return $definition->display();
416
        });
417
    }
418
419
    /**
420
     * Convert characters to HTML entities recursively.
421
     *
422
     * @param array|string $item
423
     *
424
     * @return mixed
425
     */
426
    protected function htmlEntityEncode($item)
427
    {
428
        if (is_array($item)) {
429
            array_walk_recursive($item, function (&$value) {
430
                $value = htmlentities($value);
431
            });
432
        } else {
433
            $item = htmlentities($item);
434
        }
435
436
        return $item;
437
    }
438
439
    /**
440
     * Create the column sorter.
441
     *
442
     * @return string|void
443
     */
444
    public function sorter()
445
    {
446
        if (!$this->sortable) {
447
            return;
448
        }
449
450
        $icon = 'fa-sort';
451
        $type = 'desc';
452
453
        if ($this->isSorted()) {
454
            $type = $this->sort['type'] == 'desc' ? 'asc' : 'desc';
455
            $icon .= "-amount-{$this->sort['type']}";
456
        }
457
458
        $query = app('request')->all();
459
        $query = array_merge($query, [$this->grid->model()->getSortName() => ['column' => $this->name, 'type' => $type]]);
460
461
        $url = URL::current().'?'.http_build_query($query);
462
463
        return "<a class=\"fa fa-fw $icon\" href=\"$url\"></a>";
464
    }
465
466
    /**
467
     * Determine if this column is currently sorted.
468
     *
469
     * @return bool
470
     */
471
    protected function isSorted()
472
    {
473
        $this->sort = app('request')->get($this->grid->model()->getSortName());
474
475
        if (empty($this->sort)) {
476
            return false;
477
        }
478
479
        return isset($this->sort['column']) && $this->sort['column'] == $this->name;
480
    }
481
482
    /**
483
     * Find a displayer to display column.
484
     *
485
     * @param string $abstract
486
     * @param array  $arguments
487
     *
488
     * @return Column
489
     */
490
    protected function resolveDisplayer($abstract, $arguments)
491
    {
492
        if (array_key_exists($abstract, static::$displayers)) {
493
            return $this->callBuiltinDisplayer(static::$displayers[$abstract], $arguments);
494
        }
495
496
        return $this->callSupportDisplayer($abstract, $arguments);
497
    }
498
499
    /**
500
     * Call Illuminate/Support displayer.
501
     *
502
     * @param string $abstract
503
     * @param array  $arguments
504
     *
505
     * @return Column
506
     */
507
    protected function callSupportDisplayer($abstract, $arguments)
508
    {
509
        return $this->display(function ($value) use ($abstract, $arguments) {
510
            if (is_array($value) || $value instanceof Arrayable) {
511
                return call_user_func_array([collect($value), $abstract], $arguments);
512
            }
513
514
            if (is_string($value)) {
515
                return call_user_func_array([Str::class, $abstract], array_merge([$value], $arguments));
516
            }
517
518
            return $value;
519
        });
520
    }
521
522
    /**
523
     * Call Builtin displayer.
524
     *
525
     * @param string $abstract
526
     * @param array  $arguments
527
     *
528
     * @return Column
529
     */
530
    protected function callBuiltinDisplayer($abstract, $arguments)
531
    {
532
        if ($abstract instanceof Closure) {
533
            return $this->display(function ($value) use ($abstract, $arguments) {
534
                return $abstract->call($this, ...array_merge([$value], $arguments));
535
            });
536
        }
537
538
        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...
539
            $grid = $this->grid;
540
            $column = $this;
541
542
            return $this->display(function ($value) use ($abstract, $grid, $column, $arguments) {
543
                $displayer = new $abstract($value, $grid, $column, $this);
544
545
                return call_user_func_array([$displayer, 'display'], $arguments);
546
            });
547
        }
548
549
        return $this;
550
    }
551
552
    /**
553
     * Passes through all unknown calls to builtin displayer or supported displayer.
554
     *
555
     * Allow fluent calls on the Column object.
556
     *
557
     * @param string $method
558
     * @param array  $arguments
559
     *
560
     * @return $this
561
     */
562
    public function __call($method, $arguments)
563
    {
564
        if ($this->isRelation() && !$this->relationColumn) {
565
            $this->name = "{$this->relation}.$method";
566
            $this->label = isset($arguments[0]) ? $arguments[0] : ucfirst($method);
567
568
            $this->relationColumn = $method;
569
570
            return $this;
571
        }
572
573
        return $this->resolveDisplayer($method, $arguments);
574
    }
575
}
576