Completed
Pull Request — master (#3047)
by Nastuzzi
02:29
created

Form::store()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 35

Duplication

Lines 11
Ratio 31.43 %

Importance

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

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

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

Loading history...
360
            $inserts = $this->prepareInsert($this->updates);
361
362
            foreach ($inserts as $column => $value) {
363
                $this->model->setAttribute($column, $value);
364
            }
365
366
            $this->model->save();
367
368
            $this->updateRelation($this->relations);
369
        });
370
371
        if (($response = $this->callSaved()) instanceof Response) {
372
            return $response;
373
        }
374
375
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
376
            return $response;
377
        }
378
379
        return $this->redirectAfterStore();
380
    }
381
382
    /**
383
     * Get ajax response.
384
     *
385
     * @param string $message
386
     *
387
     * @return bool|\Illuminate\Http\JsonResponse
388
     */
389
    protected function ajaxResponse($message)
390
    {
391
        $request = Request::capture();
392
393
        // ajax but not pjax
394
        if ($request->ajax() && !$request->pjax()) {
395
            return response()->json([
0 ignored issues
show
Bug introduced by
The method json does only exist in Illuminate\Contracts\Routing\ResponseFactory, but not in Illuminate\Http\Response.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

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