Completed
Pull Request — master (#3054)
by
unknown
02:23
created

Form::callSaved()   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
     * @var Closure
192
     */
193
    protected static $initCallback;
194
195
    /**
196
     * Create a new form instance.
197
     *
198
     * @param $model
199
     * @param \Closure $callback
200
     */
201
    public function __construct($model, Closure $callback = null)
202
    {
203
        $this->model = $model;
204
205
        $this->builder = new Builder($this);
206
207
        if ($callback instanceof Closure) {
208
            $callback($this);
209
        }
210
211
        $this->isSoftDeletes = in_array(SoftDeletes::class, class_uses($this->model));
212
213
        if (static::$initCallback instanceof Closure) {
214
            call_user_func(static::$initCallback, $this);
215
        }
216
    }
217
218
    /**
219
     * Initialize with user pre-defined default disables, etc.
220
     *
221
     * @param Closure $callback
222
     */
223
    public static function init(Closure $callback = null)
224
    {
225
        static::$initCallback = $callback;
226
    }
227
228
    /**
229
     * @param Field $field
230
     *
231
     * @return $this
232
     */
233
    public function pushField(Field $field)
234
    {
235
        $field->setForm($this);
236
237
        $this->builder->fields()->push($field);
238
239
        return $this;
240
    }
241
242
    /**
243
     * @return Model
244
     */
245
    public function model()
246
    {
247
        return $this->model;
248
    }
249
250
    /**
251
     * @return Builder
252
     */
253
    public function builder()
254
    {
255
        return $this->builder;
256
    }
257
258
    /**
259
     * Generate a edit form.
260
     *
261
     * @param $id
262
     *
263
     * @return $this
264
     */
265
    public function edit($id)
266
    {
267
        $this->builder->setMode(Builder::MODE_EDIT);
268
        $this->builder->setResourceId($id);
269
270
        $this->setFieldValue($id);
271
272
        return $this;
273
    }
274
275
    /**
276
     * Use tab to split form.
277
     *
278
     * @param string  $title
279
     * @param Closure $content
280
     *
281
     * @return $this
282
     */
283
    public function tab($title, Closure $content, $active = false)
284
    {
285
        $this->getTab()->append($title, $content, $active);
286
287
        return $this;
288
    }
289
290
    /**
291
     * Get Tab instance.
292
     *
293
     * @return Tab
294
     */
295
    public function getTab()
296
    {
297
        if (is_null($this->tab)) {
298
            $this->tab = new Tab($this);
299
        }
300
301
        return $this->tab;
302
    }
303
304
    /**
305
     * Destroy data entity and remove files.
306
     *
307
     * @param $id
308
     *
309
     * @return mixed
310
     */
311
    public function destroy($id)
312
    {
313
        collect(explode(',', $id))->filter()->each(function ($id) {
314
            $builder = $this->model()->newQuery();
315
316
            if ($this->isSoftDeletes) {
317
                $builder = $builder->withTrashed();
318
            }
319
320
            $model = $builder->with($this->getRelations())->findOrFail($id);
321
322
            if ($this->isSoftDeletes && $model->trashed()) {
323
                $this->deleteFiles($model, true);
324
                $model->forceDelete();
325
326
                return;
327
            }
328
329
            $this->deleteFiles($model);
330
            $model->delete();
331
        });
332
333
        return true;
334
    }
335
336
    /**
337
     * Remove files in record.
338
     *
339
     * @param Model $model
340
     * @param bool  $forceDelete
341
     */
342
    protected function deleteFiles(Model $model, $forceDelete = false)
343
    {
344
        // If it's a soft delete, the files in the data will not be deleted.
345
        if (!$forceDelete && $this->isSoftDeletes) {
346
            return;
347
        }
348
349
        $data = $model->toArray();
350
351
        $this->builder->fields()->filter(function ($field) {
352
            return $field instanceof Field\File;
353
        })->each(function (Field\File $file) use ($data) {
354
            $file->setOriginal($data);
355
356
            $file->destroy();
357
        });
358
    }
359
360
    /**
361
     * Store a new record.
362
     *
363
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
364
     */
365
    public function store()
366
    {
367
        $data = Input::all();
368
369
        // Handle validation errors.
370
        if ($validationMessages = $this->validationMessages($data)) {
371
            return back()->withInput()->withErrors($validationMessages);
372
        }
373
374
        if (($response = $this->prepare($data)) instanceof Response) {
375
            return $response;
376
        }
377
378 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...
379
            $inserts = $this->prepareInsert($this->updates);
380
381
            foreach ($inserts as $column => $value) {
382
                $this->model->setAttribute($column, $value);
383
            }
384
385
            $this->model->save();
386
387
            $this->updateRelation($this->relations);
388
        });
