Completed
Pull Request — master (#1350)
by
unknown
03:01
created

Form::prepareUpdate()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 6
nop 2
dl 0
loc 31
rs 6.7272
c 0
b 0
f 0
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 \App\Admin\Extensions\Form\TextareaEmoji  TextareaEmoji($column, $label = '')
34
 * @method Field\Hidden         hidden($column, $label = '')
35
 * @method Field\Id             id($column, $label = '')
36
 * @method Field\Ip             ip($column, $label = '')
37
 * @method Field\Url            url($column, $label = '')
38
 * @method Field\Color          color($column, $label = '')
39
 * @method Field\Email          email($column, $label = '')
40
 * @method Field\Mobile         mobile($column, $label = '')
41
 * @method Field\Slider         slider($column, $label = '')
42
 * @method Field\Map            map($latitude, $longitude, $label = '')
43
 * @method \App\Admin\Extensions\Form\GeoCompleteMap  geocompletemap($latitude, $longitude, $label = '')
44
 * @method \App\Admin\Extensions\Form\multiSelectTag  multiselect_tags($column, $label = '')
45
* @method \App\Admin\Extensions\Form\InstagramAddSelect2  instagram_add_select2($column, $label = '',$ajax_url,$type)
46
 * @method Field\Editor         editor($column, $label = '')
47
 * @method Field\File           file($column, $label = '')
48
 * @method Field\Image          image($column, $label = '')
49
 * @method Field\Date           date($column, $label = '')
50
 * @method Field\Datetime       datetime($column, $label = '')
51
 * @method Field\Time           time($column, $label = '')
52
 * @method Field\Year           year($column, $label = '')
53
 * @method Field\Month          month($column, $label = '')
54
 * @method Field\DateRange      dateRange($start, $end, $label = '')
55
 * @method Field\DateTimeRange  datetimeRange($start, $end, $label = '')
56
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
57
 * @method Field\Number         number($column, $label = '')
58
 * @method Field\Currency       currency($column, $label = '')
59
 * @method Field\HasMany        hasMany($relationName, $callback)
60
 * @method Field\SwitchField    switch($column, $label = '')
61
 * @method Field\Display        display($column, $label = '')
62
 * @method Field\Rate           rate($column, $label = '')
63
 * @method Field\Divide         divider()
64
 * @method Field\Password       password($column, $label = '')
65
 * @method Field\Decimal        decimal($column, $label = '')
66
 * @method Field\Html           html($html, $label = '')
67
 * @method Field\Tags           tags($column, $label = '')
68
 * @method Field\Icon           icon($column, $label = '')
69
 * @method Field\Embeds         embeds($column, $label = '')
70
 * @method Field\MultipleImage  multipleImage($column, $label = '')
71
 * @method Field\MultipleFile   multipleFile($column, $label = '')
72
 * @method Field\Captcha        captcha($column, $label = '')
73
 * @method Field\Listbox        listbox($column, $label = '')
74
 */
75
class Form
76
{
77
    /**
78
     * Eloquent model of the form.
79
     *
80
     * @var Model
81
     */
82
    protected $model;
83
84
    /**
85
     * @var \Illuminate\Validation\Validator
86
     */
87
    protected $validator;
88
89
    /**
90
     * @var Builder
91
     */
92
    protected $builder;
93
94
    /**
95
     * Submitted callback.
96
     *
97
     * @var Closure
98
     */
99
    protected $submitted;
100
101
    /**
102
     * Saving callback.
103
     *
104
     * @var Closure
105
     */
106
    protected $saving;
107
108
    /**
109
     * Saved callback.
110
     *
111
     * @var Closure
112
     */
113
    protected $saved;
114
115
    /**
116
     * Data for save to current model from input.
117
     *
118
     * @var array
119
     */
120
    protected $updates = [];
121
122
    /**
123
     * Data for save to model's relations from input.
124
     *
125
     * @var array
126
     */
127
    protected $relations = [];
128
129
    /**
130
     * Input data.
131
     *
132
     * @var array
133
     */
134
    protected $inputs = [];
135
136
    /**
137
     * Available fields.
138
     *
139
     * @var array
140
     */
141
    public static $availableFields = [];
142
143
    /**
144
     * Ignored saving fields.
145
     *
146
     * @var array
147
     */
148
    protected $ignored = [];
149
150
    /**
151
     * Collected field assets.
152
     *
153
     * @var array
154
     */
155
    protected static $collectedAssets = [];
156
157
    /**
158
     * @var Form\Tab
159
     */
160
    protected $tab = null;
161
162
    /**
163
     * Remove flag in `has many` form.
164
     */
165
    const REMOVE_FLAG_NAME = '_remove_';
166
167
    /**
168
     * Field rows in form.
169
     *
170
     * @var array
171
     */
172
    public $rows = [];
173
174
    /**
175
     * Create a new form instance.
176
     *
177
     * @param $model
178
     * @param \Closure $callback
179
     */
180
    public function __construct($model, Closure $callback)
181
    {
182
        $this->model = $model;
183
184
        $this->builder = new Builder($this);
185
186
        $callback($this);
187
    }
188
189
    /**
190
     * @param Field $field
191
     *
192
     * @return $this
193
     */
194
    public function pushField(Field $field)
195
    {
196
        $field->setForm($this);
197
198
        $this->builder->fields()->push($field);
199
200
        return $this;
201
    }
202
203
    /**
204
     * @return Model
205
     */
206
    public function model()
207
    {
208
        return $this->model;
209
    }
210
211
    /**
212
     * @return Builder
213
     */
214
    public function builder()
215
    {
216
        return $this->builder;
217
    }
218
219
    /**
220
     * Generate a edit form.
221
     *
222
     * @param $id
223
     *
224
     * @return $this
225
     */
226
    public function edit($id)
227
    {
228
        $this->builder->setMode(Builder::MODE_EDIT);
229
        $this->builder->setResourceId($id);
230
231
        $this->setFieldValue($id);
232
233
        return $this;
234
    }
235
236
    /**
237
     * @param $id
238
     *
239
     * @return $this
240
     */
241
    public function view($id)
242
    {
243
        $this->builder->setMode(Builder::MODE_VIEW);
244
        $this->builder->setResourceId($id);
245
246
        $this->setFieldValue($id);
247
248
        return $this;
249
    }
250
251
    /**
252
     * Use tab to split form.
253
     *
254
     * @param string  $title
255
     * @param Closure $content
256
     *
257
     * @return $this
258
     */
259
    public function tab($title, Closure $content, $active = false)
260
    {
261
        $this->getTab()->append($title, $content, $active);
262
263
        return $this;
264
    }
265
266
    /**
267
     * Get Tab instance.
268
     *
269
     * @return Tab
270
     */
271
    public function getTab()
272
    {
273
        if (is_null($this->tab)) {
274
            $this->tab = new Tab($this);
275
        }
276
277
        return $this->tab;
278
    }
279
280
    /**
281
     * Destroy data entity and remove files.
282
     *
283
     * @param $id
284
     *
285
     * @return mixed
286
     */
287
    public function destroy($id)
288
    {
289
        $ids = explode(',', $id);
290
291
        foreach ($ids as $id) {
292
            if (empty($id)) {
293
                continue;
294
            }
295
            $this->deleteFilesAndImages($id);
296
            $this->model->find($id)->delete();
297
        }
298
299
        return true;
300
    }
301
302
    /**
303
     * Remove files or images in record.
304
     *
305
     * @param $id
306
     */
307
    protected function deleteFilesAndImages($id)
308
    {
309
        $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...
310
            ->findOrFail($id)->toArray();
311
312
        $this->builder->fields()->filter(function ($field) {
313
            return $field instanceof Field\File;
314
        })->each(function (File $file) use ($data) {
315
            $file->setOriginal($data);
316
317
            $file->destroy();
318
        });
319
    }
320
321
    /**
322
     * Store a new record.
323
     *
324
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
325
     */
326
    public function store()
327
    {
328
        $data = Input::all();
329
330
        // Handle validation errors.
331
        if ($validationMessages = $this->validationMessages($data)) {
332
            return back()->withInput()->withErrors($validationMessages);
333
        }
334
335
        if (($response = $this->prepare($data)) instanceof Response) {
336
            return $response;
337
        }
338
339 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...
340
            $inserts = $this->prepareInsert($this->updates);
341
342
            foreach ($inserts as $column => $value) {
343
                $this->model->setAttribute($column, $value);
344
            }
345
346
            $this->model->save();
347
348
            $this->updateRelation($this->relations);
349
        });
350
351
        if (($response = $this->complete($this->saved)) instanceof Response) {
352
            return $response;
353
        }
354
355
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
356
            return $response;
357
        }
