Completed
Push — master ( 6fbb78...0fea44 )
by Song
02:24
created

Form::ignore()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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