Completed
Pull Request — master (#2014)
by Song
02:47
created

Form   F

Complexity

Total Complexity 158

Size/Duplication

Total Lines 1345
Duplicated Lines 5.5 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 74
loc 1345
rs 0.5217
wmc 158
lcom 1
cbo 16

62 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A pushField() 0 8 1
A model() 0 4 1
A builder() 0 4 1
A edit() 0 9 1
A view() 0 9 1
A tab() 0 6 1
A getTab() 0 8 2
A destroy() 0 14 3
A deleteFilesAndImages() 0 13 1
B store() 11 35 6
A redirectAfterStore() 8 8 2
A ajaxResponse() 0 14 3
A prepare() 0 16 3
A removeIgnoredFields() 0 6 1
A getRelationInputs() 0 16 4
A callSubmitted() 0 6 2
A callSaving() 0 6 2
A complete() 0 6 2
A redirectAfterUpdate() 8 8 2
A isEditable() 0 4 1
A handleEditable() 0 12 2
A handleFileDelete() 0 11 2
A handleOrderable() 0 14 4
C updateRelation() 0 78 18
B invalidColumn() 0 11 6
B prepareInsert() 0 23 5
A isHasOneRelation() 0 14 3
A submitted() 0 4 1
A saving() 0 4 1
A saved() 0 4 1
A ignore() 0 6 1
B getDataByColumn() 18 18 5
A getFieldByColumn() 0 12 2
A setFieldOriginalValue() 0 10 1
A setFieldValue() 0 14 1
A doNotSnakeAttributes() 0 6 1
B validationMessages() 0 18 6
A mergeValidationMessages() 0 10 2
C getRelations() 0 26 8
A setAction() 0 6 1
A setWidth() 0 11 1
A setView() 0 6 1
C prepareUpdate() 7 31 7
C update() 12 58 9
A setTitle() 0 6 1
A row() 0 6 1
A tools() 0 4 1
A disableSubmit() 0 6 1
A disableReset() 0 6 1
A resource() 0 14 4
A render() 0 8 2
A input() 0 8 2
A registerBuiltinFields() 0 55 2
A extend() 0 4 1
A forget() 0 4 1
A findFieldClass() 10 10 2
B collectFieldAssets() 0 25 4
A __get() 0 4 1
A __set() 0 4 1
A __call() 0 12 2
A __toString() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Form 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 Form, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Encore\Admin;
4
5
use Closure;
6
use Encore\Admin\Exception\Handler;
7
use Encore\Admin\Form\Builder;
8
use Encore\Admin\Form\Field;
9
use Encore\Admin\Form\Field\File;
10
use Encore\Admin\Form\Row;
11
use Encore\Admin\Form\Tab;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Relations\Relation;
14
use Illuminate\Http\Request;
15
use Illuminate\Support\Arr;
16
use Illuminate\Support\Facades\DB;
17
use Illuminate\Support\Facades\Input;
18
use Illuminate\Support\MessageBag;
19
use Illuminate\Support\Str;
20
use Illuminate\Validation\Validator;
21
use Spatie\EloquentSortable\Sortable;
22
use Symfony\Component\HttpFoundation\Response;
23
24
/**
25
 * Class Form.
26
 *
27
 * @method Field\Text           text($column, $label = '')
28
 * @method Field\Checkbox       checkbox($column, $label = '')
29
 * @method Field\Radio          radio($column, $label = '')
30
 * @method Field\Select         select($column, $label = '')
31
 * @method Field\MultipleSelect multipleSelect($column, $label = '')
32
 * @method Field\Textarea       textarea($column, $label = '')
33
 * @method Field\Hidden         hidden($column, $label = '')
34
 * @method Field\Id             id($column, $label = '')
35
 * @method Field\Ip             ip($column, $label = '')
36
 * @method Field\Url            url($column, $label = '')
37
 * @method Field\Color          color($column, $label = '')
38
 * @method Field\Email          email($column, $label = '')
39
 * @method Field\Mobile         mobile($column, $label = '')
40
 * @method Field\Slider         slider($column, $label = '')
41
 * @method Field\Map            map($latitude, $longitude, $label = '')
42
 * @method Field\Editor         editor($column, $label = '')
43
 * @method Field\File           file($column, $label = '')
44
 * @method Field\Image          image($column, $label = '')
45
 * @method Field\Date           date($column, $label = '')
46
 * @method Field\Datetime       datetime($column, $label = '')
47
 * @method Field\Time           time($column, $label = '')
48
 * @method Field\Year           year($column, $label = '')
49
 * @method Field\Month          month($column, $label = '')
50
 * @method Field\DateRange      dateRange($start, $end, $label = '')
51
 * @method Field\DateTimeRange  datetimeRange($start, $end, $label = '')
52
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
53
 * @method Field\Number         number($column, $label = '')
54
 * @method Field\Currency       currency($column, $label = '')
55
 * @method Field\HasMany        hasMany($relationName, $callback)
56
 * @method Field\SwitchField    switch($column, $label = '')
57
 * @method Field\Display        display($column, $label = '')
58
 * @method Field\Rate           rate($column, $label = '')
59
 * @method Field\Divide         divider()
60
 * @method Field\Password       password($column, $label = '')
61
 * @method Field\Decimal        decimal($column, $label = '')
62
 * @method Field\Html           html($html, $label = '')
63
 * @method Field\Tags           tags($column, $label = '')
64
 * @method Field\Icon           icon($column, $label = '')
65
 * @method Field\Embeds         embeds($column, $label = '')
66
 * @method Field\MultipleImage  multipleImage($column, $label = '')
67
 * @method Field\MultipleFile   multipleFile($column, $label = '')
68
 * @method Field\Captcha        captcha($column, $label = '')
69
 * @method Field\Listbox        listbox($column, $label = '')
70
 */
71
class Form
72
{
73
    /**
74
     * Eloquent model of the form.
75
     *
76
     * @var Model
77
     */
78
    protected $model;
79
80
    /**
81
     * @var \Illuminate\Validation\Validator
82
     */
83
    protected $validator;
84
85
    /**
86
     * @var Builder
87
     */
88
    protected $builder;
89
90
    /**
91
     * Submitted callback.
92
     *
93
     * @var Closure
94
     */
95
    protected $submitted;
96
97
    /**
98
     * Saving callback.
99
     *
100
     * @var Closure
101
     */
102
    protected $saving;
103
104
    /**
105
     * Saved callback.
106
     *
107
     * @var Closure
108
     */
109
    protected $saved;
110
111
    /**
112
     * Data for save to current model from input.
113
     *
114
     * @var array
115
     */
116
    protected $updates = [];
117
118
    /**
119
     * Data for save to model's relations from input.
120
     *
121
     * @var array
122
     */
123
    protected $relations = [];
124
125
    /**
126
     * Input data.
127
     *
128
     * @var array
129
     */
130
    protected $inputs = [];
131
132
    /**
133
     * Available fields.
134
     *
135
     * @var array
136
     */
137
    public static $availableFields = [];
138
139
    /**
140
     * Ignored saving fields.
141
     *
142
     * @var array
143
     */
144
    protected $ignored = [];
145
146
    /**
147
     * Collected field assets.
148
     *
149
     * @var array
150
     */
151
    protected static $collectedAssets = [];
152
153
    /**
154
     * @var Form\Tab
155
     */
156
    protected $tab = null;
157
158
    /**
159
     * Remove flag in `has many` form.
160
     */
161
    const REMOVE_FLAG_NAME = '_remove_';
162
163
    /**
164
     * Field rows in form.
165
     *
166
     * @var array
167
     */
168
    public $rows = [];
169
170
    /**
171
     * Create a new form instance.
172
     *
173
     * @param $model
174
     * @param \Closure $callback
175
     */
176
    public function __construct($model, Closure $callback)
177
    {
178
        $this->model = $model;
179
180
        $this->builder = new Builder($this);
181
182
        $callback($this);
183
    }
184
185
    /**
186
     * @param Field $field
187
     *
188
     * @return $this
189
     */
190
    public function pushField(Field $field)
191
    {
192
        $field->setForm($this);
193
194
        $this->builder->fields()->push($field);
195
196
        return $this;
197
    }
198
199
    /**
200
     * @return Model
201
     */
202
    public function model()
203
    {
204
        return $this->model;
205
    }
206
207
    /**
208
     * @return Builder
209
     */
210
    public function builder()
211
    {
212
        return $this->builder;
213
    }
214
215
    /**
216
     * Generate a edit form.
217
     *
218
     * @param $id
219
     *
220
     * @return $this
221
     */
222
    public function edit($id)
223
    {
224
        $this->builder->setMode(Builder::MODE_EDIT);
225
        $this->builder->setResourceId($id);
226
227
        $this->setFieldValue($id);
228
229
        return $this;
230
    }
231
232
    /**
233
     * @param $id
234
     *
235
     * @return $this
236
     */
237
    public function view($id)
238
    {
239
        $this->builder->setMode(Builder::MODE_VIEW);
240
        $this->builder->setResourceId($id);
241
242
        $this->setFieldValue($id);
243
244
        return $this;
245
    }
246
247
    /**
248
     * Use tab to split form.
249
     *
250
     * @param string  $title
251
     * @param Closure $content
252
     *
253
     * @return $this
254
     */
255
    public function tab($title, Closure $content, $active = false)
256
    {
257
        $this->getTab()->append($title, $content, $active);
258
259
        return $this;
260
    }
261
262
    /**
263
     * Get Tab instance.
264
     *
265
     * @return Tab
266
     */
267
    public function getTab()
268
    {
269
        if (is_null($this->tab)) {
270
            $this->tab = new Tab($this);
271
        }
272
273
        return $this->tab;
274
    }
275
276
    /**
277
     * Destroy data entity and remove files.
278
     *
279
     * @param $id
280
     *
281
     * @return mixed
282
     */
283
    public function destroy($id)
284
    {
285
        $ids = explode(',', $id);
286
287
        foreach ($ids as $id) {
288
            if (empty($id)) {
289
                continue;
290
            }
291
            $this->deleteFilesAndImages($id);
292
            $this->model->find($id)->delete();
293
        }
294
295
        return true;
296
    }
297
298
    /**
299
     * Remove files or images in record.
300
     *
301
     * @param $id
302
     */
303
    protected function deleteFilesAndImages($id)
304
    {
305
        $data = $this->model->with($this->getRelations())
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

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...
306
            ->findOrFail($id)->toArray();
307
308
        $this->builder->fields()->filter(function ($field) {
309
            return $field instanceof Field\File;
310
        })->each(function (File $file) use ($data) {
311
            $file->setOriginal($data);
312
313
            $file->destroy();
314
        });
315
    }
316
317
    /**
318
     * Store a new record.
319
     *
320
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
321
     */
322
    public function store()
323
    {
324
        $data = Input::all();
325
326
        // Handle validation errors.
327
        if ($validationMessages = $this->validationMessages($data)) {
328
            return back()->withInput()->withErrors($validationMessages);
329
        }
330
331
        if (($response = $this->prepare($data)) instanceof Response) {
332
            return $response;
333
        }
334
335 View Code Duplication
        DB::transaction(function () {
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...
336
            $inserts = $this->prepareInsert($this->updates);
337
338
            foreach ($inserts as $column => $value) {
339
                $this->model->setAttribute($column, $value);
340
            }
341
342
            $this->model->save();
343
344
            $this->updateRelation($this->relations);
345
        });
346
347
        if (($response = $this->complete($this->saved)) instanceof Response) {
348
            return $response;
349
        }
350
351
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
352
            return $response;
353
        }
354
355
        return $this->redirectAfterStore();
356
    }
357
358
    /**
359
     * Get RedirectResponse after store.
360
     *
361
     * @return \Illuminate\Http\RedirectResponse
362
     */
363 View Code Duplication
    protected function redirectAfterStore()
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...
364
    {
365
        admin_toastr(trans('admin.save_succeeded'));
366
367
        $url = Input::get(Builder::PREVIOUS_URL_KEY) ?: $this->resource(0);
368
369
        return redirect($url);
370
    }
371
372
    /**
373
     * Get ajax response.
374
     *
375
     * @param string $message
376
     *
377
     * @return bool|\Illuminate\Http\JsonResponse
378
     */
379
    protected function ajaxResponse($message)
380
    {
381
        $request = Request::capture();
382
383
        // ajax but not pjax
384
        if ($request->ajax() && !$request->pjax()) {
385
            return response()->json([
386
                'status'  => true,
387
                'message' => $message,
388
            ]);
389
        }
390
391
        return false;
392
    }
393
394
    /**
395
     * Prepare input data for insert or update.
396
     *
397
     * @param array $data
398
     *
399
     * @return mixed
400
     */
401
    protected function prepare($data = [])
402
    {
403
        if (($response = $this->callSubmitted()) instanceof Response) {
404
            return $response;
405
        }
406
407
        $this->inputs = $this->removeIgnoredFields($data);
408
409
        if (($response = $this->callSaving()) instanceof Response) {
410
            return $response;
411
        }
412
413
        $this->relations = $this->getRelationInputs($this->inputs);
414
415
        $this->updates = array_except($this->inputs, array_keys($this->relations));
416
    }
417
418
    /**
419
     * Remove ignored fields from input.
420
     *
421
     * @param array $input
422
     *
423
     * @return array
424
     */
425
    protected function removeIgnoredFields($input)
426
    {
427
        array_forget($input, $this->ignored);
428
429
        return $input;
430
    }
431
432
    /**
433
     * Get inputs for relations.
434
     *
435
     * @param array $inputs
436
     *
437
     * @return array
438
     */
439
    protected function getRelationInputs($inputs = [])
440
    {
441
        $relations = [];
442
443
        foreach ($inputs as $column => $value) {
444
            if (method_exists($this->model, $column)) {
445
                $relation = call_user_func([$this->model, $column]);
446
447
                if ($relation instanceof Relation) {
448
                    $relations[$column] = $value;
449
                }
450
            }
451
        }
452
453
        return $relations;
454
    }
455
456
    /**
457
     * Call submitted callback.
458
     *
459
     * @return mixed
460
     */
461
    protected function callSubmitted()
462
    {
463
        if ($this->submitted instanceof Closure) {
464
            return call_user_func($this->submitted, $this);
465
        }
466
    }
467
468
    /**
469
     * Call saving callback.
470
     *
471
     * @return mixed
472
     */
473
    protected function callSaving()
474
    {
475
        if ($this->saving instanceof Closure) {
476
            return call_user_func($this->saving, $this);
477
        }
478
    }
479
480
    /**
481
     * Callback after saving a Model.
482
     *
483
     * @param Closure|null $callback
484
     *
485
     * @return mixed|null
486
     */
487
    protected function complete(Closure $callback = null)
488
    {
489
        if ($callback instanceof Closure) {
490
            return $callback($this);
491
        }
492
    }
493
494
    /**
495
     * Handle update.
496
     *
497
     * @param int $id
498
     *
499
     * @return \Symfony\Component\HttpFoundation\Response
500
     */
501
    public function update($id, $data = null)
502
    {
503
        $data = ($data) ?: Input::all();
504
505
        $isEditable = $this->isEditable($data);
506
507
        $data = $this->handleEditable($data);
508
509
        $data = $this->handleFileDelete($data);
510
511
        if ($this->handleOrderable($id, $data)) {
512
            return response([
0 ignored issues
show
Documentation introduced by
array('status' => true, ...min.update_succeeded')) is of type array<string,boolean|obj...r>|string|array|null"}>, but the function expects a string.

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...
513
                'status'  => true,
514
                'message' => trans('admin.update_succeeded'),
515
            ]);
516
        }
517
518
        /* @var Model $this->model */
519
        $this->model = $this->model->with($this->getRelations())->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

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...
520
521
        $this->setFieldOriginalValue();
522
523
        // Handle validation errors.
524
        if ($validationMessages = $this->validationMessages($data)) {
525
            if (!$isEditable) {
526
                return back()->withInput()->withErrors($validationMessages);
527
            } else {
528
                return response()->json(['errors' => array_dot($validationMessages->getMessages())], 422);
529
            }
530
        }
531
532
        if (($response = $this->prepare($data)) instanceof Response) {
533
            return $response;
534
        }
535
536 View Code Duplication
        DB::transaction(function () {
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...
537
            $updates = $this->prepareUpdate($this->updates);
538
539
            foreach ($updates as $column => $value) {
540
                /* @var Model $this->model */
541
                $this->model->setAttribute($column, $value);
542
            }
543
544
            $this->model->save();
545
546
            $this->updateRelation($this->relations);
547
        });
548
549
        if (($result = $this->complete($this->saved)) instanceof Response) {
550
            return $result;
551
        }
552
553
        if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) {
554
            return $response;
555
        }
556
557
        return $this->redirectAfterUpdate();
558
    }
559
560
    /**
561
     * Get RedirectResponse after update.
562
     *
563
     * @return \Illuminate\Http\RedirectResponse
564
     */
565 View Code Duplication
    protected function redirectAfterUpdate()
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...
566
    {
567
        admin_toastr(trans('admin.update_succeeded'));
568
569
        $url = Input::get(Builder::PREVIOUS_URL_KEY) ?: $this->resource(-1);
570
571
        return redirect($url);
572
    }
573
574
    /**
575
     * Check if request is from editable.
576
     *
577
     * @param array $input
578
     *
579
     * @return bool
580
     */
581
    protected function isEditable(array $input = [])
582
    {
583
        return array_key_exists('_editable', $input);
584
    }
585
586
    /**
587
     * Handle editable update.
588
     *
589
     * @param array $input
590
     *
591
     * @return array
592
     */
593
    protected function handleEditable(array $input = [])
594
    {
595
        if (array_key_exists('_editable', $input)) {
596
            $name = $input['name'];
597
            $value = $input['value'];
598
599
            array_forget($input, ['pk', 'value', 'name']);
600
            array_set($input, $name, $value);
601
        }
602
603
        return $input;
604
    }
605
606
    /**
607
     * @param array $input
608
     *
609
     * @return array
610
     */
611
    protected function handleFileDelete(array $input = [])
612
    {
613
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
614
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
615
            unset($input['key']);
616
        }
617
618
        Input::replace($input);
619
620
        return $input;
621
    }