389
390
        if (($response = $this->callSaved()) instanceof Response) {
391
            return $response;
392
        }
393
394
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
395
            return $response;
396
        }
397
398
        return $this->redirectAfterStore();
399
    }
400
401
    /**
402
     * Get ajax response.
403
     *
404
     * @param string $message
405
     *
406
     * @return bool|\Illuminate\Http\JsonResponse
407
     */
408
    protected function ajaxResponse($message)
409
    {
410
        $request = Request::capture();
411
412
        // ajax but not pjax
413
        if ($request->ajax() && !$request->pjax()) {
414
            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...
415
                'status'  => true,
416
                'message' => $message,
417
            ]);
418
        }
419
420
        return false;
421
    }
422
423
    /**
424
     * Prepare input data for insert or update.
425
     *
426
     * @param array $data
427
     *
428
     * @return mixed
429
     */
430
    protected function prepare($data = [])
431
    {
432
        if (($response = $this->callSubmitted()) instanceof Response) {
433
            return $response;
434
        }
435
436
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);
437
438
        if (($response = $this->callSaving()) instanceof Response) {
439
            return $response;
440
        }
441
442
        $this->relations = $this->getRelationInputs($this->inputs);
443
444
        $this->updates = array_except($this->inputs, array_keys($this->relations));
445
    }
446
447
    /**
448
     * Remove ignored fields from input.
449
     *
450
     * @param array $input
451
     *
452
     * @return array
453
     */
454
    protected function removeIgnoredFields($input)
455
    {
456
        array_forget($input, $this->ignored);
457
458
        return $input;
459
    }
460
461
    /**
462
     * Get inputs for relations.
463
     *
464
     * @param array $inputs
465
     *
466
     * @return array
467
     */
468
    protected function getRelationInputs($inputs = [])
469
    {
470
        $relations = [];
471
472
        foreach ($inputs as $column => $value) {
473
            if (method_exists($this->model, $column)) {
474
                $relation = call_user_func([$this->model, $column]);
475
476
                if ($relation instanceof Relations\Relation) {
477
                    $relations[$column] = $value;
478
                }
479
            }
480
        }
481
482
        return $relations;
483
    }
484
485
    /**
486
     * Call editing callbacks.
487
     *
488
     * @return void
489
     */
490
    protected function callEditing()
491
    {
492
        foreach ($this->editing as $func) {
493
            call_user_func($func, $this);
494
        }
495
    }
496
497
    /**
498
     * Call submitted callback.
499
     *
500
     * @return mixed
501
     */
502
    protected function callSubmitted()
503
    {
504
        foreach ($this->submitted as $func) {
505
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
506
                return $ret;
507
            }
508
        }
509
    }
510
511
    /**
512
     * Call saving callback.
513
     *
514
     * @return mixed
515
     */
516
    protected function callSaving()
517
    {
518
        foreach ($this->saving as $func) {
519
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
520
                return $ret;
521
            }
522
        }
523
    }
524
525
    /**
526
     * Callback after saving a Model.
527
     *
528
     * @return mixed|null
529
     */
530
    protected function callSaved()
531
    {
532
        foreach ($this->saved as $func) {
533
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
534
                return $ret;
535
            }
536
        }
537
    }
538
539
    /**
540
     * Handle update.
541
     *
542
     * @param int $id
543
     *
544
     * @return \Symfony\Component\HttpFoundation\Response
545
     */
546
    public function update($id, $data = null)
547
    {
548
        $data = ($data) ?: Input::all();
549
550
        $isEditable = $this->isEditable($data);
551
552
        $data = $this->handleEditable($data);
553
554
        $data = $this->handleFileDelete($data);
555
556
        if ($this->handleOrderable($id, $data)) {
557
            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 557 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
558
                'status'  => true,
559
                'message' => trans('admin.update_succeeded'),
560
            ]);
561
        }
562
563
        /* @var Model $this->model */
564
        $builder = $this->model();
565
566
        if ($this->isSoftDeletes) {
567
            $builder = $builder->withTrashed();
568
        }
569
570
        $this->model = $builder->with($this->getRelations())->findOrFail($id);
571
572
        $this->setFieldOriginalValue();
573
574
        // Handle validation errors.
575
        if ($validationMessages = $this->validationMessages($data)) {
576
            if (!$isEditable) {
577
                return back()->withInput()->withErrors($validationMessages);
578
            } else {
579
                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...
580
            }
581
        }
