Completed
Pull Request — master (#2883)
by
unknown
02:07
created

Form::store()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 33

Duplication

Lines 9
Ratio 27.27 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 0
dl 9
loc 33
rs 9.0808
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\Row;
10
use Encore\Admin\Form\Tab;
11
use Illuminate\Contracts\Support\Renderable;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Relations;
14
use Illuminate\Database\Eloquent\SoftDeletes;
15
use Illuminate\Http\Request;
16
use Illuminate\Support\Arr;
17
use Illuminate\Support\Facades\DB;
18
use Illuminate\Support\Facades\Input;
19
use Illuminate\Support\MessageBag;
20
use Illuminate\Support\Str;
21
use Illuminate\Validation\Validator;
22
use Spatie\EloquentSortable\Sortable;
23
use Symfony\Component\HttpFoundation\Response;
24
25
/**
26
 * Class Form.
27
 *
28
 * @method Field\Text           text($column, $label = '')
29
 * @method Field\Checkbox       checkbox($column, $label = '')
30
 * @method Field\Radio          radio($column, $label = '')
31
 * @method Field\Select         select($column, $label = '')
32
 * @method Field\MultipleSelect multipleSelect($column, $label = '')
33
 * @method Field\Textarea       textarea($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 Field\Editor         editor($column, $label = '')
44
 * @method Field\File           file($column, $label = '')
45
 * @method Field\Image          image($column, $label = '')
46
 * @method Field\Date           date($column, $label = '')
47
 * @method Field\Datetime       datetime($column, $label = '')
48
 * @method Field\Time           time($column, $label = '')
49
 * @method Field\Year           year($column, $label = '')
50
 * @method Field\Month          month($column, $label = '')
51
 * @method Field\DateRange      dateRange($start, $end, $label = '')
52
 * @method Field\DateTimeRange  datetimeRange($start, $end, $label = '')
53
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
54
 * @method Field\Number         number($column, $label = '')
55
 * @method Field\Currency       currency($column, $label = '')
56
 * @method Field\HasMany        hasMany($relationName, $callback)
57
 * @method Field\SwitchField    switch($column, $label = '')
58
 * @method Field\Display        display($column, $label = '')
59
 * @method Field\Rate           rate($column, $label = '')
60
 * @method Field\Divide         divider()
61
 * @method Field\Password       password($column, $label = '')
62
 * @method Field\Decimal        decimal($column, $label = '')
63
 * @method Field\Html           html($html, $label = '')
64
 * @method Field\Tags           tags($column, $label = '')
65
 * @method Field\Icon           icon($column, $label = '')
66
 * @method Field\Embeds         embeds($column, $label = '')
67
 * @method Field\MultipleImage  multipleImage($column, $label = '')
68
 * @method Field\MultipleFile   multipleFile($column, $label = '')
69
 * @method Field\Captcha        captcha($column, $label = '')
70
 * @method Field\Listbox        listbox($column, $label = '')
71
 */
72
class Form implements Renderable
73
{
74
    /**
75
     * Eloquent model of the form.
76
     *
77
     * @var Model
78
     */
79
    protected $model;
80
81
    /**
82
     * @var \Illuminate\Validation\Validator
83
     */
84
    protected $validator;
85
86
    /**
87
     * @var Builder
88
     */
89
    protected $builder;
90
91
    /**
92
     * Submitted callback.
93
     *
94
     * @var Closure[]
95
     */
96
    protected $submitted = [];
97
98
    /**
99
     * Saving callback.
100
     *
101
     * @var Closure[]
102
     */
103
    protected $saving = [];
104
105
    /**
106
     * Saved callback.
107
     *
108
     * @var Closure[]
109
     */
110
    protected $saved = [];
111
112
    /**
113
     * Callbacks after getting editing model.
114
     *
115
     * @var Closure[]
116
     */
117
    protected $editing = [];
118
119
    /**
120
     * Data for save to current model from input.
121
     *
122
     * @var array
123
     */
124
    protected $updates = [];
125
126
    /**
127
     * Data for save to model's relations from input.
128
     *
129
     * @var array
130
     */
131
    protected $relations = [];
132
133
    /**
134
     * Input data.
135
     *
136
     * @var array
137
     */
138
    protected $inputs = [];
139
140
    /**
141
     * Available fields.
142
     *
143
     * @var array
144
     */
145
    public static $availableFields = [];
146
147
    /**
148
     * Form field alias.
149
     *
150
     * @var array
151
     */
152
    public static $fieldAlias = [];
153
154
    /**
155
     * Ignored saving fields.
156
     *
157
     * @var array
158
     */
159
    protected $ignored = [];
160
161
    /**
162
     * Collected field assets.
163
     *
164
     * @var array
165
     */
166
    protected static $collectedAssets = [];
167
168
    /**
169
     * @var Form\Tab
170
     */
171
    protected $tab = null;
172
173
    /**
174
     * Remove flag in `has many` form.
175
     */
176
    const REMOVE_FLAG_NAME = '_remove_';
177
178
    /**
179
     * Field rows in form.
180
     *
181
     * @var array
182
     */
183
    public $rows = [];
184
185
    /**
186
     * @var bool
187
     */
188
    protected $isSoftDeletes = false;
189
190
    /**
191
     * Create a new form instance.
192
     *
193
     * @param $model
194
     * @param \Closure $callback
195
     */
196
    public function __construct($model, Closure $callback = null)
197
    {
198
        $this->model = $model;
199
200
        $this->builder = new Builder($this);
201
202
        if ($callback instanceof Closure) {
203
            $callback($this);
204
        }
205
206
        $this->isSoftDeletes = in_array(SoftDeletes::class, class_uses($this->model));
207
    }
208
209
    /**
210
     * @param Field $field
211
     *
212
     * @return $this
213
     */
214
    public function pushField(Field $field)
215
    {
216
        $field->setForm($this);
217
218
        $this->builder->fields()->push($field);
219
220
        return $this;
221
    }
222
223
    /**
224
     * @return Model
225
     */
226
    public function model()
227
    {
228
        return $this->model;
229
    }
230
231
    /**
232
     * @return Builder
233
     */
234
    public function builder()
235
    {
236
        return $this->builder;
237
    }
238
239
    /**
240
     * Generate a edit form.
241
     *
242
     * @param $id
243
     *
244
     * @return $this
245
     */
246
    public function edit($id)
247
    {
248
        $this->builder->setMode(Builder::MODE_EDIT);
249
        $this->builder->setResourceId($id);
250
251
        $this->setFieldValue($id);
252
253
        return $this;
254
    }
255
256
    /**
257
     * Use tab to split form.
258
     *
259
     * @param string  $title
260
     * @param Closure $content
261
     *
262
     * @return $this
263
     */
264
    public function tab($title, Closure $content, $active = false)
265
    {
266
        $this->getTab()->append($title, $content, $active);
267
268
        return $this;
269
    }
270
271
    /**
272
     * Get Tab instance.
273
     *
274
     * @return Tab
275
     */
276
    public function getTab()
277
    {
278
        if (is_null($this->tab)) {
279
            $this->tab = new Tab($this);
280
        }
281
282
        return $this->tab;
283
    }
284
285
    /**
286
     * Destroy data entity and remove files.
287
     *
288
     * @param $id
289
     *
290
     * @return mixed
291
     */
292
    public function destroy($id)
293
    {
294
        collect(explode(',', $id))->filter()->each(function ($id) {
295
            $builder = $this->model()->newQuery();
296
297
            if ($this->isSoftDeletes) {
298
                $builder = $builder->withTrashed();
299
            }
300
301
            $model = $builder->with($this->getRelations())->findOrFail($id);
302
303
            if ($this->isSoftDeletes && $model->trashed()) {
304
                $this->deleteFiles($model, true);
305
                $model->forceDelete();
306
307
                return;
308
            }
309
310
            $this->deleteFiles($model);
311
            $model->delete();
312
        });
313
314
        return true;
315
    }
316
317
    /**
318
     * Remove files in record.
319
     *
320
     * @param Model $model
321
     * @param bool  $forceDelete
322
     */
323
    protected function deleteFiles(Model $model, $forceDelete = false)
324
    {
325
        // If it's a soft delete, the files in the data will not be deleted.
326
        if (!$forceDelete && $this->isSoftDeletes) {
327
            return;
328
        }
329
330
        $data = $model->toArray();
331
332
        $this->builder->fields()->filter(function ($field) {
333
            return $field instanceof Field\File;
334
        })->each(function (Field\File $file) use ($data) {
335
            $file->setOriginal($data);
336
337
            $file->destroy();
338
        });
339
    }
340
341
    /**
342
     * Store a new record.
343
     *
344
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
345
     */
346
    public function store()
347
    {
348
        $data = Input::all();
349
350
        // Handle validation errors.
351
        if ($validationMessages = $this->validationMessages($data)) {
352
            return back()->withInput()->withErrors($validationMessages);
353
        }
354
355
        if (($response = $this->prepare($data)) instanceof Response) {
356
            return $response;
357
        }
358
359 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...
360
            $inserts = $this->prepareInsert($this->updates);
361
362
            $this->model->fill($inserts);
363
364
            $this->model->save();
365
366
            $this->updateRelation($this->relations);
367
        });
368
369
        if (($response = $this->callSaved()) instanceof Response) {
370
            return $response;
371
        }
372
373
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
374
            return $response;
375
        }
