Completed
Push — master ( 024746...0b49f1 )
by Song
04:00
created

Show::initContents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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