622
623
    /**
624
     * Handle orderable update.
625
     *
626
     * @param int   $id
627
     * @param array $input
628
     *
629
     * @return bool
630
     */
631
    protected function handleOrderable($id, array $input = [])
632
    {
633
        if (array_key_exists('_orderable', $input)) {
634
            $model = $this->model->find($id);
635
636
            if ($model instanceof Sortable) {
0 ignored issues
show
Bug introduced by
The class Spatie\EloquentSortable\Sortable 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...
637
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
638
639
                return true;
640
            }
641
        }
642
643
        return false;
644
    }
645
646
    /**
647
     * Update relation data.
648
     *
649
     * @param array $relationsData
650
     *
651
     * @return void
652
     */
653
    protected function updateRelation($relationsData)
654
    {
655
        foreach ($relationsData as $name => $values) {
656
            if (!method_exists($this->model, $name)) {
657
                continue;
658
            }
659
660
            $relation = $this->model->$name();
661
662
            $oneToOneRelation = $relation instanceof \Illuminate\Database\Eloquent\Relations\HasOne
663
                || $relation instanceof \Illuminate\Database\Eloquent\Relations\MorphOne;
664
665
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
666
667
            if (empty($prepared)) {
668
                continue;
669
            }
670
671
            switch (get_class($relation)) {
672
                case \Illuminate\Database\Eloquent\Relations\BelongsToMany::class:
673
                case \Illuminate\Database\Eloquent\Relations\MorphToMany::class:
674
                    if (isset($prepared[$name])) {
675
                        $relation->sync($prepared[$name]);
676
                    }
677
                    break;
678
                case \Illuminate\Database\Eloquent\Relations\HasOne::class:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
679
680
                    $related = $this->model->$name;
681
682
                    // if related is empty
683
                    if (is_null($related)) {
684
                        $related = $relation->getRelated();
685
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
686
                    }
687
688
                    foreach ($prepared[$name] as $column => $value) {
689
                        $related->setAttribute($column, $value);
690
                    }
691
692
                    $related->save();
693
                    break;
694
                case \Illuminate\Database\Eloquent\Relations\MorphOne::class:
695
                    $related = $this->model->$name;
696
                    if (is_null($related)) {
697
                        $related = $relation->make();
698
                    }
699
                    foreach ($prepared[$name] as $column => $value) {
700
                        $related->setAttribute($column, $value);
701
                    }
702
                    $related->save();
703
                    break;
704
                case \Illuminate\Database\Eloquent\Relations\HasMany::class:
705
                case \Illuminate\Database\Eloquent\Relations\MorphMany::class:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
706
707
                    foreach ($prepared[$name] as $related) {
708
                        $relation = $this->model()->$name();
709
710
                        $keyName = $relation->getRelated()->getKeyName();
711
712
                        $instance = $relation->findOrNew(array_get($related, $keyName));
713
714
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
715
                            $instance->delete();
716
717
                            continue;
718
                        }
719
720
                        array_forget($related, static::REMOVE_FLAG_NAME);
721
722
                        $instance->fill($related);
723
724
                        $instance->save();
725
                    }
726
727
                    break;
728
            }
729
        }
