Completed
Push — master ( 4aab6d...ad5c9d )
by Song
03:35
created

Column::callSupportDisplayer()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 1
nop 2
dl 0
loc 14
rs 9.7998
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\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 Model
109
     */
110
    protected static $model;
111
112
    const SELECT_COLUMN_NAME = '__row_selector__';
113
114
    /**
115
     * @param string $name
116
     * @param string $label
117
     */
118
    public function __construct($name, $label)
119
    {
120
        $this->name = $name;
121
122
        $this->label = $this->formatLabel($label);
123
    }
124
125
    /**
126
     * Extend column displayer.
127
     *
128
     * @param $name
129
     * @param $displayer
130
     */
131
    public static function extend($name, $displayer)
132
    {
133
        static::$displayers[$name] = $displayer;
134
    }
135
136
    /**
137
     * Define a column globally.
138
     *
139
     * @param string $name
140
     * @param mixed  $definition
141
     */
142
    public static function define($name, $definition)
143
    {
144
        static::$defined[$name] = $definition;
145
    }
146
147
    /**
148
     * Set grid instance for column.
149
     *
150
     * @param Grid $grid
151
     */
152
    public function setGrid(Grid $grid)
153
    {
154
        $this->grid = $grid;
155
156
        $this->setModel($grid->model()->eloquent());
157
    }
158
159
    /**
160
     * Set model for column.
161
     *
162
     * @param $model
163
     */
164
    public function setModel($model)
165
    {
166
        if (is_null(static::$model) && ($model instanceof Model)) {
167
            static::$model = $model->newInstance();
168
        }
169
    }
170
171
    /**
172
     * Set original data for column.
173
     *
174
     * @param 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
     * Display column using array value map.
307
     *
308
     * @param array $values
309
     * @param null  $default
310
     *
311
     * @return $this
312
     */
313 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...
314
    {
315
        return $this->display(function ($value) use ($values, $default) {
316
            if (is_null($value)) {
317
                return $default;
318
            }
319
320
            return array_get($values, $value, $default);
321
        });
322
    }
323
324
    /**
325
     * Render this column with the given view
326
     *
327
     * @param string $view
328
     * @return $this
329
     */
330
    public function view($view)
331
    {
332
        return $this->display(function ($value) use ($view) {
333
            $model = $this;
334
            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...
335
        });
336
    }
337
338
    /**
339
     * If has display callbacks.
340
     *
341
     * @return bool
342
     */
343
    protected function hasDisplayCallbacks()
344
    {
345
        return !empty($this->displayCallbacks);
346
    }
347
348
    /**
349
     * Call all of the "display" callbacks column.
350
     *
351
     * @param mixed $value
352
     * @param int   $key
353
     *
354
     * @return mixed
355
     */
356
    protected function callDisplayCallbacks($value, $key)
357
    {
358
        foreach ($this->displayCallbacks as $callback) {
359
360
            $previous = $value;
361
362
            $callback = $this->bindOriginalRow($callback, $key);
363
            $value = call_user_func_array($callback, [$value, $this]);
364
365
            if (($value instanceof static) &&
366
                ($last = array_pop($this->displayCallbacks))
367
            ) {
368
                $value = call_user_func($last, $previous);
369
            }
370
        }
371
372
        return $value;
373
    }
374
375
    /**
376
     * Set original grid data to column.
377
     *
378
     * @param Closure $callback
379
     * @param int     $key
380
     *
381
     * @return Closure
382
     */
383
    protected function bindOriginalRow(Closure $callback, $key)
384
    {
385
        $originalRow = static::$originalGridData[$key];
386
387
        return $callback->bindTo(static::$model->newFromBuilder($originalRow));
388
    }
389
390
    /**
391
     * Fill all data to every column.
392
     *
393
     * @param array $data
394
     *
395
     * @return mixed
396
     */
397
    public function fill(array $data)
398
    {
399
        foreach ($data as $key => &$row) {
400
            $this->original = $value = array_get($row, $this->name);
401
402
            $value = $this->htmlEntityEncode($value);
403
404
            array_set($row, $this->name, $value);
405
406
            if ($this->isDefinedColumn()) {
407
                $this->useDefinedColumn();
408
            }
409
410
            if ($this->hasDisplayCallbacks()) {
411
                $value = $this->callDisplayCallbacks($this->original, $key);
412
                array_set($row, $this->name, $value);
413
            }
414
        }
415
416
        return $data;
417
    }
418
419
    /**
420
     * If current column is a defined column.
421
     *
422
     * @return bool
423
     */
424
    protected function isDefinedColumn()
425
    {
426
        return array_key_exists($this->name, static::$defined);
427
    }
428
429
    /**
430
     * Use a defined column.
431
     *
432
     * @throws \Exception
433
     */
434
    protected function useDefinedColumn()