376
377
        return $this->redirectAfterStore();
378
    }
379
380
    /**
381
     * Get ajax response.
382
     *
383
     * @param string $message
384
     *
385
     * @return bool|\Illuminate\Http\JsonResponse
386
     */
387
    protected function ajaxResponse($message)
388
    {
389
        $request = Request::capture();
390
391
        // ajax but not pjax
392
        if ($request->ajax() && !$request->pjax()) {
393
            return response()->json([
0 ignored issues
show
Bug introduced by
The method json does only exist in Illuminate\Contracts\Routing\ResponseFactory, but not in Illuminate\Http\Response.

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...
394
                'status'  => true,
395
                'message' => $message,
396
            ]);
397
        }
398
399
        return false;
400
    }
401
402
    /**
403
     * Prepare input data for insert or update.
404
     *
405
     * @param array $data
406
     *
407
     * @return mixed
408
     */
409
    protected function prepare($data = [])
410
    {
411
        if (($response = $this->callSubmitted()) instanceof Response) {
412
            return $response;
413
        }
414
415
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);
416
417
        if (($response = $this->callSaving()) instanceof Response) {
418
            return $response;
419
        }
420
421
        $this->relations = $this->getRelationInputs($this->inputs);
422
423
        $this->updates = array_except($this->inputs, array_keys($this->relations));