582
583
        if (($response = $this->prepare($data)) instanceof Response) {
584
            return $response;
585
        }
586
587 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...
588
            $updates = $this->prepareUpdate($this->updates);
589
590
            foreach ($updates as $column => $value) {
591
                /* @var Model $this->model */
592
                $this->model->setAttribute($column, $value);
593
            }
594
595
            $this->model->save();
596
597
            $this->updateRelation($this->relations);
598
        });
599
600
        if (($result = $this->callSaved()) instanceof Response) {
601
            return $result;
602
        }
603
604
        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 605 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
605
            return $response;
606
        }
607
608
        return $this->redirectAfterUpdate($id);
609
    }
610
611
    /**
612
     * Get RedirectResponse after store.
613
     *
614
     * @return \Illuminate\Http\RedirectResponse
615
     */
616
    protected function redirectAfterStore()
617
    {
618
        $resourcesPath = $this->resource(0);
619
620
        $key = $this->model->getKey();
621
622
        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 622 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterStore of type Illuminate\Http\RedirectResponse.
Loading history...
623
    }
624
625
    /**
626
     * Get RedirectResponse after update.
627
     *
628
     * @param mixed $key
629
     *
630
     * @return \Illuminate\Http\RedirectResponse
631
     */
632
    protected function redirectAfterUpdate($key)
633
    {
634
        $resourcesPath = $this->resource(-1);
635
636
        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 636 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterUpdate of type Illuminate\Http\RedirectResponse.
Loading history...
637
    }
638
639
    /**
640
     * Get RedirectResponse after data saving.
641
     *
642
     * @param string $resourcesPath
643
     * @param string $key
644
     *
645
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
646
     */
647
    protected function redirectAfterSaving($resourcesPath, $key)
648
    {
649
        if (request('after-save') == 1) {
650
            // continue editing
651
            $url = rtrim($resourcesPath, '/')."/{$key}/edit";
652
        } elseif (request('after-save') == 2) {
653
            // continue creating
654
            $url = rtrim($resourcesPath, '/').'/create';
655
        } elseif (request('after-save') == 3) {
656
            // view resource
657
            $url = rtrim($resourcesPath, '/')."/{$key}";
658
        } else {
659
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
660
        }
661
662
        admin_toastr(trans('admin.save_succeeded'));
663
664
        return redirect($url);
665
    }
666
667
    /**
668
     * Check if request is from editable.
669
     *
670
     * @param array $input
671
     *
672
     * @return bool
673
     */
674
    protected function isEditable(array $input = [])
675
    {
676
        return array_key_exists('_editable', $input);
677
    }
678
679
    /**
680
     * Handle editable update.
681
     *
682
     * @param array $input
683
     *
684
     * @return array
685
     */
686
    protected function handleEditable(array $input = [])
687
    {
688
        if (array_key_exists('_editable', $input)) {
689
            $name = $input['name'];
690
            $value = $input['value'];
691
692
            array_forget($input, ['pk', 'value', 'name']);
693
            array_set($input, $name, $value);
694
        }
695
696
        return $input;
697
    }
698
699
    /**
700
     * @param array $input
701
     *
702
     * @return array
703
     */
704
    protected function handleFileDelete(array $input = [])
705
    {
706
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
707
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
708
            unset($input['key']);
709
        }
710
711
        Input::replace($input);
712
713
        return $input;
714
    }
715
716
    /**
717
     * Handle orderable update.
718
     *
719
     * @param int   $id
720
     * @param array $input
721
     *
722
     * @return bool
723
     */
724
    protected function handleOrderable($id, array $input = [])
725
    {
726
        if (array_key_exists('_orderable', $input)) {
727
            $model = $this->model->find($id);
728
729
            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...
730
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
731
732
                return true;
733
            }
734
        }
735
736
        return false;
737
    }
738
739
    /**
740
     * Update relation data.
741
     *
742
     * @param array $relationsData
743
     *
744
     * @return void
745
     */
746
    protected function updateRelation($relationsData)
