Completed
Pull Request — master (#2622)
by
unknown
02:40
created

Form::callSaving()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 3
nop 0
dl 0
loc 8
rs 10
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
            foreach ($inserts as $column => $value) {
363
                $this->model->setAttribute($column, $value);
364
            }
365
366
            $this->model->save();
367
368
            $this->updateRelation($this->relations);
369
        });
370
371
        if (($response = $this->callSaved()) instanceof Response) {
372
            return $response;
373
        }
374
375
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
376
            return $response;
377
        }
378
379
        return $this->redirectAfterStore();
380
    }
381
382
    /**
383
     * Get ajax response.
384
     *
385
     * @param string $message
386
     *
387
     * @return bool|\Illuminate\Http\JsonResponse
388
     */
389
    protected function ajaxResponse($message)
390
    {
391
        $request = Request::capture();
392
393
        // ajax but not pjax
394
        if ($request->ajax() && !$request->pjax()) {
395
            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...
396
                'status'  => true,
397
                'message' => $message,
398
            ]);
399
        }
400
401
        return false;
402
    }
403
404
    /**
405
     * Prepare input data for insert or update.
406
     *
407
     * @param array $data
408
     *
409
     * @return mixed
410
     */
411
    protected function prepare($data = [])
412
    {
413
        if (($response = $this->callSubmitted()) instanceof Response) {
414
            return $response;
415
        }
416
417
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);
418
419
        if (($response = $this->callSaving()) instanceof Response) {
420
            return $response;
421
        }
422
423
        $this->relations = $this->getRelationInputs($this->inputs);
424
425
        $this->updates = array_except($this->inputs, array_keys($this->relations));
426
    }
427
428
    /**
429
     * Remove ignored fields from input.
430
     *
431
     * @param array $input
432
     *
433
     * @return array
434
     */
435
    protected function removeIgnoredFields($input)
436
    {
437
        array_forget($input, $this->ignored);
438
439
        return $input;
440
    }
441
442
    /**
443
     * Get inputs for relations.
444
     *
445
     * @param array $inputs
446
     *
447
     * @return array
448
     */
449
    protected function getRelationInputs($inputs = [])
450
    {
451
        $relations = [];
452
453
        foreach ($inputs as $column => $value) {
454
            if (method_exists($this->model, $column)) {
455
                $relation = call_user_func([$this->model, $column]);
456
457
                if ($relation instanceof Relations\Relation) {
458
                    $relations[$column] = $value;
459
                }
460
            }
461
        }
462
463
        return $relations;
464
    }
465
466
    /**
467
     * Call editing callbacks.
468
     *
469
     * @return void
470
     */
471
    protected function callEditing()
472
    {
473
        foreach ($this->editing as $func) {
474
            call_user_func($func, $this);
475
        }
476
    }
477
478
    /**
479
     * Call submitted callback.
480
     *
481
     * @return mixed
482
     */
483
    protected function callSubmitted()
484
    {
485
        foreach ($this->submitted as $func) {
486
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
487
                return $ret;
488
            }
489
        }
490
    }
491
492
    /**
493
     * Call saving callback.
494
     *
495
     * @return mixed
496
     */
497
    protected function callSaving()
498
    {
499
        foreach ($this->saving as $func) {
500
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
501
                return $ret;
502
            }
503
        }
504
    }
505
506
    /**
507
     * Callback after saving a Model.
508
     *
509
     * @return mixed|null
510
     */
511
    protected function callSaved()
512
    {
513
        foreach ($this->saved as $func) {
514
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
515
                return $ret;
516
            }
517
        }
518
    }
519
520
    /**
521
     * Handle update.
522
     *
523
     * @param int $id
524
     *
525
     * @return \Symfony\Component\HttpFoundation\Response
526
     */
527
    public function update($id, $data = null)
528
    {
529
        $data = ($data) ?: Input::all();
530
531
        $isEditable = $this->isEditable($data);
532
533
        $data = $this->handleEditable($data);
534
535
        $data = $this->handleFileDelete($data);
536
537
        if ($this->handleOrderable($id, $data)) {
538
            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 538 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
539
                'status'  => true,
540
                'message' => trans('admin.update_succeeded'),
541
            ]);
542
        }
543
544
        /* @var Model $this->model */
545
        $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...
546
547
        $this->setFieldOriginalValue();
548
549
        // Handle validation errors.