435
    {
436
        // clear all display callbacks.
437
        $this->displayCallbacks = [];
438
439
        $class = static::$defined[$this->name];
440
441
        if ($class instanceof Closure) {
442
            $this->display($class);
443
444
            return;
445
        }
446
447
        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...
448
            throw new \Exception("Invalid column definition [$class]");
449
        }
450
451
        $grid = $this->grid;
452
        $column = $this;
453
454
        $this->display(function ($value) use ($grid, $column, $class) {
455
            /** @var AbstractDisplayer $definition */
456
            $definition = new $class($value, $grid, $column, $this);
457
458
            return $definition->display();
459
        });
460
    }
461
462
    /**
463
     * Convert characters to HTML entities recursively.
464
     *
465
     * @param array|string $item
466
     *
467
     * @return mixed
468
     */
469
    protected function htmlEntityEncode($item)
470
    {
471
        if (is_array($item)) {
472
            array_walk_recursive($item, function (&$value) {
473
                $value = htmlentities($value);
474
            });
475
        } else {
476
            $item = htmlentities($item);
477
        }
478
479
        return $item;
480
    }
481
482
    /**
483
     * Create the column sorter.
484
     *
485
     * @return string|void
486
     */
487
    public function sorter()
488
    {
489
        if (!$this->sortable) {
490
            return;
491
        }
492
493
        $icon = 'fa-sort';
494
        $type = 'desc';
495
496
        if ($this->isSorted()) {
497
            $type = $this->sort['type'] == 'desc' ? 'asc' : 'desc';
498
            $icon .= "-amount-{$this->sort['type']}";
499
        }
500
501
        $query = app('request')->all();
502
        $query = array_merge($query, [$this->grid->model()->getSortName() => ['column' => $this->name, 'type' => $type]]);
503
504
        $url = URL::current().'?'.http_build_query($query);
505
506
        return "<a class=\"fa fa-fw $icon\" href=\"$url\"></a>";
507
    }
508
509
    /**
510
     * Determine if this column is currently sorted.
511
     *
512
     * @return bool
513
     */
514
    protected function isSorted()
515
    {
516
        $this->sort = app('request')->get($this->grid->model()->getSortName());
517
518
        if (empty($this->sort)) {
519
            return false;
520
        }
521
522
        return isset($this->sort['column']) && $this->sort['column'] == $this->name;
523
    }
524
525
    /**
526
     * Find a displayer to display column.
527
     *
528
     * @param string $abstract
529
     * @param array  $arguments
530
     *
531
     * @return Column
532
     */
533
    protected function resolveDisplayer($abstract, $arguments)
534
    {
535
        if (array_key_exists($abstract, static::$displayers)) {
536
            return $this->callBuiltinDisplayer(static::$displayers[$abstract], $arguments);
537
        }
538
539
        return $this->callSupportDisplayer($abstract, $arguments);
540
    }
541
542
    /**
543
     * Call Illuminate/Support displayer.
544
     *
545
     * @param string $abstract
546
     * @param array  $arguments
547
     *
548
     * @return Column
549
     */
550
    protected function callSupportDisplayer($abstract, $arguments)
551
    {
552
        return $this->display(function ($value) use ($abstract, $arguments) {
553
            if (is_array($value) || $value instanceof Arrayable) {
554
                return call_user_func_array([collect($value), $abstract], $arguments);
555
            }
556
557
            if (is_string($value)) {
558
                return call_user_func_array([Str::class, $abstract], array_merge([$value], $arguments));
559
            }
560
561
            return $value;
562
        });
563
    }
564
565
    /**
566
     * Call Builtin displayer.
567
     *
568
     * @param string $abstract
569
     * @param array  $arguments
570
     *
571
     * @return Column
572
     */
573
    protected function callBuiltinDisplayer($abstract, $arguments)
574
    {
575
        if ($abstract instanceof Closure) {
576
            return $this->display(function ($value) use ($abstract, $arguments) {
577
                return $abstract->call($this, ...array_merge([$value], $arguments));
578
            });
579
        }
580
581
        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...
582
            $grid = $this->grid;
583
            $column = $this;
584
585
            return $this->display(function ($value) use ($abstract, $grid, $column, $arguments) {
586
                $displayer = new $abstract($value, $grid, $column, $this);
587
588
                return $displayer->display(...$arguments);
589
            });
590
        }
591
592
        return $this;
593
    }
594
595
    /**
596
     * Passes through all unknown calls to builtin displayer or supported displayer.
597
     *
598
     * Allow fluent calls on the Column object.
599
     *
600
     * @param string $method
601
     * @param array  $arguments
602
     *
603
     * @return $this
604
     */
605
    public function __call($method, $arguments)
606
    {
607
        if ($this->isRelation() && !$this->relationColumn) {
608
            $this->name = "{$this->relation}.$method";
609
            $this->label = isset($arguments[0]) ? $arguments[0] : ucfirst($method);
610
611
            $this->relationColumn = $method;
612
613
            return $this;
614
        }
615
616
        return $this->resolveDisplayer($method, $arguments);
617
    }
618
}
619