747
    {
748
        foreach ($relationsData as $name => $values) {
749
            if (!method_exists($this->model, $name)) {
750
                continue;
751
            }
752
753
            $relation = $this->model->$name();
754
755
            $oneToOneRelation = $relation instanceof Relations\HasOne
756
                || $relation instanceof Relations\MorphOne
757
                || $relation instanceof Relations\BelongsTo;
758
759
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
760
761
            if (empty($prepared)) {
762
                continue;
763
            }
764
765
            switch (true) {
766
                case $relation instanceof Relations\BelongsToMany:
767
                case $relation instanceof Relations\MorphToMany:
768
                    if (isset($prepared[$name])) {
769
                        $relation->sync($prepared[$name]);
770
                    }
771
                    break;
772
                case $relation instanceof Relations\HasOne:
773
774
                    $related = $this->model->$name;
775
776
                    // if related is empty
777
                    if (is_null($related)) {
778
                        $related = $relation->getRelated();
779
                        $qualifiedParentKeyName = $relation->getQualifiedParentKeyName();
780
                        $localKey = array_last(explode('.', $qualifiedParentKeyName));
781
                        $related->{$relation->getForeignKeyName()} = $this->model->{$localKey};
782
                    }
783
784
                    foreach ($prepared[$name] as $column => $value) {
785
                        $related->setAttribute($column, $value);
786
                    }
787
788
                    $related->save();
789
                    break;
790
                case $relation instanceof Relations\BelongsTo:
791
792
                    $parent = $this->model->$name;
793
794
                    // if related is empty
795
                    if (is_null($parent)) {
796
                        $parent = $relation->getRelated();
797
                    }
798
799
                    foreach ($prepared[$name] as $column => $value) {
800
                        $parent->setAttribute($column, $value);
801
                    }
802
803
                    $parent->save();
804
805
                    // When in creating, associate two models
806
                    if (!$this->model->{$relation->getForeignKey()}) {
807
                        $this->model->{$relation->getForeignKey()} = $parent->getKey();
808
809
                        $this->model->save();
810
                    }
811
812
                    break;
813
                case $relation instanceof Relations\MorphOne:
814
                    $related = $this->model->$name;
815
                    if (is_null($related)) {
816
                        $related = $relation->make();
817
                    }
818
                    foreach ($prepared[$name] as $column => $value) {
819
                        $related->setAttribute($column, $value);
820
                    }
821
                    $related->save();
822
                    break;
823
                case $relation instanceof Relations\HasMany:
824
                case $relation instanceof Relations\MorphMany:
825
826
                    foreach ($prepared[$name] as $related) {
827
                        /** @var Relations\Relation $relation */
828
                        $relation = $this->model()->$name();
829
830
                        $keyName = $relation->getRelated()->getKeyName();
831
832
                        $instance = $relation->findOrNew(array_get($related, $keyName));
833
834
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
835
                            $instance->delete();
836
837
                            continue;
838
                        }
839
840
                        array_forget($related, static::REMOVE_FLAG_NAME);
841
842
                        $instance->fill($related);
843
844
                        $instance->save();
845
                    }
846
847
                    break;
848
            }
849
        }
850
    }
851
852
    /**
853
     * Prepare input data for update.
854
     *
855
     * @param array $updates
856
     * @param bool  $oneToOneRelation If column is one-to-one relation.
857
     *
858
     * @return array
859
     */
860
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
861
    {
862
        $prepared = [];
863
864
        /** @var Field $field */
865
        foreach ($this->builder->fields() as $field) {
866
            $columns = $field->column();
867
868
            // If column not in input array data, then continue.
869
            if (!array_has($updates, $columns)) {
870
                continue;
871
            }
872
873
            if ($this->invalidColumn($columns, $oneToOneRelation)) {
874
                continue;
875
            }
876
877
            $value = $this->getDataByColumn($updates, $columns);
878
879
            $value = $field->prepare($value);
880
881 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...
882
                foreach ($columns as $name => $column) {
883
                    array_set($prepared, $column, $value[$name]);
884
                }
885
            } elseif (is_string($columns)) {
886
                array_set($prepared, $columns, $value);
887
            }
888
        }
889
890
        return $prepared;
891
    }
892
893
    /**
894
     * @param string|array $columns
895
     * @param bool         $oneToOneRelation
896
     *
897
     * @return bool
898
     */
899
    protected function invalidColumn($columns, $oneToOneRelation = false)
900
    {
901
        foreach ((array) $columns as $column) {
902
            if ((!$oneToOneRelation && Str::contains($column, '.')) ||
903
                ($oneToOneRelation && !Str::contains($column, '.'))) {
904
                return true;
905
            }
906
        }
907
908
        return false;
909
    }
910
911
    /**
912
     * Prepare input data for insert.
913
     *
914
     * @param $inserts
915
     *
916
     * @return array
917
     */
918
    protected function prepareInsert($inserts)
919
    {
920
        if ($this->isHasOneRelation($inserts)) {
921
            $inserts = array_dot($inserts);
922
        }
923
924
        foreach ($inserts as $column => $value) {
925
            if (is_null($field = $this->getFieldByColumn($column))) {
926
                unset($inserts[$column]);
927
                continue;
928
            }
929
930
            $inserts[$column] = $field->prepare($value);
931
        }
932
933
        $prepared = [];
934
935
        foreach ($inserts as $key => $value) {
936
            array_set($prepared, $key, $value);
937
        }
938
939
        return $prepared;
940
    }