424
    }
425
426
    /**
427
     * Remove ignored fields from input.
428
     *
429
     * @param array $input
430
     *
431
     * @return array
432
     */
433
    protected function removeIgnoredFields($input)
434
    {
435
        array_forget($input, $this->ignored);
436
437
        return $input;
438
    }
439
440
    /**
441
     * Get inputs for relations.
442
     *
443
     * @param array $inputs
444
     *
445
     * @return array
446
     */
447
    protected function getRelationInputs($inputs = [])
448
    {
449
        $relations = [];
450
451
        foreach ($inputs as $column => $value) {
452
            if (method_exists($this->model, $column)) {
453
                $relation = call_user_func([$this->model, $column]);
454
455
                if ($relation instanceof Relations\Relation) {
456
                    $relations[$column] = $value;
457
                }
458
            }
459
        }
460
461
        return $relations;
462
    }
463
464
    /**
465
     * Call editing callbacks.
466
     *
467
     * @return void
468
     */
469
    protected function callEditing()
470
    {
471
        foreach ($this->editing as $func) {
472
            call_user_func($func, $this);
473
        }
474
    }
475
476
    /**
477
     * Call submitted callback.
478
     *
479
     * @return mixed
480
     */
481
    protected function callSubmitted()
482
    {
483
        foreach ($this->submitted as $func) {
484
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
485
                return $ret;
486
            }
487
        }
488
    }
489
490
    /**
491
     * Call saving callback.
492
     *
493
     * @return mixed
494
     */
495
    protected function callSaving()
496
    {
497
        foreach ($this->saving as $func) {
498
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
499
                return $ret;
500
            }
501
        }
502
    }
503
504
    /**
505
     * Callback after saving a Model.
506
     *
507
     * @return mixed|null
508
     */
509
    protected function callSaved()
510
    {
511
        foreach ($this->saved as $func) {
512
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
513
                return $ret;
514
            }
515
        }
516
    }
517
518
    /**
519
     * Handle update.
520
     *
521
     * @param int $id
522
     *
523
     * @return \Symfony\Component\HttpFoundation\Response
524
     */
525
    public function update($id, $data = null)
526
    {
527
        $data = ($data) ?: Input::all();
528
529
        $isEditable = $this->isEditable($data);
530
531
        $data = $this->handleEditable($data);
532
533
        $data = $this->handleFileDelete($data);
534
535
        if ($this->handleOrderable($id, $data)) {
536
            return response([
0 ignored issues
show
Bug Compatibility introduced by
The expression response(array('status' ...n.update_succeeded'))); of type Illuminate\Http\Response...Routing\ResponseFactory adds the type Illuminate\Contracts\Routing\ResponseFactory to the return on line 536 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
537
                'status'  => true,
538
                'message' => trans('admin.update_succeeded'),
539
            ]);
540
        }
541
542
        /* @var Model $this->model */
543
        $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...
544
545
        $this->setFieldOriginalValue();
546
547
        // Handle validation errors.
548
        if ($validationMessages = $this->validationMessages($data)) {
549
            if (!$isEditable) {
550
                return back()->withInput()->withErrors($validationMessages);
551
            } else {
552
                return response()->json(['errors' => array_dot($validationMessages->getMessages())], 422);
0 ignored issues
show
Bug introduced by
The method json does only exist in Illuminate\Contracts\Routing\ResponseFactory, but not in Illuminate\Http\Response.

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...
553
            }
554
        }
555
556
        if (($response = $this->prepare($data)) instanceof Response) {
557
            return $response;
558
        }
559
560 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...
561
            $updates = $this->prepareUpdate($this->updates);
562
563
            $this->model->fill($updates);
564
565
            $this->model->save();
566
567
            $this->updateRelation($this->relations);
568
        });
569
570
        if (($result = $this->callSaved()) instanceof Response) {
571
            return $result;
572
        }
573
574
        if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) {
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->ajaxResponse(tran...in.update_succeeded')); of type boolean|Illuminate\Http\JsonResponse adds the type boolean to the return on line 575 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
575
            return $response;
576
        }
577
578
        return $this->redirectAfterUpdate($id);
579
    }
580
581
    /**
582
     * Get RedirectResponse after store.
583
     *
584
     * @return \Illuminate\Http\RedirectResponse
585
     */
586
    protected function redirectAfterStore()
587
    {
588
        $resourcesPath = $this->resource(0);
589
590
        $key = $this->model->getKey();
591
592
        return $this->redirectAfterSaving($resourcesPath, $key);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->redirectAfterSaving($resourcesPath, $key); of type Illuminate\Http\Redirect...nate\Routing\Redirector adds the type Illuminate\Routing\Redirector to the return on line 592 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterStore of type Illuminate\Http\RedirectResponse.
Loading history...
593
    }
594
595
    /**
596
     * Get RedirectResponse after update.
597
     *
598
     * @param mixed $key
599
     *
600
     * @return \Illuminate\Http\RedirectResponse
601
     */