730
    }
731
732
    /**
733
     * Prepare input data for update.
734
     *
735
     * @param array $updates
736
     * @param bool  $oneToOneRelation If column is one-to-one relation.
737
     *
738
     * @return array
739
     */
740
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
741
    {
742
        $prepared = [];
743
744
        foreach ($this->builder->fields() as $field) {
745
            $columns = $field->column();
746
747
            // If column not in input array data, then continue.
748
            if (!array_has($updates, $columns)) {
749
                continue;
750
            }
751
752
            if ($this->invalidColumn($columns, $oneToOneRelation)) {
753
                continue;
754
            }
755
756
            $value = $this->getDataByColumn($updates, $columns);
757
758
            $value = $field->prepare($value);
759
760 View Code Duplication
            if (is_array($columns)) {
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...
761
                foreach ($columns as $name => $column) {
762
                    array_set($prepared, $column, $value[$name]);
763
                }
764
            } elseif (is_string($columns)) {
765
                array_set($prepared, $columns, $value);
766
            }
767
        }
768
769
        return $prepared;
770
    }
771
772
    /**
773
     * @param string|array $columns
774
     * @param bool         $oneToOneRelation
775
     *
776
     * @return bool
777
     */
778
    protected function invalidColumn($columns, $oneToOneRelation = false)