941
942
    /**
943
     * Is input data is has-one relation.
944
     *
945
     * @param array $inserts
946
     *
947
     * @return bool
948
     */
949
    protected function isHasOneRelation($inserts)
950
    {
951
        $first = current($inserts);
952
953
        if (!is_array($first)) {
954
            return false;
955
        }
956
957
        if (is_array(current($first))) {
958
            return false;
959
        }
960
961
        return Arr::isAssoc($first);
962
    }
963
964
    /**
965
     * Set after getting editing model callback.
966
     *
967
     * @param Closure $callback
968
     *
969
     * @return void
970
     */
971
    public function editing(Closure $callback)
972
    {
973
        $this->editing[] = $callback;
974
    }
975
976
    /**
977
     * Set submitted callback.
978
     *
979
     * @param Closure $callback
980
     *
981
     * @return void
982
     */
983
    public function submitted(Closure $callback)
984
    {
985
        $this->submitted[] = $callback;
986
    }
987
988
    /**
989
     * Set saving callback.
990
     *
991
     * @param Closure $callback
992
     *
993
     * @return void
994
     */
995
    public function saving(Closure $callback)
996
    {
997
        $this->saving[] = $callback;
998
    }
999
1000
    /**
1001
     * Set saved callback.
1002
     *
1003
     * @param Closure $callback
1004
     *
1005
     * @return void
1006
     */
1007
    public function saved(Closure $callback)
1008
    {
1009
        $this->saved[] = $callback;
1010
    }
1011
1012
    /**
1013
     * Ignore fields to save.
1014
     *
1015
     * @param string|array $fields
1016
     *
1017
     * @return $this
1018
     */
1019
    public function ignore($fields)
1020
    {
1021
        $this->ignored = array_merge($this->ignored, (array) $fields);
1022
1023
        return $this;
1024
    }
1025
1026
    /**
1027
     * @param array        $data
1028
     * @param string|array $columns
1029
     *
1030
     * @return array|mixed
1031
     */
1032 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...
1033
    {
1034
        if (is_string($columns)) {
1035
            return array_get($data, $columns);
1036
        }
1037
1038
        if (is_array($columns)) {
1039
            $value = [];
1040
            foreach ($columns as $name => $column) {
1041
                if (!array_has($data, $column)) {
1042
                    continue;
1043
                }
1044
                $value[$name] = array_get($data, $column);
1045
            }
1046
1047
            return $value;
1048
        }
1049
    }
1050
1051
    /**
1052
     * Find field object by column.
1053
     *
1054
     * @param $column
1055
     *
1056
     * @return mixed
1057
     */
1058
    protected function getFieldByColumn($column)
1059
    {
1060
        return $this->builder->fields()->first(
1061
            function (Field $field) use ($column) {
1062
                if (is_array($field->column())) {
1063
                    return in_array($column, $field->column());
1064
                }
1065
1066
                return $field->column() == $column;
1067
            }
1068
        );
1069
    }
1070
1071
    /**
1072
     * Set original data for each field.
1073
     *
1074
     * @return void
1075
     */
1076
    protected function setFieldOriginalValue()
1077
    {
1078
//        static::doNotSnakeAttributes($this->model);
1079
1080
        $values = $this->model->toArray();
1081
1082
        $this->builder->fields()->each(function (Field $field) use ($values) {
1083
            $field->setOriginal($values);
1084
        });
1085
    }
1086
1087
    /**
1088
     * Set all fields value in form.
1089
     *
1090
     * @param $id
1091
     *
1092
     * @return void
1093
     */
1094
    protected function setFieldValue($id)
1095
    {
1096
        $relations = $this->getRelations();
1097
1098
        $builder = $this->model();
1099
1100
        if ($this->isSoftDeletes) {
1101
            $builder = $builder->withTrashed();
1102
        }
1103
1104
        $this->model = $builder->with($relations)->findOrFail($id);
1105
1106
        $this->callEditing();
1107
1108
//        static::doNotSnakeAttributes($this->model);
1109
1110
        $data = $this->model->toArray();
1111
1112
        $this->builder->fields()->each(function (Field $field) use ($data) {
1113
            if (!in_array($field->column(), $this->ignored)) {
1114
                $field->fill($data);
1115
            }
1116
        });
1117
    }