358
359
        return $this->redirectAfterStore();
360
    }
361
362
    /**
363
     * Get RedirectResponse after store.
364
     *
365
     * @return \Illuminate\Http\RedirectResponse
366
     */
367 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...
368
    {
369
        admin_toastr(trans('admin.save_succeeded'));
370
371
        $url = Input::get(Builder::PREVIOUS_URL_KEY) ?: $this->resource(0);
372
373
        return redirect($url);
374
    }
375
376
    /**
377
     * Get ajax response.
378
     *
379
     * @param string $message
380
     *
381
     * @return bool|\Illuminate\Http\JsonResponse
382
     */
383
    protected function ajaxResponse($message)
384
    {
385
        $request = Request::capture();
386
387
        // ajax but not pjax
388
        if ($request->ajax() && !$request->pjax()) {
389
            return response()->json([
390
                'status'  => true,
391
                'message' => $message,
392
            ]);
393
        }
394
395
        return false;
396
    }
397
398
    /**
399
     * Prepare input data for insert or update.
400
     *
401
     * @param array $data
402
     *
403
     * @return mixed
404
     */
405
    protected function prepare($data = [])
406
    {
407
        if (($response = $this->callSubmitted()) instanceof Response) {
408
            return $response;
409
        }
410
411
        $this->inputs = $this->removeIgnoredFields($data);
412
413
        if (($response = $this->callSaving()) instanceof Response) {
414
            return $response;
415
        }
416
417
        $this->relations = $this->getRelationInputs($this->inputs);
418
419
        $this->updates = array_except($this->inputs, array_keys($this->relations));
420
    }