779
    {
780
        foreach ((array) $columns as $column) {
781
            if ((!$oneToOneRelation && Str::contains($column, '.')) ||
782
                ($oneToOneRelation && !Str::contains($column, '.'))) {
783
                return true;
784
            }
785
        }
786
787
        return false;
788
    }
789
790
    /**
791
     * Prepare input data for insert.
792
     *
793
     * @param $inserts
794
     *
795
     * @return array
796
     */
797
    protected function prepareInsert($inserts)
798
    {
799
        if ($this->isHasOneRelation($inserts)) {
800
            $inserts = array_dot($inserts);
801
        }
802
803
        foreach ($inserts as $column => $value) {
804
            if (is_null($field = $this->getFieldByColumn($column))) {
805
                unset($inserts[$column]);
806
                continue;
807
            }
808
809
            $inserts[$column] = $field->prepare($value);
810
        }
811
812
        $prepared = [];
813
814
        foreach ($inserts as $key => $value) {
815
            array_set($prepared, $key, $value);
816
        }
817
818
        return $prepared;
819
    }
820
821
    /**
822
     * Is input data is has-one relation.
823
     *
824
     * @param array $inserts
825
     *
826
     * @return bool
827
     */
828
    protected function isHasOneRelation($inserts)
829
    {
830
        $first = current($inserts);
831
832
        if (!is_array($first)) {
833
            return false;
834
        }
835
836
        if (is_array(current($first))) {
837
            return false;
838
        }
839
840
        return Arr::isAssoc($first);
841
    }