550
        if ($validationMessages = $this->validationMessages($data)) {
551
            if (!$isEditable) {
552
                return back()->withInput()->withErrors($validationMessages);
553
            } else {
554
                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...
555
            }
556
        }
557
558
        if (($response = $this->prepare($data)) instanceof Response) {
559
            return $response;
560
        }
561
562 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...
563
            $updates = $this->prepareUpdate($this->updates);
564
565
            foreach ($updates as $column => $value) {
566
                /* @var Model $this->model */
567
                $this->model->setAttribute($column, $value);
568
            }
569
570
            $this->model->save();
571
572
            $this->updateRelation($this->relations);
573
        });
574
575
        if (($result = $this->callSaved()) instanceof Response) {
576
            return $result;
577
        }
578
579
        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 580 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
580
            return $response;
581
        }
582
583
        return $this->redirectAfterUpdate($id);
584
    }
585
586
    /**
587
     * Get RedirectResponse after store.
588
     *
589
     * @return \Illuminate\Http\RedirectResponse
590
     */
591
    protected function redirectAfterStore()
592
    {
593
        $resourcesPath = $this->resource(0);
594
595
        $key = $this->model->getKey();
596
597
        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 597 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterStore of type Illuminate\Http\RedirectResponse.
Loading history...
598
    }
599
600
    /**
601
     * Get RedirectResponse after update.
602
     *
603
     * @param mixed $key
604
     *
605
     * @return \Illuminate\Http\RedirectResponse
606
     */
607
    protected function redirectAfterUpdate($key)
608
    {
609
        $resourcesPath = $this->resource(-1);
610
611
        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 611 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterUpdate of type Illuminate\Http\RedirectResponse.
Loading history...
612
    }
613
614
    /**
615
     * Get RedirectResponse after data saving.
616
     *
617
     * @param string $resourcesPath
618
     * @param string $key
619
     *
620
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
621
     */
622
    protected function redirectAfterSaving($resourcesPath, $key)
623
    {
624
        if (request('after-save') == 1) {
625
            // continue editing
626
            $url = rtrim($resourcesPath, '/')."/{$key}/edit";
627
        } elseif (request('after-save') == 2) {
628
            // view resource
629
            $url = rtrim($resourcesPath, '/')."/{$key}";
630
        } else {
631
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
632
        }
633
634
        admin_toastr(trans('admin.save_succeeded'));
635
636
        return redirect($url);
637
    }
638
639
    /**
640
     * Check if request is from editable.
641
     *
642
     * @param array $input
643
     *
644
     * @return bool
645
     */
646
    protected function isEditable(array $input = [])
647
    {
648
        return array_key_exists('_editable', $input);
649
    }
650
651
    /**
652
     * Handle editable update.
653
     *
654
     * @param array $input
655
     *
656
     * @return array
657
     */
658
    protected function handleEditable(array $input = [])
659
    {
660
        if (array_key_exists('_editable', $input)) {
661
            $name = $input['name'];
662
            $value = $input['value'];
663
664
            array_forget($input, ['pk', 'value', 'name']);
665
            array_set($input, $name, $value);
666
        }
667
668
        return $input;
669
    }
670
671
    /**
672
     * @param array $input
673
     *
674
     * @return array
675
     */
676
    protected function handleFileDelete(array $input = [])
677
    {
678
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
679
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
680
            unset($input['key']);
681
        }
682
683
        Input::replace($input);
684
685
        return $input;
686
    }
687
688
    /**
689
     * Handle orderable update.
690
     *
691
     * @param int   $id
692
     * @param array $input
693
     *
694
     * @return bool
695
     */
696
    protected function handleOrderable($id, array $input = [])
697
    {
698
        if (array_key_exists('_orderable', $input)) {
699
            $model = $this->model->find($id);
700
701
            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...
702
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
703
704
                return true;
705
            }
706
        }
707
708
        return false;
709
    }
710
711
    /**
712
     * Update relation data.
713
     *
714
     * @param array $relationsData
715
     *
716
     * @return void
717
     */
718
    protected function updateRelation($relationsData)