602
    protected function redirectAfterUpdate($key)
603
    {
604
        $resourcesPath = $this->resource(-1);
605
606
        return $this->redirectAfterSaving($resourcesPath, $key);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->redirectAfterSaving($resourcesPath, $key); of type Illuminate\Http\Redirect...nate\Routing\Redirector adds the type Illuminate\Routing\Redirector to the return on line 606 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterUpdate of type Illuminate\Http\RedirectResponse.
Loading history...
607
    }
608
609
    /**
610
     * Get RedirectResponse after data saving.
611
     *
612
     * @param string $resourcesPath
613
     * @param string $key
614
     *
615
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
616
     */
617
    protected function redirectAfterSaving($resourcesPath, $key)
618
    {
619
        if (request('after-save') == 1) {
620
            // continue editing
621
            $url = rtrim($resourcesPath, '/')."/{$key}/edit";
622
        } elseif (request('after-save') == 2) {
623
            // continue creating
624
            $url = rtrim($resourcesPath, '/').'/create';
625
        } elseif (request('after-save') == 3) {
626
            // view resource
627
            $url = rtrim($resourcesPath, '/')."/{$key}";
628
        } else {
629
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
630
        }
631
632
        admin_toastr(trans('admin.save_succeeded'));
633
634
        return redirect($url);
635
    }
636
637
    /**
638
     * Check if request is from editable.
639
     *
640
     * @param array $input
641
     *
642
     * @return bool
643
     */
644
    protected function isEditable(array $input = [])
645
    {
646
        return array_key_exists('_editable', $input);
647
    }
648
649
    /**
650
     * Handle editable update.
651
     *
652
     * @param array $input
653
     *
654
     * @return array
655
     */
656
    protected function handleEditable(array $input = [])
657
    {
658
        if (array_key_exists('_editable', $input)) {
659
            $name = $input['name'];
660
            $value = $input['value'];
661
662
            array_forget($input, ['pk', 'value', 'name']);
663
            array_set($input, $name, $value);
664
        }
665
666
        return $input;
667
    }
668
669
    /**
670
     * @param array $input
671
     *
672
     * @return array
673
     */
674
    protected function handleFileDelete(array $input = [])
675
    {
676
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
677
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
678
            unset($input['key']);
679
        }
680
681
        Input::replace($input);
682
683
        return $input;
684
    }
685
686
    /**
687
     * Handle orderable update.
688
     *
689
     * @param int   $id
690
     * @param array $input
691
     *
692
     * @return bool
693
     */
694
    protected function handleOrderable($id, array $input = [])
695
    {
696
        if (array_key_exists('_orderable', $input)) {
697
            $model = $this->model->find($id);
698
699
            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...
700
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
701
702
                return true;
703
            }
704
        }
705
706
        return false;
707
    }
708
709
    /**
710
     * Update relation data.
711
     *
712
     * @param array $relationsData
713
     *
714
     * @return void
715
     */
716
    protected function updateRelation($relationsData)
717
    {
718
        foreach ($relationsData as $name => $values) {
719
            if (!method_exists($this->model, $name)) {
720
                continue;
721
            }
722
723
            $relation = $this->model->$name();
724
725
            $oneToOneRelation = $relation instanceof Relations\HasOne
726
                || $relation instanceof Relations\MorphOne
727
                || $relation instanceof Relations\BelongsTo;
728
729
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
730
731
            if (empty($prepared)) {
732
                continue;
733
            }
734
735
            switch (get_class($relation)) {
736
                case Relations\BelongsToMany::class:
737
                case Relations\MorphToMany::class:
738
                    if (isset($prepared[$name])) {
739
                        $relation->sync($prepared[$name]);
740
                    }
741
                    break;
742
                case Relations\HasOne::class:
743
744
                    $related = $this->model->$name;
745
746
                    // if related is empty
747
                    if (is_null($related)) {
748
                        $related = $relation->getRelated();
749
                        $qualifiedParentKeyName = $relation->getQualifiedParentKeyName();
750
                        $localKey = array_last(explode('.', $qualifiedParentKeyName));
751
                        $related->{$relation->getForeignKeyName()} = $this->model->{$localKey};
752
                    }
753
754
                    foreach ($prepared[$name] as $column => $value) {
755
                        $related->setAttribute($column, $value);
756
                    }
757
758
                    $related->save();
759
                    break;
760
                case Relations\BelongsTo::class:
761
762
                    $parent = $this->model->$name;
763
764
                    // if related is empty
765
                    if (is_null($parent)) {
766
                        $parent = $relation->getRelated();
767
                    }
768
769
                    foreach ($prepared[$name] as $column => $value) {
770
                        $parent->setAttribute($column, $value);
771
                    }
772
773
                    $parent->save();
774
775
                    // When in creating, associate two models
776
                    if (!$this->model->{$relation->getForeignKey()}) {
777
                        $this->model->{$relation->getForeignKey()} = $parent->getKey();
778
779
                        $this->model->save();
780
                    }
781
782
                    break;
783
                case Relations\MorphOne::class:
784
                    $related = $this->model->$name;
785
                    if (is_null($related)) {
786
                        $related = $relation->make();
787
                    }
788
                    foreach ($prepared[$name] as $column => $value) {
789
                        $related->setAttribute($column, $value);
790
                    }
791
                    $related->save();
792
                    break;
793
                case Relations\HasMany::class:
794
                case Relations\MorphMany::class:
795
796
                    foreach ($prepared[$name] as $related) {
797
                        /** @var Relations\Relation $relation */
798
                        $relation = $this->model()->$name();
799
800
                        $keyName = $relation->getRelated()->getKeyName();
801
802
                        $instance = $relation->findOrNew(array_get($related, $keyName));
803
804
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
805
                            $instance->delete();
806
807
                            continue;
808
                        }
809
810
                        array_forget($related, static::REMOVE_FLAG_NAME);
811
812
                        $instance->fill($related);
813
814
                        $instance->save();
815
                    }
816
817
                    break;
818
            }
819
        }