842
843
    /**
844
     * Set submitted callback.
845
     *
846
     * @param Closure $callback
847
     *
848
     * @return void
849
     */
850
    public function submitted(Closure $callback)
851
    {
852
        $this->submitted = $callback;
853
    }
854
855
    /**
856
     * Set saving callback.
857
     *
858
     * @param Closure $callback
859
     *
860
     * @return void
861
     */
862
    public function saving(Closure $callback)
863
    {
864
        $this->saving = $callback;
865
    }
866
867
    /**
868
     * Set saved callback.
869
     *
870
     * @param callable $callback
871
     *
872
     * @return void
873
     */
874
    public function saved(Closure $callback)
875
    {
876
        $this->saved = $callback;
877
    }
878
879
    /**
880
     * Ignore fields to save.
881
     *
882
     * @param string|array $fields
883
     *
884
     * @return $this
885
     */
886
    public function ignore($fields)
887
    {
888
        $this->ignored = array_merge($this->ignored, (array) $fields);
889
890
        return $this;
891
    }
892
893
    /**
894
     * @param array        $data
895
     * @param string|array $columns
896
     *
897
     * @return array|mixed
898
     */
899 View Code Duplication
    protected function getDataByColumn($data, $columns)
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...
900
    {
901
        if (is_string($columns)) {
902
            return array_get($data, $columns);
903
        }
904
905
        if (is_array($columns)) {
906
            $value = [];
907
            foreach ($columns as $name => $column) {
908
                if (!array_has($data, $column)) {
909
                    continue;
910
                }
911
                $value[$name] = array_get($data, $column);
912
            }
913
914
            return $value;
915
        }
916
    }