421
422
    /**
423
     * Remove ignored fields from input.
424
     *
425
     * @param array $input
426
     *
427
     * @return array
428
     */
429
    protected function removeIgnoredFields($input)
430
    {
431
        array_forget($input, $this->ignored);
432
433
        return $input;
434
    }
435
436
    /**
437
     * Get inputs for relations.
438
     *
439
     * @param array $inputs
440
     *
441
     * @return array
442
     */
443
    protected function getRelationInputs($inputs = [])
444
    {
445
        $relations = [];
446
447
        foreach ($inputs as $column => $value) {
448
            if (method_exists($this->model, $column)) {
449
                $relation = call_user_func([$this->model, $column]);
450
451
                if ($relation instanceof Relation) {
452
                    $relations[$column] = $value;
453
                }
454
            }
455
        }
456
457
        return $relations;
458
    }
459
460
    /**
461
     * Call submitted callback.
462
     *
463
     * @return mixed
464
     */
465
    protected function callSubmitted()
466
    {
467
        if ($this->submitted instanceof Closure) {
468
            return call_user_func($this->submitted, $this);
469
        }
470
    }
471
472
    /**
473
     * Call saving callback.
474
     *
475
     * @return mixed
476
     */
477
    protected function callSaving()
478
    {
479
        if ($this->saving instanceof Closure) {
480
            return call_user_func($this->saving, $this);
481
        }
482
    }
483
484
    /**
485
     * Callback after saving a Model.
486
     *
487
     * @param Closure|null $callback
488
     *
489
     * @return mixed|null
490
     */
491
    protected function complete(Closure $callback = null)
492
    {
493
        if ($callback instanceof Closure) {
494
            return $callback($this);
495
        }
496
    }
497
498
    /**
499
     * Handle update.
500
     *
501
     * @param int $id
502
     *
503
     * @return \Symfony\Component\HttpFoundation\Response
504
     */
505
    public function update($id)
506
    {
507
        $data = Input::all();
508
509
        $isEditable = $this->isEditable($data);
510
511
        $data = $this->handleEditable($data);
512
513
        $data = $this->handleFileDelete($data);
514
515
        if ($this->handleOrderable($id, $data)) {
516
            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...
517
                'status'  => true,
518
                'message' => trans('admin.update_succeeded'),
519
            ]);
520
        }
521
522
        /* @var Model $this->model */
523
        $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...
524
525
        $this->setFieldOriginalValue();
526
527
        // Handle validation errors.
528
        if ($validationMessages = $this->validationMessages($data)) {
529
            if (!$isEditable) {
530
                return back()->withInput()->withErrors($validationMessages);
531
            } else {
532
                return response()->json(['errors' => array_dot($validationMessages->getMessages())], 422);
533
            }
534
        }
535
536
        if (($response = $this->prepare($data)) instanceof Response) {
537
            return $response;
538
        }
539
540 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...
541
            $updates = $this->prepareUpdate($this->updates);
542
543
            foreach ($updates as $column => $value) {
544
                /* @var Model $this->model */
545
                $this->model->setAttribute($column, $value);
546
            }
547
548
            $this->model->save();
549
550
            $this->updateRelation($this->relations);
551
        });
552
553
        if (($result = $this->complete($this->saved)) instanceof Response) {
554
            return $result;
555
        }
556
557
        if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) {
558
            return $response;
559
        }
560
561
        return $this->redirectAfterUpdate();
562
    }
563
564
    /**
565
     * Get RedirectResponse after update.
566
     *
567
     * @return \Illuminate\Http\RedirectResponse
568
     */