820
    }
821
822
    /**
823
     * Prepare input data for update.
824
     *
825
     * @param array $updates
826
     * @param bool  $oneToOneRelation If column is one-to-one relation.
827
     *
828
     * @return array
829
     */
830
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
831
    {
832
        $prepared = [];
833
834
        /** @var Field $field */
835
        foreach ($this->builder->fields() as $field) {
836
            $columns = $field->column();
837
838
            // If column not in input array data, then continue.
839
            if (!array_has($updates, $columns)) {
840
                continue;
841
            }
842
843
            if ($this->invalidColumn($columns, $oneToOneRelation)) {
844
                continue;
845
            }
846
847
            $value = $this->getDataByColumn($updates, $columns);
848
849
            $value = $field->prepare($value);
850
851 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...
852
                foreach ($columns as $name => $column) {
853
                    array_set($prepared, $column, $value[$name]);
854
                }
855
            } elseif (is_string($columns)) {
856
                array_set($prepared, $columns, $value);
857
            }
858
        }
859
860
        return $prepared;
861
    }
862
863
    /**
864
     * @param string|array $columns
865
     * @param bool         $oneToOneRelation
866
     *
867
     * @return bool
868
     */
869
    protected function invalidColumn($columns, $oneToOneRelation = false)
870
    {
871
        foreach ((array) $columns as $column) {
872
            if ((!$oneToOneRelation && Str::contains($column, '.')) ||
873
                ($oneToOneRelation && !Str::contains($column, '.'))) {
874
                return true;
875
            }
876
        }
877
878
        return false;
879
    }
880
881
    /**
882
     * Prepare input data for insert.
883
     *
884
     * @param $inserts
885
     *
886
     * @return array
887
     */
888
    protected function prepareInsert($inserts)
889
    {
890
        if ($this->isHasOneRelation($inserts)) {
891
            $inserts = array_dot($inserts);
892
        }
893
894
        foreach ($inserts as $column => $value) {
895
            if (is_null($field = $this->getFieldByColumn($column))) {
896
                unset($inserts[$column]);
897
                continue;
898
            }
899
900
            $inserts[$column] = $field->prepare($value);
901
        }
902
903
        $prepared = [];
904
905
        foreach ($inserts as $key => $value) {
906
            array_set($prepared, $key, $value);
907
        }
908
909
        return $prepared;
910
    }
911
912
    /**
913
     * Is input data is has-one relation.
914
     *
915
     * @param array $inserts
916
     *
917
     * @return bool
918
     */
919
    protected function isHasOneRelation($inserts)
920
    {
921
        $first = current($inserts);
922
923
        if (!is_array($first)) {
924
            return false;
925
        }
926
927
        if (is_array(current($first))) {
928
            return false;
929
        }
930
931
        return Arr::isAssoc($first);
932
    }
933
934
    /**
935
     * Set after getting editing model callback.
936
     *
937
     * @param Closure $callback
938
     *
939
     * @return void
940
     */
941
    public function editing(Closure $callback)
942
    {
943
        $this->editing[] = $callback;
944
    }
945
946
    /**
947
     * Set submitted callback.
948
     *
949
     * @param Closure $callback
950
     *
951
     * @return void
952
     */
953
    public function submitted(Closure $callback)
954
    {
955
        $this->submitted[] = $callback;
956
    }
957
958
    /**
959
     * Set saving callback.
960
     *
961
     * @param Closure $callback
962
     *
963
     * @return void
964
     */
965
    public function saving(Closure $callback)
966
    {
967
        $this->saving[] = $callback;
968
    }
969
970
    /**
971
     * Set saved callback.
972
     *
973
     * @param Closure $callback
974
     *
975
     * @return void
976
     */
977
    public function saved(Closure $callback)
978
    {
979
        $this->saved[] = $callback;
980
    }
981
982
    /**
983
     * Ignore fields to save.
984
     *
985
     * @param string|array $fields
986
     *
987
     * @return $this
988
     */
989
    public function ignore($fields)
990
    {
991
        $this->ignored = array_merge($this->ignored, (array) $fields);
992
993
        return $this;
994
    }
995
996
    /**
997
     * @param array        $data
998
     * @param string|array $columns
999
     *
1000
     * @return array|mixed
1001
     */