719
    {
720
        foreach ($relationsData as $name => $values) {
721
            if (!method_exists($this->model, $name)) {
722
                continue;
723
            }
724
725
            $relation = $this->model->$name();
726
727
            $oneToOneRelation = $relation instanceof Relations\HasOne
728
                || $relation instanceof Relations\MorphOne
729
                || $relation instanceof Relations\BelongsTo;
730
731
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
732
733
            if (empty($prepared)) {
734
                continue;
735
            }
736
737
            switch (get_class($relation)) {
738
                case Relations\BelongsToMany::class:
739
                case Relations\MorphToMany::class:
740
                    if (isset($prepared[$name])) {
741
                        $relation->sync($prepared[$name]);
742
                    }
743
                    break;
744 View Code Duplication
                case Relations\HasOne::class:
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...
745
746
                    $related = $this->model->$name;
747
748
                    // if related is empty
749
                    if (is_null($related)) {
750
                        $related = $relation->getRelated();
751
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
752
                    }
753
754
                    foreach ($prepared[$name] as $column => $value) {
755
                        $related->setAttribute($column, $value);
756
                    }
757
758
                    $related->save();
759
                    break;
760 View Code Duplication
                case Relations\BelongsTo::class:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
761
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()->newQuery();
1069
1070
        if ($this->isSoftDeletes) {
1071
            $builder->withTrashed();
1072
        }
1073
1074
        $this->model = $builder->with($relations)->findOrFail($id);
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 fields.
1150
     * @return Collection
1151
     */
1152
    public function getFields(){
1153
        return $this->builder->fields();
1154
    }
1155
1156
    /**
1157
     * Get all relations of model from callable.
1158
     *
1159
     * @return array
1160
     */
1161
    public function getRelations()
1162
    {
1163
        $relations = $columns = [];
1164
1165
        /** @var Field $field */
1166
        foreach ($this->builder->fields() as $field) {
1167
            $columns[] = $field->column();
1168
        }
1169
1170
        foreach (array_flatten($columns) as $column) {
1171
            if (str_contains($column, '.')) {
1172
                list($relation) = explode('.', $column);
1173
1174
                if (method_exists($this->model, $relation) &&
1175
                    $this->model->$relation() instanceof Relations\Relation
1176
                ) {
1177
                    $relations[] = $relation;
1178
                }
1179
            } elseif (method_exists($this->model, $column) &&
1180
                !method_exists(Model::class, $column)
1181
            ) {
1182
                $relations[] = $column;
1183
            }
1184
        }
1185
1186
        return array_unique($relations);
1187
    }
1188
1189
    /**
1190
     * Set action for form.
1191
     *
1192
     * @param string $action
1193
     *
1194
     * @return $this
1195
     */
1196
    public function setAction($action)
1197
    {
1198
        $this->builder()->setAction($action);
1199
1200
        return $this;
1201
    }
1202
1203
    /**
1204
     * Set field and label width in current form.
1205
     *
1206
     * @param int $fieldWidth
1207
     * @param int $labelWidth
1208
     *
1209
     * @return $this
1210
     */
1211
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1212
    {
1213
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1214
            /* @var Field $field  */
1215
            $field->setWidth($fieldWidth, $labelWidth);
1216
        });
1217
1218
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1219
1220
        return $this;
1221
    }
1222
1223
    /**
1224
     * Set view for form.
1225
     *
1226
     * @param string $view
1227
     *
1228
     * @return $this
1229
     */
1230
    public function setView($view)
1231
    {
1232
        $this->builder()->setView($view);
1233
1234
        return $this;
1235
    }
1236
1237
    /**
1238
     * Set title for form.
1239
     *
1240
     * @param string $title
1241
     *
1242
     * @return $this
1243
     */
1244
    public function setTitle($title = '')
1245
    {
1246
        $this->builder()->setTitle($title);
1247
1248
        return $this;
1249
    }
1250
1251
    /**
1252
     * Add a row in form.
1253
     *
1254
     * @param Closure $callback
1255
     *
1256
     * @return $this
1257
     */
1258
    public function row(Closure $callback)
1259
    {
1260
        $this->rows[] = new Row($callback, $this);
1261
1262
        return $this;
1263
    }
1264
1265
    /**
1266
     * Tools setting for form.
1267
     *
1268
     * @param Closure $callback
1269
     */
1270
    public function tools(Closure $callback)
1271
    {
1272
        $callback->call($this, $this->builder->getTools());
1273
    }
1274
1275
    /**
1276
     * Disable form submit.
1277
     *
1278
     * @return $this
1279
     *
1280
     * @deprecated
1281
     */
1282
    public function disableSubmit()
1283
    {
1284
        $this->builder()->getFooter()->disableSubmit();
1285
1286
        return $this;
1287
    }
1288
1289
    /**
1290
     * Disable form reset.
1291
     *
1292
     * @return $this
1293
     *
1294
     * @deprecated
1295
     */
1296
    public function disableReset()
1297
    {
1298
        $this->builder()->getFooter()->disableReset();
1299
1300
        return $this;
1301
    }
1302
1303
    /**
1304
     * Disable View Checkbox on footer.
1305
     *
1306
     * @return $this
1307
     */
1308
    public function disableViewCheck()
1309
    {
1310
        $this->builder()->getFooter()->disableViewCheck();
1311
1312
        return $this;
1313
    }
1314
1315
    /**
1316
     * Disable Editing Checkbox on footer.
1317
     *
1318
     * @return $this
1319
     */
1320
    public function disableEditingCheck()
1321
    {
1322
        $this->builder()->getFooter()->disableEditingCheck();
1323
1324
        return $this;
1325
    }
1326
1327
    /**
1328
     * Footer setting for form.
1329
     *
1330
     * @param Closure $callback
1331
     */
1332
    public function footer(Closure $callback)
1333
    {
1334
        call_user_func($callback, $this->builder()->getFooter());
1335
    }
1336
1337
    /**
1338
     * Get current resource route url.
1339
     *
1340
     * @param int $slice
1341
     *
1342
     * @return string
1343
     */
1344
    public function resource($slice = -2)
1345
    {
1346
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1347
1348
        if ($slice != 0) {
1349
            $segments = array_slice($segments, 0, $slice);
1350
        }
1351
1352
        return implode('/', $segments);
1353
    }
1354
1355
    /**
1356
     * Render the form contents.
1357
     *
1358
     * @return string
1359
     */
1360
    public function render()
1361
    {
1362
        try {
1363
            return $this->builder->render();
1364
        } catch (\Exception $e) {
1365
            return Handler::renderException($e);
1366
        }
1367
    }
1368
1369
    /**
1370
     * Get or set input data.
1371
     *
1372
     * @param string $key
1373
     * @param null   $value
1374
     *
1375
     * @return array|mixed
1376
     */
1377
    public function input($key, $value = null)
1378
    {
1379
        if (is_null($value)) {
1380
            return array_get($this->inputs, $key);
1381
        }
1382
1383
        return array_set($this->inputs, $key, $value);
1384
    }
1385
1386
    /**
1387
     * Register builtin fields.
1388
     *
1389
     * @return void
1390
     */
1391
    public static function registerBuiltinFields()
1392
    {
1393
        $map = [
1394
            'button'         => Field\Button::class,
1395
            'checkbox'       => Field\Checkbox::class,
1396
            'color'          => Field\Color::class,
1397
            'currency'       => Field\Currency::class,
1398
            'date'           => Field\Date::class,
1399
            'dateRange'      => Field\DateRange::class,
1400
            'datetime'       => Field\Datetime::class,
1401
            'dateTimeRange'  => Field\DatetimeRange::class,
1402
            'datetimeRange'  => Field\DatetimeRange::class,
1403
            'decimal'        => Field\Decimal::class,
1404
            'display'        => Field\Display::class,
1405
            'divider'        => Field\Divide::class,
1406
            'divide'         => Field\Divide::class,
1407
            'embeds'         => Field\Embeds::class,
1408
            'editor'         => Field\Editor::class,
1409
            'email'          => Field\Email::class,
1410
            'file'           => Field\File::class,
1411
            'hasMany'        => Field\HasMany::class,
1412
            'hidden'         => Field\Hidden::class,
1413
            'id'             => Field\Id::class,
1414
            'image'          => Field\Image::class,
1415
            'ip'             => Field\Ip::class,
1416
            'map'            => Field\Map::class,
1417
            'mobile'         => Field\Mobile::class,
1418
            'month'          => Field\Month::class,
1419
            'multipleSelect' => Field\MultipleSelect::class,
1420
            'number'         => Field\Number::class,
1421
            'password'       => Field\Password::class,
1422
            'radio'          => Field\Radio::class,
1423
            'rate'           => Field\Rate::class,
1424
            'select'         => Field\Select::class,
1425
            'slider'         => Field\Slider::class,
1426
            'switch'         => Field\SwitchField::class,
1427
            'text'           => Field\Text::class,
1428
            'textarea'       => Field\Textarea::class,
1429
            'time'           => Field\Time::class,
1430
            'timeRange'      => Field\TimeRange::class,
1431
            'url'            => Field\Url::class,
1432
            'year'           => Field\Year::class,
1433
            'html'           => Field\Html::class,
1434
            'tags'           => Field\Tags::class,
1435
            'icon'           => Field\Icon::class,
1436
            'multipleFile'   => Field\MultipleFile::class,
1437
            'multipleImage'  => Field\MultipleImage::class,
1438
            'captcha'        => Field\Captcha::class,
1439
            'listbox'        => Field\Listbox::class,
1440
        ];
1441
1442
        foreach ($map as $abstract => $class) {
1443
            static::extend($abstract, $class);
1444
        }
1445
    }
1446
1447
    /**
1448
     * Register custom field.
1449
     *
1450
     * @param string $abstract
1451
     * @param string $class
1452
     *
1453
     * @return void
1454
     */
1455
    public static function extend($abstract, $class)
1456
    {
1457
        static::$availableFields[$abstract] = $class;
1458
    }
1459
1460
    /**
1461
     * Set form field alias.
1462
     *
1463
     * @param string $field
1464
     * @param string $alias
1465
     *
1466
     * @return void
1467
     */
1468
    public static function alias($field, $alias)
1469
    {
1470
        static::$fieldAlias[$alias] = $field;
1471
    }
1472
1473
    /**
1474
     * Remove registered field.
1475
     *
1476
     * @param array|string $abstract
1477
     */
1478
    public static function forget($abstract)
1479
    {
1480
        array_forget(static::$availableFields, $abstract);
1481
    }
1482
1483
    /**
1484
     * Find field class.
1485
     *
1486
     * @param string $method
1487
     *
1488
     * @return bool|mixed
1489
     */
1490
    public static function findFieldClass($method)
1491
    {
1492
        // If alias exists.
1493
        if (isset(static::$fieldAlias[$method])) {
1494
            $method = static::$fieldAlias[$method];
1495
        }
1496
1497
        $class = array_get(static::$availableFields, $method);
1498
1499
        if (class_exists($class)) {
1500
            return $class;
1501
        }
1502
1503
        return false;
1504
    }
1505
1506
    /**
1507
     * Collect assets required by registered field.
1508
     *
1509
     * @return array
1510
     */
1511
    public static function collectFieldAssets()
1512
    {
1513
        if (!empty(static::$collectedAssets)) {
1514
            return static::$collectedAssets;
1515
        }
1516
1517
        $css = collect();
1518
        $js = collect();
1519
1520
        foreach (static::$availableFields as $field) {
1521
            if (!method_exists($field, 'getAssets')) {
1522
                continue;
1523
            }
1524
1525
            $assets = call_user_func([$field, 'getAssets']);
1526
1527
            $css->push(array_get($assets, 'css'));
1528
            $js->push(array_get($assets, 'js'));
1529
        }
1530
1531
        return static::$collectedAssets = [
1532
            'css' => $css->flatten()->unique()->filter()->toArray(),
1533
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1534
        ];
1535
    }
1536
1537
    /**
1538
     * Getter.
1539
     *
1540
     * @param string $name
1541
     *
1542
     * @return array|mixed
1543
     */
1544
    public function __get($name)
1545
    {
1546
        return $this->input($name);
1547
    }
1548
1549
    /**
1550
     * Setter.
1551
     *
1552
     * @param string $name
1553
     * @param $value
1554
     */
1555
    public function __set($name, $value)
1556
    {
1557
        $this->input($name, $value);
1558
    }
1559
1560
    /**
1561
     * Generate a Field object and add to form builder if Field exists.
1562
     *
1563
     * @param string $method
1564
     * @param array  $arguments
1565
     *
1566
     * @return Field
1567
     */
1568
    public function __call($method, $arguments)
1569
    {
1570
        if ($className = static::findFieldClass($method)) {
1571
            $column = array_get($arguments, 0, ''); //[0];
1572
1573
            $element = new $className($column, array_slice($arguments, 1));
1574
1575
            $this->pushField($element);
1576
1577
            return $element;
1578
        }
1579
1580
        admin_error('Error', "Field type [$method] does not exist.");
1581
1582
        return new Field\Nullable();
1583
    }
1584
}
1585