569 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...
570
    {
571
        admin_toastr(trans('admin.update_succeeded'));
572
573
        $url = Input::get(Builder::PREVIOUS_URL_KEY) ?: $this->resource(-1);
574
575
        return redirect($url);
576
    }
577
578
    /**
579
     * Check if request is from editable.
580
     *
581
     * @param array $input
582
     *
583
     * @return bool
584
     */
585
    protected function isEditable(array $input = [])
586
    {
587
        return array_key_exists('_editable', $input);
588
    }
589
590
    /**
591
     * Handle editable update.
592
     *
593
     * @param array $input
594
     *
595
     * @return array
596
     */
597
    protected function handleEditable(array $input = [])
598
    {
599
        if (array_key_exists('_editable', $input)) {
600
            $name = $input['name'];
601
            $value = $input['value'];
602
603
            array_forget($input, ['pk', 'value', 'name']);
604
            array_set($input, $name, $value);
605
        }
606
607
        return $input;
608
    }
609
610
    /**
611
     * @param array $input
612
     *
613
     * @return array
614
     */
615
    protected function handleFileDelete(array $input = [])
616
    {
617
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
618
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
619
            unset($input['key']);
620
        }
621
622
        Input::replace($input);
623
624
        return $input;
625
    }
626
627
    /**
628
     * Handle orderable update.
629
     *
630
     * @param int   $id
631
     * @param array $input
632
     *
633
     * @return bool
634
     */
635
    protected function handleOrderable($id, array $input = [])
636
    {
637
        if (array_key_exists('_orderable', $input)) {
638
            $model = $this->model->find($id);
639
640
            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...
641
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
642
643
                return true;
644
            }
645
        }
646
647
        return false;
648
    }
649
650
    /**
651
     * Update relation data.
652
     *
653
     * @param array $relationsData
654
     *
655
     * @return void
656
     */
657
    protected function updateRelation($relationsData)
658
    {
659
        foreach ($relationsData as $name => $values) {
660
            if (!method_exists($this->model, $name)) {
661
                continue;
662
            }
663
664
            $relation = $this->model->$name();
665
666
            $oneToOneRelation = $relation instanceof \Illuminate\Database\Eloquent\Relations\HasOne
667
                || $relation instanceof \Illuminate\Database\Eloquent\Relations\MorphOne;
668
669
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
670
671
            if (empty($prepared)) {
672
                continue;
673
            }
674
675
            switch (get_class($relation)) {
676
                case \Illuminate\Database\Eloquent\Relations\BelongsToMany::class:
677
                case \Illuminate\Database\Eloquent\Relations\MorphToMany::class:
678
                    if (isset($prepared[$name])) {
679
                        $relation->sync($prepared[$name]);
680
                    }
681
                    break;
682
                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...
683
684
                    $related = $this->model->$name;
685
686
                    // if related is empty
687
                    if (is_null($related)) {
688
                        $related = $relation->getRelated();
689
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
690
                    }
691
692
                    foreach ($prepared[$name] as $column => $value) {
693
                        $related->setAttribute($column, $value);
694
                    }
695
696
                    $related->save();
697
                    break;
698
                case \Illuminate\Database\Eloquent\Relations\MorphOne::class:
699
                    $related = $this->model->$name;
700
                    if (is_null($related)) {
701
                        $related = $relation->make();
702
                    }
703
                    foreach ($prepared[$name] as $column => $value) {
704
                        $related->setAttribute($column, $value);
705
                    }
706
                    $related->save();
707
                    break;
708
                case \Illuminate\Database\Eloquent\Relations\HasMany::class:
709
                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...
710
711
                    foreach ($prepared[$name] as $related) {
712
                        $relation = $this->model()->$name();
713
714
                        $keyName = $relation->getRelated()->getKeyName();
715
716
                        $instance = $relation->findOrNew(array_get($related, $keyName));
717
718
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
719
                            $instance->delete();
720
721
                            continue;
722
                        }
723
724
                        array_forget($related, static::REMOVE_FLAG_NAME);
725
726
                        $instance->fill($related);
727
728
                        $instance->save();
729
                    }
730
731
                    break;
732
            }
733
        }
734
    }
735
736
    /**
737
     * Prepare input data for update.
738
     *
739
     * @param array $updates
740
     * @param bool  $oneToOneRelation If column is one-to-one relation.
741
     *
742
     * @return array
743
     */