1002 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...
1003
    {
1004
        if (is_string($columns)) {
1005
            return array_get($data, $columns);
1006
        }
1007
1008
        if (is_array($columns)) {
1009
            $value = [];
1010
            foreach ($columns as $name => $column) {
1011
                if (!array_has($data, $column)) {
1012
                    continue;
1013
                }
1014
                $value[$name] = array_get($data, $column);
1015
            }
1016
1017
            return $value;
1018
        }
1019
    }
1020
1021
    /**
1022
     * Find field object by column.
1023
     *
1024
     * @param $column
1025
     *
1026
     * @return mixed
1027
     */
1028
    protected function getFieldByColumn($column)
1029
    {
1030
        return $this->builder->fields()->first(
1031
            function (Field $field) use ($column) {
1032
                if (is_array($field->column())) {
1033
                    return in_array($column, $field->column());
1034
                }
1035
1036
                return $field->column() == $column;
1037
            }
1038
        );
1039
    }
1040
1041
    /**
1042
     * Set original data for each field.
1043
     *
1044
     * @return void
1045
     */
1046
    protected function setFieldOriginalValue()
1047
    {
1048
//        static::doNotSnakeAttributes($this->model);
1049
1050
        $values = $this->model->toArray();
1051
1052
        $this->builder->fields()->each(function (Field $field) use ($values) {
1053
            $field->setOriginal($values);
1054
        });
1055
    }
1056
1057
    /**
1058
     * Set all fields value in form.
1059
     *
1060
     * @param $id
1061
     *
1062
     * @return void
1063
     */
1064
    protected function setFieldValue($id)
1065
    {
1066
        $relations = $this->getRelations();
1067
1068
        $builder = $this->model();
1069
1070
        if ($this->isSoftDeletes) {
1071
            $builder->withTrashed();
1072
        }
1073
1074
        $this->model = $builder->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...
1075
1076
        $this->callEditing();
1077
1078
//        static::doNotSnakeAttributes($this->model);
1079
1080
        $data = $this->model->toArray();
1081
1082
        $this->builder->fields()->each(function (Field $field) use ($data) {
1083
            if (!in_array($field->column(), $this->ignored)) {
1084
                $field->fill($data);
1085
            }
1086
        });
1087
    }
1088
1089
    /**
1090
     * Don't snake case attributes.
1091
     *
1092
     * @param Model $model
1093
     *
1094
     * @return void
1095
     */
1096
    protected static function doNotSnakeAttributes(Model $model)
1097
    {
1098
        $class = get_class($model);
1099
1100
        $class::$snakeAttributes = false;
1101
    }
1102
1103
    /**
1104
     * Get validation messages.
1105
     *
1106
     * @param array $input
1107
     *
1108
     * @return MessageBag|bool
1109
     */
1110
    public function validationMessages($input)
1111
    {
1112
        $failedValidators = [];
1113
1114
        /** @var Field $field */
1115
        foreach ($this->builder->fields() as $field) {
1116
            if (!$validator = $field->getValidator($input)) {
1117
                continue;
1118
            }
1119
1120
            if (($validator instanceof Validator) && !$validator->passes()) {
1121
                $failedValidators[] = $validator;
1122
            }
1123
        }
1124
1125
        $message = $this->mergeValidationMessages($failedValidators);
1126
1127
        return $message->any() ? $message : false;
1128
    }
1129
1130
    /**
1131
     * Merge validation messages from input validators.
1132
     *
1133
     * @param \Illuminate\Validation\Validator[] $validators
1134
     *
1135
     * @return MessageBag
1136
     */
1137
    protected function mergeValidationMessages($validators)
1138
    {
1139
        $messageBag = new MessageBag();
1140
1141
        foreach ($validators as $validator) {
1142
            $messageBag = $messageBag->merge($validator->messages());
1143
        }
1144
1145
        return $messageBag;
1146
    }
1147
1148
    /**
1149
     * Get all relations of model from callable.
1150
     *
1151
     * @return array
1152
     */
1153
    public function getRelations()
1154
    {
1155
        $relations = $columns = [];
1156
1157
        /** @var Field $field */
1158
        foreach ($this->builder->fields() as $field) {
1159
            $columns[] = $field->column();
1160
        }
1161
1162
        foreach (array_flatten($columns) as $column) {
1163
            if (str_contains($column, '.')) {
1164
                list($relation) = explode('.', $column);
1165
1166
                if (method_exists($this->model, $relation) &&
1167
                    $this->model->$relation() instanceof Relations\Relation
1168
                ) {
1169
                    $relations[] = $relation;
1170
                }
1171
            } elseif (method_exists($this->model, $column) &&
1172
                !method_exists(Model::class, $column)
1173
            ) {
1174
                $relations[] = $column;
1175
            }
1176
        }
1177
1178
        return array_unique($relations);
1179
    }
1180
1181
    /**
1182
     * Set action for form.
1183
     *
1184
     * @param string $action
1185
     *
1186
     * @return $this
1187
     */
1188
    public function setAction($action)
1189
    {
1190
        $this->builder()->setAction($action);
1191
1192
        return $this;
1193
    }
1194
1195
    /**
1196
     * Set field and label width in current form.
1197
     *
1198
     * @param int $fieldWidth
1199
     * @param int $labelWidth
1200
     *
1201
     * @return $this
1202
     */
1203
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1204
    {
1205
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1206
            /* @var Field $field  */
1207
            $field->setWidth($fieldWidth, $labelWidth);
1208
        });
