Completed
Pull Request — master (#4171)
by Muhlis
09:37
created

Show::setWidth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin;
4
5
use Encore\Admin\Show\Divider;
6
use Encore\Admin\Show\Field;
7
use Encore\Admin\Show\Panel;
8
use Encore\Admin\Show\Relation;
9
use Illuminate\Contracts\Support\Renderable;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
13
use Illuminate\Database\Eloquent\Relations\HasMany;
14
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
15
use Illuminate\Database\Eloquent\Relations\HasOne;
16
use Illuminate\Database\Eloquent\Relations\MorphMany;
17
use Illuminate\Database\Eloquent\Relations\MorphOne;
18
use Illuminate\Database\Eloquent\Relations\Relation as EloquentRelation;
19
use Illuminate\Support\Arr;
20
use Illuminate\Support\Collection;
21
use Illuminate\Support\Str;
22
23
class Show implements Renderable
24
{
25
    /**
26
     * The Eloquent model to show.
27
     *
28
     * @var Model
29
     */
30
    protected $model;
31
32
    /**
33
     * Show panel builder.
34
     *
35
     * @var callable
36
     */
37
    protected $builder;
38
39
    /**
40
     * Resource path for this show page.
41
     *
42
     * @var string
43
     */
44
    protected $resource;
45
46
    /**
47
     * Fields to be show.
48
     *
49
     * @var Collection
50
     */
51
    protected $fields;
52
53
    /**
54
     * Relations to be show.
55
     *
56
     * @var Collection
57
     */
58
    protected $relations;
59
60
    /**
61
     * Enable overwrite field and relation.
62
     *
63
     * @var bool
64
     */
65
    protected $overWrite = false;
66
67
    /**
68
     * @var Panel
69
     */
70
    protected $panel;
71
72
    /**
73
     * Extended fields.
74
     *
75
     * @var array
76
     */
77
    public static $extendedFields = [];
78
79
    /**
80
     * @var \Closure
81
     */
82
    protected static $initCallback;
83
84
    /**
85
     * Show constructor.
86
     *
87
     * @param Model $model
88
     * @param mixed $builder
89
     */
90
    public function __construct($model, $builder = null)
91
    {
92
        $this->model = $model;
93
        $this->builder = $builder;
94
95
        $this->initPanel();
96
        $this->initContents();
97
98
        if (static::$initCallback instanceof \Closure) {
99
            call_user_func(static::$initCallback, $this);
100
        }
101
    }
102
103
    /**
104
     * Initialize with user pre-defined default disables, etc.
105
     *
106
     * @param \Closure $callback
107
     */
108
    public static function init(\Closure $callback = null)
109
    {
110
        static::$initCallback = $callback;
111
    }
112
113
    /**
114
     * Register custom field.
115
     *
116
     * @param string $abstract
117
     * @param string $class
118
     *
119
     * @return void
120
     */
121
    public static function extend($abstract, $class)
122
    {
123
        static::$extendedFields[$abstract] = $class;
124
    }
125
126
    /**
127
     * Initialize the contents to show.
128
     */
129
    protected function initContents()
130
    {
131
        $this->fields = new Collection();
132
        $this->relations = new Collection();
133
    }
134
135
    /**
136
     * Initialize panel.
137
     */
138
    protected function initPanel()
139
    {
140
        $this->panel = new Panel($this);
141
    }
142
143
    /**
144
     * Get panel instance.
145
     *
146
     * @return Panel
147
     */
148
    public function panel()
149
    {
150
        return $this->panel;
151
    }
152
153
    /**
154
     * Overwrite properties for field and relation.
155
     *
156
     * @return Show
157
     */
158
    public function overWrite()
159
    {
160
        $this->overWrite = true;
161
162
        return $this;
163
    }
164
165
    /**
166
     * Add a model field to show.
167
     *
168
     * @param string $name
169
     * @param string $label
170
     *
171
     * @return Field
172
     */
173
    public function field($name, $label = '')
174
    {
175
        return $this->addField($name, $label);
176
    }
177
178
    /**
179
     * Add multiple fields.
180
     *
181
     * @param array $fields
182
     *
183
     * @return $this
184
     */
185
    public function fields(array $fields = [])
186
    {
187
        if (!Arr::isAssoc($fields)) {
188
            $fields = array_combine($fields, $fields);
189
        }
190
191
        foreach ($fields as $field => $label) {
192
            $this->field($field, $label);
193
        }
194
195
        return $this;
196
    }
197
198
    /**
199
     * Show all fields.
200
     *
201
     * @return Show
202
     */
203
    public function all()
204
    {
205
        $fields = array_keys($this->model->getAttributes());
206
207
        return $this->fields($fields);
208
    }
209
210
    /**
211
     * Add a relation to show.
212
     *
213
     * @param string          $name
214
     * @param string|\Closure $label
215
     * @param null|\Closure   $builder
216
     *
217
     * @return Relation
218
     */
219
    public function relation($name, $label, $builder = null)
220
    {
221
        if (is_null($builder)) {
222
            $builder = $label;
223
            $label = '';
224
        }
225
226
        return $this->addRelation($name, $builder, $label);
0 ignored issues
show
Bug introduced by
It seems like $builder can also be of type string; however, Encore\Admin\Show::addRelation() does only seem to accept object<Closure>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $label defined by parameter $label on line 219 can also be of type object<Closure>; however, Encore\Admin\Show::addRelation() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
227
    }
228
229
    /**
230
     * Add a model field to show.
231
     *
232
     * @param string $name
233
     * @param string $label
234
     *
235
     * @return Field
236
     */
237
    protected function addField($name, $label = '')
238
    {
239
        $field = new Field($name, $label);
240
241
        $field->setParent($this);
242
        if ($this->overWrite) {
243
            $this->overwriteExistingField($name);
244
        }
245
246
        return tap($field, function ($field) {
247
            $this->fields->push($field);
248
        });
249
    }
250
251
    /**
252
     * Add a relation panel to show.
253
     *
254
     * @param string   $name
255
     * @param \Closure $builder
256
     * @param string   $label
257
     *
258
     * @return Relation
259
     */
260
    protected function addRelation($name, $builder, $label = '')
261
    {
262
        $relation = new Relation($name, $builder, $label);
263
264
        $relation->setParent($this);
265
266
        if ($this->overWrite) {
267
            $this->overwriteExistingRelation($name);
268
        }
269
270
        return tap($relation, function ($relation) {
271
            $this->relations->push($relation);
272
        });
273
    }
274
275
    /**
276
     * Overwrite existing field.
277
     *
278
     * @param string $name
279
     */
280
    protected function overwriteExistingField($name)
281
    {
282
        if ($this->fields->isEmpty()) {
283
            return;
284
        }
285
286
        $this->fields = $this->fields->filter(
287
            function (Field $field) use ($name) {
288
                return $field->getName() != $name;
289
            }
290
        );
291
    }
292
293
    /**
294
     * Overwrite existing relation.
295
     *
296
     * @param string $name
297
     */
298
    protected function overwriteExistingRelation($name)
299
    {
300
        if ($this->relations->isEmpty()) {
301
            return;
302
        }
303
304
        $this->relations = $this->relations->filter(
305
            function (Relation $relation) use ($name) {
306
                return $relation->getName() != $name;
307
            }
308
        );
309
    }
310
311
    /**
312
     * Show a divider.
313
     */
314
    public function divider()
315
    {
316
        $this->fields->push(new Divider());
317
    }
318
319
    /**
320
     * Set resource path.
321
     *
322
     * @param string $resource
323
     *
324
     * @return $this
325
     */
326
    public function setResource($resource)
327
    {
328
        $this->resource = $resource;
329
330
        return $this;
331
    }
332
333
    /**
334
     * Get resource path.
335
     *
336
     * @return string
337
     */
338
    public function getResourcePath()
339
    {
340
        if (empty($this->resource)) {
341
            $path = request()->path();
342
343
            $segments = explode('/', $path);
344
            array_pop($segments);
345
346
            $this->resource = implode('/', $segments);
347
        }
348
349
        return url($this->resource);
350
    }
351
352
    /**
353
     * Set field and label width in fields.
354
     *
355
     * @param int $fieldWidth
356
     * @param int $labelWidth
357
     *
358
     * @return $this
359
     */
360
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
361
    {
362
        collect($this->fields)->each(function ($field) use ($fieldWidth, $labelWidth) {
363
            $field->each->setWidth($fieldWidth, $labelWidth);
364
        });
365
366
        return $this;
367
    }
368
369
    /**
370
     * Set the model instance.
371
     *
372
     * @param Model $model
373
     *
374
     * @return $this
375
     */
376
    public function setModel($model)
377
    {
378
        $this->model = $model;
379
380
        return $this;
381
    }
382
383
    /**
384
     * Get the model instance being queried.
385
     *
386
     * @return Model
387
     */
388
    public function getModel()
389
    {
390
        return $this->model;
391
    }
392
393
    /**
394
     * Add field and relation dynamically.
395
     *
396
     * @param string $method
397
     * @param array  $arguments
398
     *
399
     * @return bool|mixed
400
     */
401
    public function __call($method, $arguments = [])
402
    {
403
        $label = isset($arguments[0]) ? $arguments[0] : ucfirst($method);
404
405
        if ($field = $this->handleGetMutatorField($method, $label)) {
406
            return $field;
407
        }
408
409
        if ($field = $this->handleRelationField($method, $arguments)) {
410
            return $field;
411
        }
412
413
        return $this->addField($method, $label);
414
    }
415
416
    /**
417
     * Handle the get mutator field.
418
     *
419
     * @param string $method
420
     * @param string $label
421
     *
422
     * @return bool|Field
423
     */
424
    protected function handleGetMutatorField($method, $label)
425
    {
426
        if (is_null($this->model)) {
427
            return false;
428
        }
429
430
        if ($this->model->hasGetMutator($method)) {
431
            return $this->addField($method, $label);
432
        }
433
434
        return false;
435
    }
436
437
    /**
438
     * Handle relation field.
439
     *
440
     * @param string $method
441
     * @param array  $arguments
442
     *
443
     * @return $this|bool|Relation|Field
444
     */
445
    protected function handleRelationField($method, $arguments)
446
    {
447
        if (!method_exists($this->model, $method)) {
448
            return false;
449
        }
450
451
        if (!($relation = $this->model->$method()) instanceof EloquentRelation) {
452
            return false;
453
        }
454
455
        if ($relation    instanceof HasOne
456
            || $relation instanceof BelongsTo
457
            || $relation instanceof MorphOne
458
        ) {
459
            $this->model->with($method);
460
461
            if (count($arguments) == 1 && $arguments[0] instanceof \Closure) {
462
                return $this->addRelation($method, $arguments[0]);
463
            }
464
465 View Code Duplication
            if (count($arguments) == 2 && $arguments[1] instanceof \Closure) {
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...
466
                return $this->addRelation($method, $arguments[1], $arguments[0]);
467
            }
468
469
            return $this->addField($method, Arr::get($arguments, 0))->setRelation(Str::snake($method));
470
        }
471
472
        if ($relation    instanceof HasMany
473
            || $relation instanceof MorphMany
474
            || $relation instanceof BelongsToMany
475
            || $relation instanceof HasManyThrough
476
        ) {
477
            if (empty($arguments) || (count($arguments) == 1 && is_string($arguments[0]))) {
478
                return $this->showRelationAsField($method, $arguments[0] ?? '');
479
            }
480
481
            $this->model->with($method);
482
483
            if (count($arguments) == 1 && is_callable($arguments[0])) {
484
                return $this->addRelation($method, $arguments[0]);
0 ignored issues
show
Documentation introduced by
$arguments[0] is of type callable, 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...
485 View Code Duplication
            } elseif (count($arguments) == 2 && is_callable($arguments[1])) {
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...
486
                return $this->addRelation($method, $arguments[1], $arguments[0]);
0 ignored issues
show
Documentation introduced by
$arguments[1] is of type callable, 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...
487
            }
488
489
            throw new \InvalidArgumentException('Invalid eloquent relation');
490
        }
491
492
        return false;
493
    }
494
495
    /**
496
     * @param string $relation
497
     * @param string $label
498
     *
499
     * @return Field
500
     */
501
    protected function showRelationAsField($relation = '', $label = '')
502
    {
503
        return $this->addField($relation, $label);
504
    }
505
506
    /**
507
     * Handle model field.
508
     *
509
     * @param string $method
510
     * @param string $label
511
     *
512
     * @return bool|Field
513
     */
514
    protected function handleModelField($method, $label)
515
    {
516
        if (in_array($method, $this->model->getAttributes())) {
517
            return $this->addField($method, $label);
518
        }
519
520
        return false;
521
    }
522
523
    /**
524
     * Render the show panels.
525
     *
526
     * @return string
527
     */
528
    public function render()
529
    {
530
        if (is_callable($this->builder)) {
531
            call_user_func($this->builder, $this);
532
        }
533
534
        if ($this->fields->isEmpty()) {
535
            $this->all();
536
        }
537
538
        if (is_array($this->builder)) {
539
            $this->fields($this->builder);
540
        }
541
542
        $this->fields->each->setValue($this->model);
543
        $this->relations->each->setModel($this->model);
544
545
        $data = [
546
            'panel'     => $this->panel->fill($this->fields),
547
            'relations' => $this->relations,
548
        ];
549
550
        return view('admin::show', $data)->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...
551
    }
552
}
553