744
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
745
    {
746
        $prepared = [];
747
748
        foreach ($this->builder->fields() as $field) {
749
            $columns = $field->column();
750
751
            // If column not in input array data, then continue.
752
            if (!array_has($updates, $columns)) {
753
                continue;
754
            }
755
756
            if ($this->invalidColumn($columns, $oneToOneRelation)) {
757
                continue;
758
            }
759
760
            $value = $this->getDataByColumn($updates, $columns);
761
762
            $value = $field->prepare($value);
763
764
            if (is_array($columns)) {
765
                foreach ($columns as $name => $column) {
766
                    array_set($prepared, $column, $value[$name]);
767
                }
768
            } elseif (is_string($columns)) {
769
                array_set($prepared, $columns, $value);
770
            }
771
        }
772
773
        return $prepared;
774
    }
775
776
    /**
777
     * @param string|array $columns
778
     * @param bool         $oneToOneRelation
779
     *
780
     * @return bool
781
     */
782
    protected function invalidColumn($columns, $oneToOneRelation = false)
783
    {
784
        foreach ((array) $columns as $column) {
785
            if ((!$oneToOneRelation && Str::contains($column, '.')) ||
786
                ($oneToOneRelation && !Str::contains($column, '.'))) {
787
                return true;
788
            }
789
        }
790
791
        return false;
792
    }
793
794
    /**
795
     * Prepare input data for insert.
796
     *
797
     * @param $inserts
798
     *
799
     * @return array
800
     */
801
    protected function prepareInsert($inserts)
802
    {
803
        if ($this->isHasOneRelation($inserts)) {
804
            $inserts = array_dot($inserts);
805
        }
806
807
        foreach ($inserts as $column => $value) {
808
            if (is_null($field = $this->getFieldByColumn($column))) {
809
                unset($inserts[$column]);
810
                continue;
811
            }
812
813
            $inserts[$column] = $field->prepare($value);
814
        }
815
816
        $prepared = [];
817
818
        foreach ($inserts as $key => $value) {
819
            array_set($prepared, $key, $value);
820
        }
821
822
        return $prepared;
823
    }
824
825
    /**
826
     * Is input data is has-one relation.
827
     *
828
     * @param array $inserts
829
     *
830
     * @return bool
831
     */
832
    protected function isHasOneRelation($inserts)
833
    {
834
        $first = current($inserts);
835
836
        if (!is_array($first)) {
837
            return false;
838
        }
839
840
        if (is_array(current($first))) {
841
            return false;
842
        }
843
844
        return Arr::isAssoc($first);
845
    }
846
847
    /**
848
     * Set submitted callback.
849
     *
850
     * @param Closure $callback
851
     *
852
     * @return void
853
     */
854
    public function submitted(Closure $callback)
855
    {
856
        $this->submitted = $callback;
857
    }
858
859
    /**
860
     * Set saving callback.
861
     *
862
     * @param Closure $callback
863
     *
864
     * @return void
865
     */
866
    public function saving(Closure $callback)
867
    {
868
        $this->saving = $callback;
869
    }
870
871
    /**
872
     * Set saved callback.
873
     *
874
     * @param callable $callback
875
     *
876
     * @return void
877
     */
878
    public function saved(Closure $callback)
879
    {
880
        $this->saved = $callback;
881
    }
882
883
    /**
884
     * Ignore fields to save.
885
     *
886
     * @param string|array $fields
887
     *
888
     * @return $this
889
     */
890
    public function ignore($fields)
891
    {
892
        $this->ignored = array_merge($this->ignored, (array) $fields);
893
894
        return $this;
895
    }
896
897
    /**
898
     * @param array        $data
899
     * @param string|array $columns
900
     *
901
     * @return array|mixed
902
     */
903 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...
904
    {
905
        if (is_string($columns)) {
906
            return array_get($data, $columns);
907
        }
908
909
        if (is_array($columns)) {
910
            $value = [];
911
            foreach ($columns as $name => $column) {
912
                if (!array_has($data, $column)) {
913
                    continue;
914
                }
915
                $value[$name] = array_get($data, $column);
916
            }
917
918
            return $value;
919
        }
920
    }
921
922
    /**
923
     * Find field object by column.
924
     *
925
     * @param $column
926
     *
927
     * @return mixed
928
     */
929
    protected function getFieldByColumn($column)
930
    {
931
        return $this->builder->fields()->first(
932
            function (Field $field) use ($column) {
933
                if (is_array($field->column())) {
934
                    return in_array($column, $field->column());
935
                }
936
937
                return $field->column() == $column;
938
            }
939
        );
940
    }
941
942
    /**
943
     * Set original data for each field.
944
     *
945
     * @return void
946
     */
947
    protected function setFieldOriginalValue()
948
    {
949
//        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...
950
951
        $values = $this->model->toArray();
952
953
        $this->builder->fields()->each(function (Field $field) use ($values) {
954
            $field->setOriginal($values);
955
        });
956
    }