917
918
    /**
919
     * Find field object by column.
920
     *
921
     * @param $column
922
     *
923
     * @return mixed
924
     */
925
    protected function getFieldByColumn($column)
926
    {
927
        return $this->builder->fields()->first(
928
            function (Field $field) use ($column) {
929
                if (is_array($field->column())) {
930
                    return in_array($column, $field->column());
931
                }
932
933
                return $field->column() == $column;
934
            }
935
        );
936
    }
937
938
    /**
939
     * Set original data for each field.
940
     *
941
     * @return void
942
     */
943
    protected function setFieldOriginalValue()
944
    {
945
//        static::doNotSnakeAttributes($this->model);
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
946
947
        $values = $this->model->toArray();
948
949
        $this->builder->fields()->each(function (Field $field) use ($values) {
950
            $field->setOriginal($values);
951
        });
952
    }
953
954
    /**
955
     * Set all fields value in form.
956
     *
957
     * @param $id
958
     *
959
     * @return void
960
     */
961
    protected function setFieldValue($id)
962
    {
963
        $relations = $this->getRelations();
964
965
        $this->model = $this->model->with($relations)->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

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...
966
967
//        static::doNotSnakeAttributes($this->model);
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
968
969
        $data = $this->model->toArray();
970
971
        $this->builder->fields()->each(function (Field $field) use ($data) {
972
            $field->fill($data);
973
        });
974
    }
975
976
    /**
977
     * Don't snake case attributes.
978
     *
979
     * @param Model $model
980
     *
981
     * @return void
982
     */
983
    protected static function doNotSnakeAttributes(Model $model)
984
    {
985
        $class = get_class($model);
986
987
        $class::$snakeAttributes = false;
988
    }
989
990
    /**
991
     * Get validation messages.
992
     *
993
     * @param array $input
994
     *
995
     * @return MessageBag|bool
996
     */
997
    protected function validationMessages($input)
998
    {
999
        $failedValidators = [];
1000
1001
        foreach ($this->builder->fields() as $field) {
1002
            if (!$validator = $field->getValidator($input)) {
1003
                continue;
1004
            }
1005
1006
            if (($validator instanceof Validator) && !$validator->passes()) {
1007
                $failedValidators[] = $validator;
1008
            }
1009
        }
1010
1011
        $message = $this->mergeValidationMessages($failedValidators);
1012
1013
        return $message->any() ? $message : false;
1014
    }
1015
1016
    /**
1017
     * Merge validation messages from input validators.
1018
     *
1019
     * @param \Illuminate\Validation\Validator[] $validators
1020
     *
1021
     * @return MessageBag
1022
     */
1023
    protected function mergeValidationMessages($validators)
1024
    {
1025
        $messageBag = new MessageBag();
1026
1027
        foreach ($validators as $validator) {
1028
            $messageBag = $messageBag->merge($validator->messages());
1029
        }
1030
1031
        return $messageBag;
1032
    }
1033
1034
    /**
1035
     * Get all relations of model from callable.
1036
     *
1037
     * @return array
1038
     */
1039
    public function getRelations()
1040
    {
1041
        $relations = $columns = [];
1042
1043
        foreach ($this->builder->fields() as $field) {
1044
            $columns[] = $field->column();
1045
        }
1046
1047
        foreach (array_flatten($columns) as $column) {
1048
            if (str_contains($column, '.')) {
1049
                list($relation) = explode('.', $column);
1050
1051
                if (method_exists($this->model, $relation) &&
1052
                    $this->model->$relation() instanceof Relation
1053
                ) {
1054
                    $relations[] = $relation;
1055
                }
1056
            } elseif (method_exists($this->model, $column) &&
1057
                !method_exists(Model::class, $column)
1058
            ) {
1059
                $relations[] = $column;
1060
            }
1061
        }
1062
1063
        return array_unique($relations);
1064
    }
1065
1066
    /**
1067
     * Set action for form.
1068
     *
1069
     * @param string $action
1070
     *
1071
     * @return $this
1072
     */
1073
    public function setAction($action)
1074
    {
1075
        $this->builder()->setAction($action);
1076
1077
        return $this;
1078
    }
1079
1080
    /**
1081
     * Set field and label width in current form.
1082
     *
1083
     * @param int $fieldWidth
1084
     * @param int $labelWidth
1085
     *
1086
     * @return $this
1087
     */
1088
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1089
    {
1090
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1091
            /* @var Field $field  */
1092
            $field->setWidth($fieldWidth, $labelWidth);
1093
        });
1094
1095
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1096
1097
        return $this;
1098
    }