1209
1210
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1211
1212
        return $this;
1213
    }
1214
1215
    /**
1216
     * Set view for form.
1217
     *
1218
     * @param string $view
1219
     *
1220
     * @return $this
1221
     */
1222
    public function setView($view)
1223
    {
1224
        $this->builder()->setView($view);
1225
1226
        return $this;
1227
    }
1228
1229
    /**
1230
     * Set title for form.
1231
     *
1232
     * @param string $title
1233
     *
1234
     * @return $this
1235
     */
1236
    public function setTitle($title = '')
1237
    {
1238
        $this->builder()->setTitle($title);
1239
1240
        return $this;
1241
    }
1242
1243
    /**
1244
     * Add a row in form.
1245
     *
1246
     * @param Closure $callback
1247
     *
1248
     * @return $this
1249
     */
1250
    public function row(Closure $callback)
1251
    {
1252
        $this->rows[] = new Row($callback, $this);
1253
1254
        return $this;
1255
    }
1256
1257
    /**
1258
     * Tools setting for form.
1259
     *
1260
     * @param Closure $callback
1261
     */
1262
    public function tools(Closure $callback)
1263
    {
1264
        $callback->call($this, $this->builder->getTools());
1265
    }
1266
1267
    /**
1268
     * Disable form submit.
1269
     *
1270
     * @return $this
1271
     *
1272
     * @deprecated
1273
     */
1274
    public function disableSubmit()
1275
    {
1276
        $this->builder()->getFooter()->disableSubmit();
1277
1278
        return $this;
1279
    }
1280
1281
    /**
1282
     * Disable form reset.
1283
     *
1284
     * @return $this
1285
     *
1286
     * @deprecated
1287
     */
1288
    public function disableReset()
1289
    {
1290
        $this->builder()->getFooter()->disableReset();
1291
1292
        return $this;
1293
    }
1294
1295
    /**
1296
     * Disable View Checkbox on footer.
1297
     *
1298
     * @return $this
1299
     */
1300
    public function disableViewCheck()
1301
    {
1302
        $this->builder()->getFooter()->disableViewCheck();
1303
1304
        return $this;
1305
    }
1306
1307
    /**
1308
     * Disable Editing Checkbox on footer.
1309
     *
1310
     * @return $this
1311
     */
1312
    public function disableEditingCheck()
1313
    {
1314
        $this->builder()->getFooter()->disableEditingCheck();
1315
1316
        return $this;
1317
    }
1318
1319
    /**
1320
     * Disable Creating Checkbox on footer.
1321
     *
1322
     * @return $this
1323
     */
1324
    public function disableCreatingCheck()
1325
    {
1326
        $this->builder()->getFooter()->disableCreatingCheck();
1327
1328
        return $this;
1329
    }
1330
1331
    /**
1332
     * Footer setting for form.
1333
     *
1334
     * @param Closure $callback
1335
     */
1336
    public function footer(Closure $callback)
1337
    {
1338
        call_user_func($callback, $this->builder()->getFooter());
1339
    }
1340
1341
    /**
1342
     * Get current resource route url.
1343
     *
1344
     * @param int $slice
1345
     *
1346
     * @return string
1347
     */
1348
    public function resource($slice = -2)
1349
    {
1350
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1351
1352
        if ($slice != 0) {
1353
            $segments = array_slice($segments, 0, $slice);
1354
        }
1355
1356
        return implode('/', $segments);
1357
    }
1358
1359
    /**
1360
     * Render the form contents.
1361
     *
1362
     * @return string
1363
     */
1364
    public function render()
1365
    {
1366
        try {
1367
            return $this->builder->render();
1368
        } catch (\Exception $e) {
1369
            return Handler::renderException($e);
1370
        }
1371
    }
1372
1373
    /**
1374
     * Get or set input data.
1375
     *
1376
     * @param string $key
1377
     * @param null   $value
1378
     *
1379
     * @return array|mixed
1380
     */
1381
    public function input($key, $value = null)
1382
    {
1383
        if (is_null($value)) {
1384
            return array_get($this->inputs, $key);
1385
        }
1386
1387
        return array_set($this->inputs, $key, $value);
1388
    }
1389
1390
    /**
1391
     * Register builtin fields.
1392
     *
1393
     * @return void
1394
     */
1395
    public static function registerBuiltinFields()