957
958
    /**
959
     * Set all fields value in form.
960
     *
961
     * @param $id
962
     *
963
     * @return void
964
     */
965
    protected function setFieldValue($id)
966
    {
967
        $relations = $this->getRelations();
968
969
        $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...
970
971
//        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...
972
973
        $data = $this->model->toArray();
974
975
        $this->builder->fields()->each(function (Field $field) use ($data) {
976
            $field->fill($data);
977
        });
978
    }
979
980
    /**
981
     * Don't snake case attributes.
982
     *
983
     * @param Model $model
984
     *
985
     * @return void
986
     */
987
    protected static function doNotSnakeAttributes(Model $model)
988
    {
989
        $class = get_class($model);
990
991
        $class::$snakeAttributes = false;
992
    }
993
994
    /**
995
     * Get validation messages.
996
     *
997
     * @param array $input
998
     *
999
     * @return MessageBag|bool
1000
     */
1001
    protected function validationMessages($input)
1002
    {
1003
        $failedValidators = [];
1004
1005
        foreach ($this->builder->fields() as $field) {
1006
            if (!$validator = $field->getValidator($input)) {
1007
                continue;
1008
            }
1009
1010
            if (($validator instanceof Validator) && !$validator->passes()) {
1011
                $failedValidators[] = $validator;
1012
            }
1013
        }
1014
1015
        $message = $this->mergeValidationMessages($failedValidators);
1016
1017
        return $message->any() ? $message : false;
1018
    }
1019
1020
    /**
1021
     * Merge validation messages from input validators.
1022
     *
1023
     * @param \Illuminate\Validation\Validator[] $validators
1024
     *
1025
     * @return MessageBag
1026
     */
1027
    protected function mergeValidationMessages($validators)
1028
    {
1029
        $messageBag = new MessageBag();
1030
1031
        foreach ($validators as $validator) {
1032
            $messageBag = $messageBag->merge($validator->messages());
1033
        }
1034
1035
        return $messageBag;
1036
    }
1037
1038
    /**
1039
     * Get all relations of model from callable.
1040
     *
1041
     * @return array
1042
     */
1043
    public function getRelations()
1044
    {
1045
        $relations = $columns = [];
1046
1047
        foreach ($this->builder->fields() as $field) {
1048
            $columns[] = $field->column();
1049
        }
1050
1051
        foreach (array_flatten($columns) as $column) {
1052
            if (str_contains($column, '.')) {
1053
                list($relation) = explode('.', $column);
1054
1055
                if (method_exists($this->model, $relation) &&
1056
                    $this->model->$relation() instanceof Relation
1057
                ) {
1058
                    $relations[] = $relation;
1059
                }
1060
            } elseif (method_exists($this->model, $column) &&
1061
                !method_exists(Model::class, $column)
1062
            ) {
1063
                $relations[] = $column;
1064
            }
1065
        }
1066
1067
        return array_unique($relations);
1068
    }
1069
1070
    /**
1071
     * Set action for form.
1072
     *
1073
     * @param string $action
1074
     *
1075
     * @return $this
1076
     */
1077
    public function setAction($action)
1078
    {
1079
        $this->builder()->setAction($action);
1080
1081
        return $this;
1082
    }
1083
1084
    /**
1085
     * Set field and label width in current form.
1086
     *
1087
     * @param int $fieldWidth
1088
     * @param int $labelWidth
1089
     *
1090
     * @return $this
1091
     */
1092
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1093
    {
1094
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1095
            /* @var Field $field  */
1096
            $field->setWidth($fieldWidth, $labelWidth);
1097
        });
1098
1099
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1100
1101
        return $this;
1102
    }
1103
1104
    /**
1105
     * Set view for form.
1106
     *
1107
     * @param string $view
1108
     *
1109
     * @return $this
1110
     */
1111
    public function setView($view)
1112
    {
1113
        $this->builder()->setView($view);
1114
1115
        return $this;
1116
    }
1117
1118
    /**
1119
     * Add a row in form.
1120
     *
1121
     * @param Closure $callback
1122
     *
1123
     * @return $this
1124
     */
1125
    public function row(Closure $callback)
1126
    {
1127
        $this->rows[] = new Row($callback, $this);
1128
1129
        return $this;
1130
    }
1131
1132
    /**
1133
     * Tools setting for form.
1134
     *
1135
     * @param Closure $callback
1136
     */
1137
    public function tools(Closure $callback)
1138
    {
1139
        $callback->call($this, $this->builder->getTools());
1140
    }
1141
1142
    /**
1143
     * Disable form submit.
1144
     *
1145
     * @return $this
1146
     */
1147
    public function disableSubmit()