1099
1100
    /**
1101
     * Set view for form.
1102
     *
1103
     * @param string $view
1104
     *
1105
     * @return $this
1106
     */
1107
    public function setView($view)
1108
    {
1109
        $this->builder()->setView($view);
1110
1111
        return $this;
1112
    }
1113
1114
    /**
1115
     * Set title for form.
1116
     *
1117
     * @param string $title
1118
     *
1119
     * @return $this
1120
     */
1121
    public function setTitle($title = '')
1122
    {
1123
        $this->builder()->setTitle($title);
1124
1125
        return $this;
1126
    }
1127
1128
    /**
1129
     * Add a row in form.
1130
     *
1131
     * @param Closure $callback
1132
     *
1133
     * @return $this
1134
     */
1135
    public function row(Closure $callback)
1136
    {
1137
        $this->rows[] = new Row($callback, $this);
1138
1139
        return $this;
1140
    }
1141
1142
    /**
1143
     * Tools setting for form.
1144
     *
1145
     * @param Closure $callback
1146
     */
1147
    public function tools(Closure $callback)
1148
    {
1149
        $callback->call($this, $this->builder->getTools());
1150
    }
1151
1152
    /**
1153
     * Disable form submit.
1154
     *
1155
     * @return $this
1156
     */
1157
    public function disableSubmit()
1158
    {
1159
        $this->builder()->options(['enableSubmit' => false]);
1160
1161
        return $this;
1162
    }
1163
1164
    /**
1165
     * Disable form reset.
1166
     *
1167
     * @return $this
1168
     */
1169
    public function disableReset()
1170
    {
1171
        $this->builder()->options(['enableReset' => false]);
1172
1173
        return $this;
1174
    }
1175
1176
    /**
1177
     * Get current resource route url.
1178
     *
1179
     * @param int $slice
1180
     *
1181
     * @return string
1182
     */
1183
    public function resource($slice = -2)
1184
    {
1185
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1186
1187
        if ($slice != 0) {
1188
            $segments = array_slice($segments, 0, $slice);
1189
        }
1190
        // # fix #1768
1191
        if ($segments[0] == 'http:' && config('admin.secure') == true) {
1192
            $segments[0] = 'https:';
1193
        }
1194
1195
        return implode('/', $segments);
1196
    }
1197
1198
    /**
1199
     * Render the form contents.
1200
     *
1201
     * @return string
1202
     */
1203
    public function render()
1204
    {
1205
        try {
1206
            return $this->builder->render();
1207
        } catch (\Exception $e) {
1208
            return Handler::renderException($e);
1209
        }
1210
    }
1211
1212
    /**
1213
     * Get or set input data.
1214
     *
1215
     * @param string $key
1216
     * @param null   $value
1217
     *
1218
     * @return array|mixed
1219
     */
1220
    public function input($key, $value = null)
1221
    {
1222
        if (is_null($value)) {
1223
            return array_get($this->inputs, $key);
1224
        }
1225
1226
        return array_set($this->inputs, $key, $value);
1227
    }
1228
1229
    /**
1230
     * Register builtin fields.
1231
     *
1232
     * @return void
1233
     */
1234
    public static function registerBuiltinFields()
1235
    {
1236
        $map = [
1237
            'button'         => \Encore\Admin\Form\Field\Button::class,
1238
            'checkbox'       => \Encore\Admin\Form\Field\Checkbox::class,
1239
            'color'          => \Encore\Admin\Form\Field\Color::class,
1240
            'currency'       => \Encore\Admin\Form\Field\Currency::class,
1241
            'date'           => \Encore\Admin\Form\Field\Date::class,
1242
            'dateRange'      => \Encore\Admin\Form\Field\DateRange::class,
1243
            'datetime'       => \Encore\Admin\Form\Field\Datetime::class,
1244
            'dateTimeRange'  => \Encore\Admin\Form\Field\DatetimeRange::class,
1245
            'datetimeRange'  => \Encore\Admin\Form\Field\DatetimeRange::class,
1246
            'decimal'        => \Encore\Admin\Form\Field\Decimal::class,
1247
            'display'        => \Encore\Admin\Form\Field\Display::class,
1248
            'divider'        => \Encore\Admin\Form\Field\Divide::class,
1249
            'divide'         => \Encore\Admin\Form\Field\Divide::class,
1250
            'embeds'         => \Encore\Admin\Form\Field\Embeds::class,
1251
            'editor'         => \Encore\Admin\Form\Field\Editor::class,
1252
            'email'          => \Encore\Admin\Form\Field\Email::class,
1253
            'file'           => \Encore\Admin\Form\Field\File::class,
1254
            'hasMany'        => \Encore\Admin\Form\Field\HasMany::class,
1255
            'hidden'         => \Encore\Admin\Form\Field\Hidden::class,
1256
            'id'             => \Encore\Admin\Form\Field\Id::class,
1257
            'image'          => \Encore\Admin\Form\Field\Image::class,
1258
            'ip'             => \Encore\Admin\Form\Field\Ip::class,
1259
            'map'            => \Encore\Admin\Form\Field\Map::class,
1260
            'mobile'         => \Encore\Admin\Form\Field\Mobile::class,
1261
            'month'          => \Encore\Admin\Form\Field\Month::class,
1262
            'multipleSelect' => \Encore\Admin\Form\Field\MultipleSelect::class,
1263
            'number'         => \Encore\Admin\Form\Field\Number::class,
1264
            'password'       => \Encore\Admin\Form\Field\Password::class,
1265
            'radio'          => \Encore\Admin\Form\Field\Radio::class,
1266
            'rate'           => \Encore\Admin\Form\Field\Rate::class,
1267
            'select'         => \Encore\Admin\Form\Field\Select::class,
1268
            'slider'         => \Encore\Admin\Form\Field\Slider::class,
1269
            'switch'         => \Encore\Admin\Form\Field\SwitchField::class,
1270
            'text'           => \Encore\Admin\Form\Field\Text::class,
1271
            'textarea'       => \Encore\Admin\Form\Field\Textarea::class,
1272
            'time'           => \Encore\Admin\Form\Field\Time::class,
1273
            'timeRange'      => \Encore\Admin\Form\Field\TimeRange::class,
1274
            'url'            => \Encore\Admin\Form\Field\Url::class,
1275
            'year'           => \Encore\Admin\Form\Field\Year::class,
1276
            'html'           => \Encore\Admin\Form\Field\Html::class,
1277
            'tags'           => \Encore\Admin\Form\Field\Tags::class,
1278
            'icon'           => \Encore\Admin\Form\Field\Icon::class,
1279
            'multipleFile'   => \Encore\Admin\Form\Field\MultipleFile::class,
1280
            'multipleImage'  => \Encore\Admin\Form\Field\MultipleImage::class,
1281
            'captcha'        => \Encore\Admin\Form\Field\Captcha::class,
1282
            'listbox'        => \Encore\Admin\Form\Field\Listbox::class,
1283
        ];
1284
1285
        foreach ($map as $abstract => $class) {
1286
            static::extend($abstract, $class);
1287
        }
1288
    }