1396
    {
1397
        $map = [
1398
            'button'         => Field\Button::class,
1399
            'checkbox'       => Field\Checkbox::class,
1400
            'color'          => Field\Color::class,
1401
            'currency'       => Field\Currency::class,
1402
            'date'           => Field\Date::class,
1403
            'dateRange'      => Field\DateRange::class,
1404
            'datetime'       => Field\Datetime::class,
1405
            'dateTimeRange'  => Field\DatetimeRange::class,
1406
            'datetimeRange'  => Field\DatetimeRange::class,
1407
            'decimal'        => Field\Decimal::class,
1408
            'display'        => Field\Display::class,
1409
            'divider'        => Field\Divide::class,
1410
            'divide'         => Field\Divide::class,
1411
            'embeds'         => Field\Embeds::class,
1412
            'editor'         => Field\Editor::class,
1413
            'email'          => Field\Email::class,
1414
            'file'           => Field\File::class,
1415
            'hasMany'        => Field\HasMany::class,
1416
            'hidden'         => Field\Hidden::class,
1417
            'id'             => Field\Id::class,
1418
            'image'          => Field\Image::class,
1419
            'ip'             => Field\Ip::class,
1420
            'map'            => Field\Map::class,
1421
            'mobile'         => Field\Mobile::class,
1422
            'month'          => Field\Month::class,
1423
            'multipleSelect' => Field\MultipleSelect::class,
1424
            'number'         => Field\Number::class,
1425
            'password'       => Field\Password::class,
1426
            'radio'          => Field\Radio::class,
1427
            'rate'           => Field\Rate::class,
1428
            'select'         => Field\Select::class,
1429
            'slider'         => Field\Slider::class,
1430
            'switch'         => Field\SwitchField::class,
1431
            'text'           => Field\Text::class,
1432
            'textarea'       => Field\Textarea::class,
1433
            'time'           => Field\Time::class,
1434
            'timeRange'      => Field\TimeRange::class,
1435
            'url'            => Field\Url::class,
1436
            'year'           => Field\Year::class,
1437
            'html'           => Field\Html::class,
1438
            'tags'           => Field\Tags::class,
1439
            'icon'           => Field\Icon::class,
1440
            'multipleFile'   => Field\MultipleFile::class,
1441
            'multipleImage'  => Field\MultipleImage::class,
1442
            'captcha'        => Field\Captcha::class,
1443
            'listbox'        => Field\Listbox::class,
1444
        ];
1445
1446
        foreach ($map as $abstract => $class) {
1447
            static::extend($abstract, $class);
1448
        }
1449
    }
1450
1451
    /**
1452
     * Register custom field.
1453
     *
1454
     * @param string $abstract
1455
     * @param string $class
1456
     *
1457
     * @return void
1458
     */
1459
    public static function extend($abstract, $class)
1460
    {
1461
        static::$availableFields[$abstract] = $class;
1462
    }
1463
1464
    /**
1465
     * Set form field alias.
1466
     *
1467
     * @param string $field
1468
     * @param string $alias
1469
     *
1470
     * @return void
1471
     */
1472
    public static function alias($field, $alias)
1473
    {
1474
        static::$fieldAlias[$alias] = $field;
1475
    }
1476
1477
    /**
1478
     * Remove registered field.
1479
     *
1480
     * @param array|string $abstract
1481
     */
1482
    public static function forget($abstract)
1483
    {
1484
        array_forget(static::$availableFields, $abstract);
1485
    }
1486
1487
    /**
1488
     * Find field class.
1489
     *
1490
     * @param string $method
1491
     *
1492
     * @return bool|mixed
1493
     */
1494
    public static function findFieldClass($method)
1495
    {
1496
        // If alias exists.
1497
        if (isset(static::$fieldAlias[$method])) {
1498
            $method = static::$fieldAlias[$method];
1499
        }
1500
1501
        $class = array_get(static::$availableFields, $method);
1502
1503
        if (class_exists($class)) {
1504
            return $class;
1505
        }
1506
1507
        return false;
1508
    }
1509
1510
    /**
1511
     * Collect assets required by registered field.
1512
     *
1513
     * @return array
1514
     */
1515
    public static function collectFieldAssets()
1516
    {
1517
        if (!empty(static::$collectedAssets)) {
1518
            return static::$collectedAssets;
1519
        }
1520
1521
        $css = collect();
1522
        $js = collect();
1523
1524
        foreach (static::$availableFields as $field) {
1525
            if (!method_exists($field, 'getAssets')) {
1526
                continue;
1527
            }
1528
1529
            $assets = call_user_func([$field, 'getAssets']);
1530
1531
            $css->push(array_get($assets, 'css'));
1532
            $js->push(array_get($assets, 'js'));
1533
        }
1534
1535
        return static::$collectedAssets = [
1536
            'css' => $css->flatten()->unique()->filter()->toArray(),
1537
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1538
        ];
1539
    }
1540
1541
    /**
1542
     * Getter.
1543
     *
1544
     * @param string $name
1545
     *
1546
     * @return array|mixed
1547
     */
1548
    public function __get($name)
1549
    {
1550
        return $this->input($name);
1551
    }
1552
1553
    /**
1554
     * Setter.
1555
     *
1556
     * @param string $name
1557
     * @param $value
1558
     */
1559
    public function __set($name, $value)
1560
    {
1561
        return array_set($this->inputs, $name, $value);
1562
    }
1563
1564
    /**
1565
     * Generate a Field object and add to form builder if Field exists.
1566
     *
1567
     * @param string $method
1568
     * @param array  $arguments
1569
     *
1570
     * @return Field
1571
     */
1572
    public function __call($method, $arguments)
1573
    {
1574
        if ($className = static::findFieldClass($method)) {
1575
            $column = array_get($arguments, 0, ''); //[0];
1576
1577
            $element = new $className($column, array_slice($arguments, 1));
1578
1579
            $this->pushField($element);
1580
1581
            return $element;
1582
        }
1583
1584
        admin_error('Error', "Field type [$method] does not exist.");
1585
1586
        return new Field\Nullable();
1587
    }
1588
}
1589