1148
    {
1149
        $this->builder()->options(['enableSubmit' => false]);
1150
1151
        return $this;
1152
    }
1153
1154
    /**
1155
     * Disable form reset.
1156
     *
1157
     * @return $this
1158
     */
1159
    public function disableReset()
1160
    {
1161
        $this->builder()->options(['enableReset' => false]);
1162
1163
        return $this;
1164
    }
1165
1166
    /**
1167
     * Get current resource route url.
1168
     *
1169
     * @param int $slice
1170
     *
1171
     * @return string
1172
     */
1173
    public function resource($slice = -2)
1174
    {
1175
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1176
1177
        if ($slice != 0) {
1178
            $segments = array_slice($segments, 0, $slice);
1179
        }
1180
1181
        return implode('/', $segments);
1182
    }
1183
1184
    /**
1185
     * Render the form contents.
1186
     *
1187
     * @return string
1188
     */
1189
    public function render()
1190
    {
1191
        try {
1192
            return $this->builder->render();
1193
        } catch (\Exception $e) {
1194
            return Handler::renderException($e);
1195
        }
1196
    }
1197
1198
    /**
1199
     * Get or set input data.
1200
     *
1201
     * @param string $key
1202
     * @param null   $value
1203
     *
1204
     * @return array|mixed
1205
     */
1206
    public function input($key, $value = null)
1207
    {
1208
        if (is_null($value)) {
1209
            return array_get($this->inputs, $key);
1210
        }
1211
1212
        return array_set($this->inputs, $key, $value);
1213
    }
1214
1215
    /**
1216
     * Register builtin fields.
1217
     *
1218
     * @return void
1219
     */
1220
    public static function registerBuiltinFields()
1221
    {
1222
        $map = [
1223
            'button'         => \Encore\Admin\Form\Field\Button::class,
1224
            'checkbox'       => \Encore\Admin\Form\Field\Checkbox::class,
1225
            'color'          => \Encore\Admin\Form\Field\Color::class,
1226
            'currency'       => \Encore\Admin\Form\Field\Currency::class,
1227
            'date'           => \Encore\Admin\Form\Field\Date::class,
1228
            'dateRange'      => \Encore\Admin\Form\Field\DateRange::class,
1229
            'datetime'       => \Encore\Admin\Form\Field\Datetime::class,
1230
            'dateTimeRange'  => \Encore\Admin\Form\Field\DatetimeRange::class,
1231
            'datetimeRange'  => \Encore\Admin\Form\Field\DatetimeRange::class,
1232
            'decimal'        => \Encore\Admin\Form\Field\Decimal::class,
1233
            'display'        => \Encore\Admin\Form\Field\Display::class,
1234
            'divider'        => \Encore\Admin\Form\Field\Divide::class,
1235
            'divide'         => \Encore\Admin\Form\Field\Divide::class,
1236
            'embeds'         => \Encore\Admin\Form\Field\Embeds::class,
1237
            'editor'         => \Encore\Admin\Form\Field\Editor::class,
1238
            'email'          => \Encore\Admin\Form\Field\Email::class,
1239
            'file'           => \Encore\Admin\Form\Field\File::class,
1240
            'hasMany'        => \Encore\Admin\Form\Field\HasMany::class,
1241
            'hidden'         => \Encore\Admin\Form\Field\Hidden::class,
1242
            'id'             => \Encore\Admin\Form\Field\Id::class,
1243
            'image'          => \Encore\Admin\Form\Field\Image::class,
1244
            'ip'             => \Encore\Admin\Form\Field\Ip::class,
1245
            'map'            => \Encore\Admin\Form\Field\Map::class,
1246
            'mobile'         => \Encore\Admin\Form\Field\Mobile::class,
1247
            'month'          => \Encore\Admin\Form\Field\Month::class,
1248
            'multipleSelect' => \Encore\Admin\Form\Field\MultipleSelect::class,
1249
            'number'         => \Encore\Admin\Form\Field\Number::class,
1250
            'password'       => \Encore\Admin\Form\Field\Password::class,
1251
            'radio'          => \Encore\Admin\Form\Field\Radio::class,
1252
            'rate'           => \Encore\Admin\Form\Field\Rate::class,
1253
            'select'         => \Encore\Admin\Form\Field\Select::class,
1254
            'slider'         => \Encore\Admin\Form\Field\Slider::class,
1255
            'switch'         => \Encore\Admin\Form\Field\SwitchField::class,
1256
            'text'           => \Encore\Admin\Form\Field\Text::class,
1257
            'textarea'       => \Encore\Admin\Form\Field\Textarea::class,
1258
            'time'           => \Encore\Admin\Form\Field\Time::class,
1259
            'timeRange'      => \Encore\Admin\Form\Field\TimeRange::class,
1260
            'url'            => \Encore\Admin\Form\Field\Url::class,
1261
            'year'           => \Encore\Admin\Form\Field\Year::class,
1262
            'html'           => \Encore\Admin\Form\Field\Html::class,
1263
            'tags'           => \Encore\Admin\Form\Field\Tags::class,
1264
            'icon'           => \Encore\Admin\Form\Field\Icon::class,
1265
            'multipleFile'   => \Encore\Admin\Form\Field\MultipleFile::class,
1266
            'multipleImage'  => \Encore\Admin\Form\Field\MultipleImage::class,
1267
            'captcha'        => \Encore\Admin\Form\Field\Captcha::class,
1268
            'listbox'        => \Encore\Admin\Form\Field\Listbox::class,
1269
        ];
1270
1271
        foreach ($map as $abstract => $class) {
1272
            static::extend($abstract, $class);
1273
        }
1274
    }