1118
1119
    /**
1120
     * Don't snake case attributes.
1121
     *
1122
     * @param Model $model
1123
     *
1124
     * @return void
1125
     */
1126
    protected static function doNotSnakeAttributes(Model $model)
1127
    {
1128
        $class = get_class($model);
1129
1130
        $class::$snakeAttributes = false;
1131
    }
1132
1133
    /**
1134
     * Get validation messages.
1135
     *
1136
     * @param array $input
1137
     *
1138
     * @return MessageBag|bool
1139
     */
1140
    public function validationMessages($input)
1141
    {
1142
        $failedValidators = [];
1143
1144
        /** @var Field $field */
1145
        foreach ($this->builder->fields() as $field) {
1146
            if (!$validator = $field->getValidator($input)) {
1147
                continue;
1148
            }
1149
1150
            if (($validator instanceof Validator) && !$validator->passes()) {
1151
                $failedValidators[] = $validator;
1152
            }
1153
        }
1154
1155
        $message = $this->mergeValidationMessages($failedValidators);
1156
1157
        return $message->any() ? $message : false;
1158
    }
1159
1160
    /**
1161
     * Merge validation messages from input validators.
1162
     *
1163
     * @param \Illuminate\Validation\Validator[] $validators
1164
     *
1165
     * @return MessageBag
1166
     */
1167
    protected function mergeValidationMessages($validators)
1168
    {
1169
        $messageBag = new MessageBag();
1170
1171
        foreach ($validators as $validator) {
1172
            $messageBag = $messageBag->merge($validator->messages());
1173
        }
1174
1175
        return $messageBag;
1176
    }
1177
1178
    /**
1179
     * Get all relations of model from callable.
1180
     *
1181
     * @return array
1182
     */
1183
    public function getRelations()
1184
    {
1185
        $relations = $columns = [];
1186
1187
        /** @var Field $field */
1188
        foreach ($this->builder->fields() as $field) {
1189
            $columns[] = $field->column();
1190
        }
1191
1192
        foreach (array_flatten($columns) as $column) {
1193
            if (str_contains($column, '.')) {
1194
                list($relation) = explode('.', $column);
1195
1196
                if (method_exists($this->model, $relation) &&
1197
                    $this->model->$relation() instanceof Relations\Relation
1198
                ) {
1199
                    $relations[] = $relation;
1200
                }
1201
            } elseif (method_exists($this->model, $column) &&
1202
                !method_exists(Model::class, $column)
1203
            ) {
1204
                $relations[] = $column;
1205
            }
1206
        }
1207
1208
        return array_unique($relations);
1209
    }
1210
1211
    /**
1212
     * Set action for form.
1213
     *
1214
     * @param string $action
1215
     *
1216
     * @return $this
1217
     */
1218
    public function setAction($action)
1219
    {
1220
        $this->builder()->setAction($action);
1221
1222
        return $this;
1223
    }
1224
1225
    /**
1226
     * Set field and label width in current form.
1227
     *
1228
     * @param int $fieldWidth
1229
     * @param int $labelWidth
1230
     *
1231
     * @return $this
1232
     */
1233
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1234
    {
1235
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1236
            /* @var Field $field  */
1237
            $field->setWidth($fieldWidth, $labelWidth);
1238
        });
1239
1240
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1241
1242
        return $this;
1243
    }
1244
1245
    /**
1246
     * Set view for form.
1247
     *
1248
     * @param string $view
1249
     *
1250
     * @return $this
1251
     */
1252
    public function setView($view)
1253
    {
1254
        $this->builder()->setView($view);
1255
1256
        return $this;
1257
    }
1258
1259
    /**
1260
     * Set title for form.
1261
     *
1262
     * @param string $title
1263
     *
1264
     * @return $this
1265
     */
1266
    public function setTitle($title = '')
1267
    {
1268
        $this->builder()->setTitle($title);
1269
1270
        return $this;
1271
    }
1272
1273
    /**
1274
     * Add a row in form.
1275
     *
1276
     * @param Closure $callback
1277
     *
1278
     * @return $this
1279
     */
1280
    public function row(Closure $callback)
1281
    {
1282
        $this->rows[] = new Row($callback, $this);
1283
1284
        return $this;
1285
    }
1286
1287
    /**
1288
     * Tools setting for form.
1289
     *
1290
     * @param Closure $callback
1291
     */
1292
    public function tools(Closure $callback)
1293
    {
1294
        $callback->call($this, $this->builder->getTools());
1295
    }
1296
1297
    /**
1298
     * Disable form submit.
1299
     *
1300
     * @return $this
1301
     *
1302
     * @deprecated
1303
     */
