Completed
Pull Request — master (#3054)
by
unknown
02:23
created

Show::handleRelationField()   D

Complexity

Conditions 21
Paths 10

Size

Total Lines 49

Duplication

Lines 6
Ratio 12.24 %

Importance

Changes 0
Metric Value
cc 21
nc 10
nop 2
dl 6
loc 49
rs 4.1666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
22
class Show implements Renderable
23
{
24
    /**
25
     * The Eloquent model to show.
26
     *
27
     * @var Model
28
     */
29
    protected $model;
30
31
    /**
32
     * Show panel builder.
33
     *
34
     * @var callable
35
     */
36
    protected $builder;
37
38
    /**
39
     * Resource path for this show page.
40
     *
41
     * @var string
42
     */
43
    protected $resource;
44
45
    /**
46
     * Fields to be show.
47
     *
48
     * @var Collection
49
     */
50
    protected $fields;
51
52
    /**
53
     * Relations to be show.
54
     *
55
     * @var Collection
56
     */
57
    protected $relations;
58
59
    /**
60
     * @var Panel
61
     */
62
    protected $panel;
63
64
    /**
65
     * @var Closure
66
     */
67
    protected static $initCallback;
68
69
    /**
70
     * Show constructor.
71
     *
72
     * @param Model $model
73
     * @param mixed $builder
74
     */
75
    public function __construct($model, $builder = null)
76
    {
77
        $this->model = $model;
78
        $this->builder = $builder;
79
80
        $this->initPanel();
81
        $this->initContents();
82
83
        if (static::$initCallback instanceof Closure) {
0 ignored issues
show
Bug introduced by
The class Encore\Admin\Closure does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
84
            call_user_func(static::$initCallback, $this);
85
        }
86
    }
87
88
    /**
89
     * Initialize with user pre-defined default disables, etc.
90
     *
91
     * @param Closure $callback
92
     */
93
    public static function init(Closure $callback = null)
94
    {
95
        static::$initCallback = $callback;
96
    }
97
98
    /**
99
     * Initialize the contents to show.
100
     */
101
    protected function initContents()
102
    {
103
        $this->fields = new Collection();
104
        $this->relations = new Collection();
105
    }
106
107
    /**
108
     * Initialize panel.
109
     */
110
    protected function initPanel()
111
    {
112
        $this->panel = new Panel($this);
113
    }
114
115
    /**
116
     * Get panel instance.
117
     *
118
     * @return Panel
119
     */
120
    public function panel()
121
    {
122
        return $this->panel;
123
    }
124
125
    /**
126
     * Add a model field to show.
127
     *
128
     * @param string $name
129
     * @param string $label
130
     *
131
     * @return Field
132
     */
133
    public function field($name, $label = '')
134
    {
135
        return $this->addField($name, $label);
136
    }
137
138
    /**
139
     * Add multiple fields.
140
     *
141
     * @param array $fields
142
     *
143
     * @return $this
144
     */
145
    public function fields(array $fields = [])
146
    {
147
        if (!Arr::isAssoc($fields)) {
148
            $fields = array_combine($fields, $fields);
149
        }
150
151
        foreach ($fields as $field => $label) {
152
            $this->field($field, $label);
153
        }
154
155
        return $this;
156
    }
157
158
    /**
159
     * Show all fields.
160
     *
161
     * @return Show
162
     */
163
    public function all()
164
    {
165
        $fields = array_keys($this->model->getAttributes());
166
167
        return $this->fields($fields);
168
    }
169
170
    /**
171
     * Add a relation to show.
172
     *
173
     * @param string          $name
174
     * @param string|\Closure $label
175
     * @param null|\Closure   $builder
176
     *
177
     * @return Relation
178
     */
179
    public function relation($name, $label, $builder = null)
180
    {
181
        if (is_null($builder)) {
182
            $builder = $label;
183
            $label = '';
184
        }
185
186
        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 179 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...
187
    }
188
189
    /**
190
     * Add a model field to show.
191
     *
192
     * @param string $name
193
     * @param string $label
194
     *
195
     * @return Field
196
     */
197
    protected function addField($name, $label = '')
198
    {
199
        $field = new Field($name, $label);
200
201
        $field->setParent($this);
202
203
        $this->overwriteExistingField($name);
204
205
        return tap($field, function ($field) {
206
            $this->fields->push($field);
207
        });
208
    }
209
210
    /**
211
     * Add a relation panel to show.
212
     *
213
     * @param string   $name
214
     * @param \Closure $builder
215
     * @param string   $label
216
     *
217
     * @return Relation
218
     */
219
    protected function addRelation($name, $builder, $label = '')
220
    {
221
        $relation = new Relation($name, $builder, $label);
222
223
        $relation->setParent($this);
224
225
        $this->overwriteExistingRelation($name);
226
227
        return tap($relation, function ($relation) {
228
            $this->relations->push($relation);
229
        });
230
    }
231
232
    /**
233
     * Overwrite existing field.
234
     *
235
     * @param string $name
236
     */
237
    protected function overwriteExistingField($name)
238
    {
239
        if ($this->fields->isEmpty()) {
240
            return;
241
        }
242
243
        $this->fields = $this->fields->filter(
244
            function (Field $field) use ($name) {
245
                return $field->getName() != $name;
246
            }
247
        );
248
    }
249
250
    /**
251
     * Overwrite existing relation.
252
     *
253
     * @param string $name
254
     */
255
    protected function overwriteExistingRelation($name)
256
    {
257
        if ($this->relations->isEmpty()) {
258
            return;
259
        }
260
261
        $this->relations = $this->relations->filter(
262
            function (Relation $relation) use ($name) {
263
                return $relation->getName() != $name;
264
            }
265
        );
266
    }
267
268
    /**
269
     * Show a divider.
270
     */
271
    public function divider()
272
    {
273
        $this->fields->push(new Divider());
274
    }
275
276
    /**
277
     * Set resource path.
278
     *
279
     * @param string $resource
280
     *
281
     * @return $this
282
     */
283
    public function setResource($resource)
284
    {
285
        $this->resource = $resource;
286
287
        return $this;
288
    }
289
290
    /**
291
     * Get resource path.
292
     *
293
     * @return string
294
     */
295
    public function getResourcePath()
296
    {
297
        if (empty($this->resource)) {
298
            $path = request()->path();
299
300
            $segments = explode('/', $path);
301
            array_pop($segments);
302
303
            $this->resource = implode('/', $segments);
304
        }
305
306
        return $this->resource;
307
    }
308
309
    /**
310
     * Set the model instance.
311
     *
312
     * @param Model $model
313
     *
314
     * @return $this
315
     */
316
    public function setModel($model)
317
    {
318
        $this->model = $model;
319
320
        return $this;
321
    }
322
323
    /**
324
     * Get the model instance being queried.
325
     *
326
     * @return Model
327
     */
328
    public function getModel()
329
    {
330
        return $this->model;
331
    }
332
333
    /**
334
     * Add field and relation dynamically.
335
     *
336
     * @param string $method
337
     * @param array  $arguments
338
     *
339
     * @return bool|mixed
340
     */
341
    public function __call($method, $arguments = [])
342
    {
343
        $label = isset($arguments[0]) ? $arguments[0] : ucfirst($method);
344
345
        if ($field = $this->handleGetMutatorField($method, $label)) {
346
            return $field;
347
        }
348
349
        if ($field = $this->handleRelationField($method, $arguments)) {
350
            return $field;
351
        }
352
353
        if ($field = $this->handleModelField($method, $label)) {
354
            return $field;
355
        }
356
357
        return $this->addField($method, $label);
358
    }
359
360
    /**
361
     * Handle the get mutator field.
362
     *
363
     * @param string $method
364
     * @param string $label
365
     *
366
     * @return bool|Field
367
     */
368
    protected function handleGetMutatorField($method, $label)
369
    {
370
        if (is_null($this->model)) {
371
            return false;
372
        }
373
374
        if ($this->model->hasGetMutator($method)) {
375
            return $this->addField($method, $label);
376
        }
377
378
        return false;
379
    }
380
381
    /**
382
     * Handle relation field.
383
     *
384
     * @param string $method
385
     * @param array  $arguments
386
     *
387
     * @return $this|bool|Relation|Field
388
     */
389
    protected function handleRelationField($method, $arguments)
390
    {
391
        if (!method_exists($this->model, $method)) {
392
            return false;
393
        }
394
395
        if (!($relation = $this->model->$method()) instanceof EloquentRelation) {
396
            return false;
397
        }
398
399
        if ($relation    instanceof HasOne
400
            || $relation instanceof BelongsTo
401
            || $relation instanceof MorphOne
402
        ) {
403
            $this->model->with($method);
404
405
            if (count($arguments) == 1 && $arguments[0] instanceof \Closure) {
406
                return $this->addRelation($method, $arguments[0]);
407
            }
408
409 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...
410
                return $this->addRelation($method, $arguments[1], $arguments[0]);
411
            }
412
413
            return $this->addField($method, array_get($arguments, 0))->setRelation(snake_case($method));
414
        }
415
416
        if ($relation    instanceof HasMany
417
            || $relation instanceof MorphMany
418
            || $relation instanceof BelongsToMany
419
            || $relation instanceof HasManyThrough
420
        ) {
421
            if (empty($arguments) || (count($arguments) == 1 && is_string($arguments[0]))) {
422
                return $this->showRelationAsField($method, $arguments[0] ?? '');
423
            }
424
425
            $this->model->with($method);
426
427
            if (count($arguments) == 1 && is_callable($arguments[0])) {
428
                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...
429 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...
430
                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...
431
            }
432
433
            throw new \InvalidArgumentException('Invalid eloquent relation');
434
        }
435
436
        return false;
437
    }
438
439
    protected function showRelationAsField($relation = '', $label = '')
440
    {
441
        return $this->addField($relation, $label);
442
    }
443
444
    /**
445
     * Handle model field.
446
     *
447
     * @param string $method
448
     * @param string $label
449
     *
450
     * @return bool|Field
451
     */
452
    protected function handleModelField($method, $label)
453
    {
454
        if (in_array($method, $this->model->getAttributes())) {
455
            return $this->addField($method, $label);
456
        }
457
458
        return false;
459
    }
460
461
    /**
462
     * Render the show panels.
463
     *
464
     * @return string
465
     */
466
    public function render()
467
    {
468
        if (is_callable($this->builder)) {
469
            call_user_func($this->builder, $this);
470
        }
471
472
        if ($this->fields->isEmpty()) {
473
            $this->all();
474
        }
475
476
        if (is_array($this->builder)) {
477
            $this->fields($this->builder);
478
        }
479
480
        $this->fields->each->setValue($this->model);
481
        $this->relations->each->setModel($this->model);
482
483
        $data = [
484
            'panel'     => $this->panel->fill($this->fields),
485
            'relations' => $this->relations,
486
        ];
487
488
        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...
489
    }
490
}
491