1275
1276
    /**
1277
     * Register custom field.
1278
     *
1279
     * @param string $abstract
1280
     * @param string $class
1281
     *
1282
     * @return void
1283
     */
1284
    public static function extend($abstract, $class)
1285
    {
1286
        static::$availableFields[$abstract] = $class;
1287
    }
1288
1289
    /**
1290
     * Remove registered field.
1291
     *
1292
     * @param array|string $abstract
1293
     */
1294
    public static function forget($abstract)
1295
    {
1296
        array_forget(static::$availableFields, $abstract);
1297
    }
1298
1299
    /**
1300
     * Find field class.
1301
     *
1302
     * @param string $method
1303
     *
1304
     * @return bool|mixed
1305
     */
1306
    public static function findFieldClass($method)
1307
    {
1308
        $class = array_get(static::$availableFields, $method);
1309
1310
        if (class_exists($class)) {
1311
            return $class;
1312
        }
1313
1314
        return false;
1315
    }
1316
1317
    /**
1318
     * Collect assets required by registered field.
1319
     *
1320
     * @return array
1321
     */
1322
    public static function collectFieldAssets()
1323
    {
1324
        if (!empty(static::$collectedAssets)) {
1325
            return static::$collectedAssets;
1326
        }
1327
1328
        $css = collect();
1329
        $js = collect();
1330
1331
        foreach (static::$availableFields as $field) {
1332
            if (!method_exists($field, 'getAssets')) {
1333
                continue;
1334
            }
1335
1336
            $assets = call_user_func([$field, 'getAssets']);
1337
1338
            $css->push(array_get($assets, 'css'));
1339
            $js->push(array_get($assets, 'js'));
1340
        }
1341
1342
        return static::$collectedAssets = [
1343
            'css' => $css->flatten()->unique()->filter()->toArray(),
1344
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1345
        ];
1346
    }
1347
    /**
1348
     * Collect rules of all fields.
1349
     *
1350
     * @return array
1351
     */
1352
    public function getRules()
1353
    {
1354
        $this->builder()->getRules();
1355
    }
1356
1357
    /**
1358
     * Collect validation Messages of all fields.
1359
     *
1360
     * @return array
1361
     */
1362
    public function getRuleMessages()
1363
    {
1364
        $this->builder()->getRuleMessages();
1365
    }
1366
1367
    /**
1368
     * Getter.
1369
     *
1370
     * @param string $name
1371
     *
1372
     * @return array|mixed
1373
     */
1374
    public function __get($name)
1375
    {
1376
        return $this->input($name);
1377
    }
1378
1379
    /**
1380
     * Setter.
1381
     *
1382
     * @param string $name
1383
     * @param $value
1384
     */
1385
    public function __set($name, $value)
1386
    {
1387
        $this->input($name, $value);
1388
    }
1389
1390
    /**
1391
     * Generate a Field object and add to form builder if Field exists.
1392
     *
1393
     * @param string $method
1394
     * @param array  $arguments
1395
     *
1396
     * @return Field|void
1397
     */
1398 View Code Duplication
    public function __call($method, $arguments)
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...
1399
    {
1400
        if ($className = static::findFieldClass($method)) {
1401
            $column = array_get($arguments, 0, ''); //[0];
1402
1403
            $element = new $className($column, array_slice($arguments, 1));
1404
1405
            $this->pushField($element);
1406
1407
            return $element;
1408
        }
1409
    }
1410
1411
    /**
1412
     * Render the contents of the form when casting to string.
1413
     *
1414
     * @return string
1415
     */
1416
    public function __toString()
1417
    {
1418
        return $this->render();
1419
    }
1420
}
1421