1304
    public function disableSubmit(bool $disable = true)
1305
    {
1306
        $this->builder()->getFooter()->disableSubmit($disable);
1307
1308
        return $this;
1309
    }
1310
1311
    /**
1312
     * Disable form reset.
1313
     *
1314
     * @return $this
1315
     *
1316
     * @deprecated
1317
     */
1318
    public function disableReset(bool $disable = true)
1319
    {
1320
        $this->builder()->getFooter()->disableReset($disable);
1321
1322
        return $this;
1323
    }
1324
1325
    /**
1326
     * Disable View Checkbox on footer.
1327
     *
1328
     * @return $this
1329
     */
1330
    public function disableViewCheck(bool $disable = true)
1331
    {
1332
        $this->builder()->getFooter()->disableViewCheck($disable);
1333
1334
        return $this;
1335
    }
1336
1337
    /**
1338
     * Disable Editing Checkbox on footer.
1339
     *
1340
     * @return $this
1341
     */
1342
    public function disableEditingCheck(bool $disable = true)
1343
    {
1344
        $this->builder()->getFooter()->disableEditingCheck($disable);
1345
1346
        return $this;
1347
    }
1348
1349
    /**
1350
     * Disable Creating Checkbox on footer.
1351
     *
1352
     * @return $this
1353
     */
1354
    public function disableCreatingCheck(bool $disable = true)
1355
    {
1356
        $this->builder()->getFooter()->disableCreatingCheck($disable);
1357
1358
        return $this;
1359
    }
1360
1361
    /**
1362
     * Footer setting for form.
1363
     *
1364
     * @param Closure $callback
1365
     */
1366
    public function footer(Closure $callback)
1367
    {
1368
        call_user_func($callback, $this->builder()->getFooter());
1369
    }
1370
1371
    /**
1372
     * Get current resource route url.
1373
     *
1374
     * @param int $slice
1375
     *
1376
     * @return string
1377
     */
1378
    public function resource($slice = -2)
1379
    {
1380
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1381
1382
        if ($slice != 0) {
1383
            $segments = array_slice($segments, 0, $slice);
1384
        }
1385
1386
        return implode('/', $segments);
1387
    }
1388
1389
    /**
1390
     * Render the form contents.
1391
     *
1392
     * @return string
1393
     */
1394
    public function render()
1395
    {
1396
        try {
1397
            return $this->builder->render();
1398
        } catch (\Exception $e) {
1399
            return Handler::renderException($e);
1400
        }
1401
    }
1402
1403
    /**
1404
     * Get or set input data.
1405
     *
1406
     * @param string $key
1407
     * @param null   $value
1408
     *
1409
     * @return array|mixed
1410
     */
1411
    public function input($key, $value = null)
1412
    {
1413
        if (is_null($value)) {
1414
            return array_get($this->inputs, $key);
1415
        }
1416
1417
        return array_set($this->inputs, $key, $value);
1418
    }
1419
1420
    /**
1421
     * Register builtin fields.
1422
     *
1423
     * @return void
1424
     */
1425
    public static function registerBuiltinFields()
1426
    {
1427
        $map = [
1428
            'button'         => Field\Button::class,
1429
            'checkbox'       => Field\Checkbox::class,
1430
            'color'          => Field\Color::class,
1431
            'currency'       => Field\Currency::class,
1432
            'date'           => Field\Date::class,
1433
            'dateRange'      => Field\DateRange::class,
1434
            'datetime'       => Field\Datetime::class,
1435
            'dateTimeRange'  => Field\DatetimeRange::class,
1436
            'datetimeRange'  => Field\DatetimeRange::class,
1437
            'decimal'        => Field\Decimal::class,
1438
            'display'        => Field\Display::class,
1439
            'divider'        => Field\Divide::class,
1440
            'divide'         => Field\Divide::class,
1441
            'embeds'         => Field\Embeds::class,
1442
            'editor'         => Field\Editor::class,
1443
            'email'          => Field\Email::class,
1444
            'file'           => Field\File::class,
1445
            'hasMany'        => Field\HasMany::class,
1446
            'hidden'         => Field\Hidden::class,
1447
            'id'             => Field\Id::class,
1448
            'image'          => Field\Image::class,
1449
            'ip'             => Field\Ip::class,
1450
            'map'            => Field\Map::class,
1451
            'mobile'         => Field\Mobile::class,
1452
            'month'          => Field\Month::class,
1453
            'multipleSelect' => Field\MultipleSelect::class,
1454
            'number'         => Field\Number::class,
1455
            'password'       => Field\Password::class,
1456
            'radio'          => Field\Radio::class,
1457
            'rate'           => Field\Rate::class,
1458
            'select'         => Field\Select::class,
1459
            'slider'         => Field\Slider::class,
1460
            'switch'         => Field\SwitchField::class,
1461
            'text'           => Field\Text::class,
1462
            'textarea'       => Field\Textarea::class,
1463
            'time'           => Field\Time::class,
1464
            'timeRange'      => Field\TimeRange::class,
1465
            'url'            => Field\Url::class,
1466
            'year'           => Field\Year::class,
1467
            'html'           => Field\Html::class,
1468
            'tags'           => Field\Tags::class,
1469
            'icon'           => Field\Icon::class,
1470
            'multipleFile'   => Field\MultipleFile::class,
1471
            'multipleImage'  => Field\MultipleImage::class,
1472
            'captcha'        => Field\Captcha::class,
1473
            'listbox'        => Field\Listbox::class,
1474
        ];
1475
1476
        foreach ($map as $abstract => $class) {
1477
            static::extend($abstract, $class);
1478
        }
1479
    }