1289
1290
    /**
1291
     * Register custom field.
1292
     *
1293
     * @param string $abstract
1294
     * @param string $class
1295
     *
1296
     * @return void
1297
     */
1298
    public static function extend($abstract, $class)
1299
    {
1300
        static::$availableFields[$abstract] = $class;
1301
    }
1302
1303
    /**
1304
     * Remove registered field.
1305
     *
1306
     * @param array|string $abstract
1307
     */
1308
    public static function forget($abstract)
1309
    {
1310
        array_forget(static::$availableFields, $abstract);
1311
    }
1312
1313
    /**
1314
     * Find field class.
1315
     *
1316
     * @param string $method
1317
     *
1318
     * @return bool|mixed
1319
     */
1320 View Code Duplication
    public static function findFieldClass($method)
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...
1321
    {
1322
        $class = array_get(static::$availableFields, $method);
1323
1324
        if (class_exists($class)) {
1325
            return $class;
1326
        }
1327
1328
        return false;
1329
    }
1330
1331
    /**
1332
     * Collect assets required by registered field.
1333
     *
1334
     * @return array
1335
     */
1336
    public static function collectFieldAssets()
1337
    {
1338
        if (!empty(static::$collectedAssets)) {
1339
            return static::$collectedAssets;
1340
        }
1341
1342
        $css = collect();
1343
        $js = collect();
1344
1345
        foreach (static::$availableFields as $field) {
1346
            if (!method_exists($field, 'getAssets')) {
1347
                continue;
1348
            }
1349
1350
            $assets = call_user_func([$field, 'getAssets']);
1351
1352
            $css->push(array_get($assets, 'css'));
1353
            $js->push(array_get($assets, 'js'));
1354
        }
1355
1356
        return static::$collectedAssets = [
1357
            'css' => $css->flatten()->unique()->filter()->toArray(),
1358
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1359
        ];
1360
    }
1361
1362
    /**
1363
     * Getter.
1364
     *
1365
     * @param string $name
1366
     *
1367
     * @return array|mixed
1368
     */
1369
    public function __get($name)
1370
    {
1371
        return $this->input($name);
1372
    }
1373
1374
    /**
1375
     * Setter.
1376
     *
1377
     * @param string $name
1378
     * @param $value
1379
     */
1380
    public function __set($name, $value)
1381
    {
1382
        $this->input($name, $value);
1383
    }
1384
1385
    /**
1386
     * Generate a Field object and add to form builder if Field exists.
1387
     *
1388
     * @param string $method
1389
     * @param array  $arguments
1390
     *
1391
     * @return Field|void
1392
     */
1393
    public function __call($method, $arguments)
1394
    {
1395
        if ($className = static::findFieldClass($method)) {
1396
            $column = array_get($arguments, 0, ''); //[0];
1397
1398
            $element = new $className($column, array_slice($arguments, 1));
1399
1400
            $this->pushField($element);
1401
1402
            return $element;
1403
        }
1404
    }
1405
1406
    /**
1407
     * Render the contents of the form when casting to string.
1408
     *
1409
     * @return string
1410
     */
1411
    public function __toString()
1412
    {
1413
        return $this->render();
1414
    }
1415
}
1416