1480
1481
    /**
1482
     * Register custom field.
1483
     *
1484
     * @param string $abstract
1485
     * @param string $class
1486
     *
1487
     * @return void
1488
     */
1489
    public static function extend($abstract, $class)
1490
    {
1491
        static::$availableFields[$abstract] = $class;
1492
    }
1493
1494
    /**
1495
     * Set form field alias.
1496
     *
1497
     * @param string $field
1498
     * @param string $alias
1499
     *
1500
     * @return void
1501
     */
1502
    public static function alias($field, $alias)
1503
    {
1504
        static::$fieldAlias[$alias] = $field;
1505
    }
1506
1507
    /**
1508
     * Remove registered field.
1509
     *
1510
     * @param array|string $abstract
1511
     */
1512
    public static function forget($abstract)
1513
    {
1514
        array_forget(static::$availableFields, $abstract);
1515
    }
1516
1517
    /**
1518
     * Find field class.
1519
     *
1520
     * @param string $method
1521
     *
1522
     * @return bool|mixed
1523
     */
1524
    public static function findFieldClass($method)
1525
    {
1526
        // If alias exists.
1527
        if (isset(static::$fieldAlias[$method])) {
1528
            $method = static::$fieldAlias[$method];
1529
        }
1530
1531
        $class = array_get(static::$availableFields, $method);
1532
1533
        if (class_exists($class)) {
1534
            return $class;
1535
        }
1536
1537
        return false;
1538
    }
1539
1540
    /**
1541
     * Collect assets required by registered field.
1542
     *
1543
     * @return array
1544
     */
1545
    public static function collectFieldAssets()
1546
    {
1547
        if (!empty(static::$collectedAssets)) {
1548
            return static::$collectedAssets;
1549
        }
1550
1551
        $css = collect();
1552
        $js = collect();
1553
1554
        foreach (static::$availableFields as $field) {
1555
            if (!method_exists($field, 'getAssets')) {
1556
                continue;
1557
            }
1558
1559
            $assets = call_user_func([$field, 'getAssets']);
1560
1561
            $css->push(array_get($assets, 'css'));
1562
            $js->push(array_get($assets, 'js'));
1563
        }
1564
1565
        return static::$collectedAssets = [
1566
            'css' => $css->flatten()->unique()->filter()->toArray(),
1567
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1568
        ];
1569
    }
1570
1571
    /**
1572
     * Getter.
1573
     *
1574
     * @param string $name
1575
     *
1576
     * @return array|mixed
1577
     */
1578
    public function __get($name)
1579
    {
1580
        return $this->input($name);
1581
    }
1582
1583
    /**
1584
     * Setter.
1585
     *
1586
     * @param string $name
1587
     * @param $value
1588
     */
1589
    public function __set($name, $value)
1590
    {
1591
        return array_set($this->inputs, $name, $value);
1592
    }
1593
1594
    /**
1595
     * Generate a Field object and add to form builder if Field exists.
1596
     *
1597
     * @param string $method
1598
     * @param array  $arguments
1599
     *
1600
     * @return Field
1601
     */
1602
    public function __call($method, $arguments)
1603
    {
1604
        if ($className = static::findFieldClass($method)) {
1605
            $column = array_get($arguments, 0, ''); //[0];
1606
1607
            $element = new $className($column, array_slice($arguments, 1));
1608
1609
            $this->pushField($element);
1610
1611
            return $element;
1612
        }
1613
1614
        admin_error('Error', "Field type [$method] does not exist.");
1615
1616
        return new Field\Nullable();
1617
    }
1618
}
1619