Completed
Push — master ( d56cf9...a8f923 )
by Song
02:35
created

Form::isSoftDeletes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin;
4
5
use Closure;
6
use Encore\Admin\Exception\Handler;
7
use Encore\Admin\Form\Builder;
8
use Encore\Admin\Form\Field;
9
use Encore\Admin\Form\Row;
10
use Encore\Admin\Form\Tab;
11
use Illuminate\Contracts\Support\Renderable;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Relations;
14
use Illuminate\Database\Eloquent\SoftDeletes;
15
use Illuminate\Http\Request;
16
use Illuminate\Support\Arr;
17
use Illuminate\Support\Facades\DB;
18
use Illuminate\Support\Facades\Input;
19
use Illuminate\Support\MessageBag;
20
use Illuminate\Support\Str;
21
use Illuminate\Validation\Validator;
22
use Spatie\EloquentSortable\Sortable;
23
use Symfony\Component\HttpFoundation\Response;
24
25
/**
26
 * Class Form.
27
 *
28
 * @method Field\Text           text($column, $label = '')
29
 * @method Field\Checkbox       checkbox($column, $label = '')
30
 * @method Field\Radio          radio($column, $label = '')
31
 * @method Field\Select         select($column, $label = '')
32
 * @method Field\MultipleSelect multipleSelect($column, $label = '')
33
 * @method Field\Textarea       textarea($column, $label = '')
34
 * @method Field\Hidden         hidden($column, $label = '')
35
 * @method Field\Id             id($column, $label = '')
36
 * @method Field\Ip             ip($column, $label = '')
37
 * @method Field\Url            url($column, $label = '')
38
 * @method Field\Color          color($column, $label = '')
39
 * @method Field\Email          email($column, $label = '')
40
 * @method Field\Mobile         mobile($column, $label = '')
41
 * @method Field\Slider         slider($column, $label = '')
42
 * @method Field\Map            map($latitude, $longitude, $label = '')
43
 * @method Field\Editor         editor($column, $label = '')
44
 * @method Field\File           file($column, $label = '')
45
 * @method Field\Image          image($column, $label = '')
46
 * @method Field\Date           date($column, $label = '')
47
 * @method Field\Datetime       datetime($column, $label = '')
48
 * @method Field\Time           time($column, $label = '')
49
 * @method Field\Year           year($column, $label = '')
50
 * @method Field\Month          month($column, $label = '')
51
 * @method Field\DateRange      dateRange($start, $end, $label = '')
52
 * @method Field\DateTimeRange  datetimeRange($start, $end, $label = '')
53
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
54
 * @method Field\Number         number($column, $label = '')
55
 * @method Field\Currency       currency($column, $label = '')
56
 * @method Field\HasMany        hasMany($relationName, $callback)
57
 * @method Field\SwitchField    switch($column, $label = '')
58
 * @method Field\Display        display($column, $label = '')
59
 * @method Field\Rate           rate($column, $label = '')
60
 * @method Field\Divide         divider()
61
 * @method Field\Password       password($column, $label = '')
62
 * @method Field\Decimal        decimal($column, $label = '')
63
 * @method Field\Html           html($html, $label = '')
64
 * @method Field\Tags           tags($column, $label = '')
65
 * @method Field\Icon           icon($column, $label = '')
66
 * @method Field\Embeds         embeds($column, $label = '')
67
 * @method Field\MultipleImage  multipleImage($column, $label = '')
68
 * @method Field\MultipleFile   multipleFile($column, $label = '')
69
 * @method Field\Captcha        captcha($column, $label = '')
70
 * @method Field\Listbox        listbox($column, $label = '')
71
 */
72
class Form implements Renderable
73
{
74
    /**
75
     * Eloquent model of the form.
76
     *
77
     * @var Model
78
     */
79
    protected $model;
80
81
    /**
82
     * @var \Illuminate\Validation\Validator
83
     */
84
    protected $validator;
85
86
    /**
87
     * @var Builder
88
     */
89
    protected $builder;
90
91
    /**
92
     * Submitted callback.
93
     *
94
     * @var Closure[]
95
     */
96
    protected $submitted = [];
97
98
    /**
99
     * Saving callback.
100
     *
101
     * @var Closure[]
102
     */
103
    protected $saving = [];
104
105
    /**
106
     * Saved callback.
107
     *
108
     * @var Closure[]
109
     */
110
    protected $saved = [];
111
112
    /**
113
     * Callbacks after getting editing model.
114
     *
115
     * @var Closure[]
116
     */
117
    protected $editing = [];
118
119
    /**
120
     * Data for save to current model from input.
121
     *
122
     * @var array
123
     */
124
    protected $updates = [];
125
126
    /**
127
     * Data for save to model's relations from input.
128
     *
129
     * @var array
130
     */
131
    protected $relations = [];
132
133
    /**
134
     * Input data.
135
     *
136
     * @var array
137
     */
138
    protected $inputs = [];
139
140
    /**
141
     * Available fields.
142
     *
143
     * @var array
144
     */
145
    public static $availableFields = [];
146
147
    /**
148
     * Form field alias.
149
     *
150
     * @var array
151
     */
152
    public static $fieldAlias = [];
153
154
    /**
155
     * Ignored saving fields.
156
     *
157
     * @var array
158
     */
159
    protected $ignored = [];
160
161
    /**
162
     * Collected field assets.
163
     *
164
     * @var array
165
     */
166
    protected static $collectedAssets = [];
167
168
    /**
169
     * @var Form\Tab
170
     */
171
    protected $tab = null;
172
173
    /**
174
     * Remove flag in `has many` form.
175
     */
176
    const REMOVE_FLAG_NAME = '_remove_';
177
178
    /**
179
     * Field rows in form.
180
     *
181
     * @var array
182
     */
183
    public $rows = [];
184
185
    /**
186
     * @var bool
187
     */
188
    protected $isSoftDeletes = false;
189
190
    /**
191
     * Create a new form instance.
192
     *
193
     * @param $model
194
     * @param \Closure $callback
195
     */
196
    public function __construct($model, Closure $callback = null)
197
    {
198
        $this->model = $model;
199
200
        $this->builder = new Builder($this);
201
202
        if ($callback instanceof Closure) {
203
            $callback($this);
204
        }
205
206
        $this->isSoftDeletes = in_array(SoftDeletes::class, class_uses($this->model));
207
    }
208
209
    /**
210
     * @param Field $field
211
     *
212
     * @return $this
213
     */
214
    public function pushField(Field $field)
215
    {
216
        $field->setForm($this);
217
218
        $this->builder->fields()->push($field);
219
220
        return $this;
221
    }
222
223
    /**
224
     * @return Model
225
     */
226
    public function model()
227
    {
228
        return $this->model;
229
    }
230
231
    /**
232
     * @return Builder
233
     */
234
    public function builder()
235
    {
236
        return $this->builder;
237
    }
238
239
    /**
240
     * Generate a edit form.
241
     *
242
     * @param $id
243
     *
244
     * @return $this
245
     */
246
    public function edit($id)
247
    {
248
        $this->builder->setMode(Builder::MODE_EDIT);
249
        $this->builder->setResourceId($id);
250
251
        $this->setFieldValue($id);
252
253
        return $this;
254
    }
255
256
    /**
257
     * Use tab to split form.
258
     *
259
     * @param string  $title
260
     * @param Closure $content
261
     *
262
     * @return $this
263
     */
264
    public function tab($title, Closure $content, $active = false)
265
    {
266
        $this->getTab()->append($title, $content, $active);
267
268
        return $this;
269
    }
270
271
    /**
272
     * Get Tab instance.
273
     *
274
     * @return Tab
275
     */
276
    public function getTab()
277
    {
278
        if (is_null($this->tab)) {
279
            $this->tab = new Tab($this);
280
        }
281
282
        return $this->tab;
283
    }
284
285
    /**
286
     * Destroy data entity and remove files.
287
     *
288
     * @param $id
289
     *
290
     * @return mixed
291
     */
292
    public function destroy($id)
293
    {
294
        collect(explode(',', $id))->filter()->each(function ($id) {
295
296
            $builder = $this->model()->newQuery();
297
298
            if ($this->isSoftDeletes) {
299
                $builder = $builder->withTrashed();
300
            }
301
302
            $model = $builder->with($this->getRelations())->findOrFail($id);
303
304
            if ($this->isSoftDeletes && $model->trashed()) {
305
                $this->deleteFiles($model, true);
306
                $model->forceDelete();
307
308
                return;
309
            }
310
311
            $this->deleteFiles($model);
312
            $model->delete();
313
        });
314
315
        return true;
316
    }
317
318
    /**
319
     * Remove files in record.
320
     *
321
     * @param Model $model
322
     * @param bool  $forceDelete
323
     */
324
    protected function deleteFiles(Model $model, $forceDelete = false)
325
    {
326
        // If it's a soft delete, the files in the data will not be deleted.
327
        if (!$forceDelete && $this->isSoftDeletes) {
328
            return;
329
        }
330
331
        $data = $model->toArray();
332
333
        $this->builder->fields()->filter(function ($field) {
334
            return $field instanceof Field\File;
335
        })->each(function (Field\File $file) use ($data) {
336
            $file->setOriginal($data);
337
338
            $file->destroy();
339
        });
340
    }
341
342
    /**
343
     * Store a new record.
344
     *
345
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
346
     */
347
    public function store()
348
    {
349
        $data = Input::all();
350
351
        // Handle validation errors.
352
        if ($validationMessages = $this->validationMessages($data)) {
353
            return back()->withInput()->withErrors($validationMessages);
354
        }
355
356
        if (($response = $this->prepare($data)) instanceof Response) {
357
            return $response;
358
        }
359
360 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...
361
            $inserts = $this->prepareInsert($this->updates);
362
363
            foreach ($inserts as $column => $value) {
364
                $this->model->setAttribute($column, $value);
365
            }
366
367
            $this->model->save();
368
369
            $this->updateRelation($this->relations);
370
        });
371
372
        if (($response = $this->callSaved()) instanceof Response) {
373
            return $response;
374
        }
375
376
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
377
            return $response;
378
        }
379
380
        return $this->redirectAfterStore();
381
    }
382
383
    /**
384
     * Get ajax response.
385
     *
386
     * @param string $message
387
     *
388
     * @return bool|\Illuminate\Http\JsonResponse
389
     */
390
    protected function ajaxResponse($message)
391
    {
392
        $request = Request::capture();
393
394
        // ajax but not pjax
395
        if ($request->ajax() && !$request->pjax()) {
396
            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...
397
                'status'  => true,
398
                'message' => $message,
399
            ]);
400
        }
401
402
        return false;
403
    }
404
405
    /**
406
     * Prepare input data for insert or update.
407
     *
408
     * @param array $data
409
     *
410
     * @return mixed
411
     */
412
    protected function prepare($data = [])
413
    {
414
        if (($response = $this->callSubmitted()) instanceof Response) {
415
            return $response;
416
        }
417
418
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);
419
420
        if (($response = $this->callSaving()) instanceof Response) {
421
            return $response;
422
        }
423
424
        $this->relations = $this->getRelationInputs($this->inputs);
425
426
        $this->updates = array_except($this->inputs, array_keys($this->relations));
427
    }
428
429
    /**
430
     * Remove ignored fields from input.
431
     *
432
     * @param array $input
433
     *
434
     * @return array
435
     */
436
    protected function removeIgnoredFields($input)
437
    {
438
        array_forget($input, $this->ignored);
439
440
        return $input;
441
    }
442
443
    /**
444
     * Get inputs for relations.
445
     *
446
     * @param array $inputs
447
     *
448
     * @return array
449
     */
450
    protected function getRelationInputs($inputs = [])
451
    {
452
        $relations = [];
453
454
        foreach ($inputs as $column => $value) {
455
            if (method_exists($this->model, $column)) {
456
                $relation = call_user_func([$this->model, $column]);
457
458
                if ($relation instanceof Relations\Relation) {
459
                    $relations[$column] = $value;
460
                }
461
            }
462
        }
463
464
        return $relations;
465
    }
466
467
    /**
468
     * Call editing callbacks.
469
     *
470
     * @return void
471
     */
472
    protected function callEditing()
473
    {
474
        foreach ($this->editing as $func) {
475
            call_user_func($func, $this);
476
        }
477
    }
478
479
    /**
480
     * Call submitted callback.
481
     *
482
     * @return mixed
483
     */
484
    protected function callSubmitted()
485
    {
486
        foreach ($this->submitted as $func) {
487
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
488
                return $ret;
489
            }
490
        }
491
    }
492
493
    /**
494
     * Call saving callback.
495
     *
496
     * @return mixed
497
     */
498
    protected function callSaving()
499
    {
500
        foreach ($this->saving as $func) {
501
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
502
                return $ret;
503
            }
504
        }
505
    }
506
507
    /**
508
     * Callback after saving a Model.
509
     *
510
     * @return mixed|null
511
     */
512
    protected function callSaved()
513
    {
514
        foreach ($this->saved as $func) {
515
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
516
                return $ret;
517
            }
518
        }
519
    }
520
521
    /**
522
     * Handle update.
523
     *
524
     * @param int $id
525
     *
526
     * @return \Symfony\Component\HttpFoundation\Response
527
     */
528
    public function update($id, $data = null)
529
    {
530
        $data = ($data) ?: Input::all();
531
532
        $isEditable = $this->isEditable($data);
533
534
        $data = $this->handleEditable($data);
535
536
        $data = $this->handleFileDelete($data);
537
538
        if ($this->handleOrderable($id, $data)) {
539
            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 539 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
540
                'status'  => true,
541
                'message' => trans('admin.update_succeeded'),
542
            ]);
543
        }
544
545
        /* @var Model $this->model */
546
        $this->model = $this->model->with($this->getRelations())->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
547
548
        $this->setFieldOriginalValue();
549
550
        // Handle validation errors.
551
        if ($validationMessages = $this->validationMessages($data)) {
552
            if (!$isEditable) {
553
                return back()->withInput()->withErrors($validationMessages);
554
            } else {
555
                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...
556
            }
557
        }
558
559
        if (($response = $this->prepare($data)) instanceof Response) {
560
            return $response;
561
        }
562
563 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...
564
            $updates = $this->prepareUpdate($this->updates);
565
566
            foreach ($updates as $column => $value) {
567
                /* @var Model $this->model */
568
                $this->model->setAttribute($column, $value);
569
            }
570
571
            $this->model->save();
572
573
            $this->updateRelation($this->relations);
574
        });
575
576
        if (($result = $this->callSaved()) instanceof Response) {
577
            return $result;
578
        }
579
580
        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 581 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
581
            return $response;
582
        }
583
584
        return $this->redirectAfterUpdate($id);
585
    }
586
587
    /**
588
     * Get RedirectResponse after store.
589
     *
590
     * @return \Illuminate\Http\RedirectResponse
591
     */
592
    protected function redirectAfterStore()
593
    {
594
        $resourcesPath = $this->resource(0);
595
596
        $key = $this->model->getKey();
597
598
        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 598 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterStore of type Illuminate\Http\RedirectResponse.
Loading history...
599
    }
600
601
    /**
602
     * Get RedirectResponse after update.
603
     *
604
     * @param mixed $key
605
     *
606
     * @return \Illuminate\Http\RedirectResponse
607
     */
608
    protected function redirectAfterUpdate($key)
609
    {
610
        $resourcesPath = $this->resource(-1);
611
612
        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 612 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterUpdate of type Illuminate\Http\RedirectResponse.
Loading history...
613
    }
614
615
    /**
616
     * Get RedirectResponse after data saving.
617
     *
618
     * @param string $resourcesPath
619
     * @param string $key
620
     *
621
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
622
     */
623
    protected function redirectAfterSaving($resourcesPath, $key)
624
    {
625
        if (request('after-save') == 1) {
626
            // continue editing
627
            $url = rtrim($resourcesPath, '/')."/{$key}/edit";
628
        } elseif (request('after-save') == 2) {
629
            // view resource
630
            $url = rtrim($resourcesPath, '/')."/{$key}";
631
        } else {
632
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
633
        }
634
635
        admin_toastr(trans('admin.save_succeeded'));
636
637
        return redirect($url);
638
    }
639
640
    /**
641
     * Check if request is from editable.
642
     *
643
     * @param array $input
644
     *
645
     * @return bool
646
     */
647
    protected function isEditable(array $input = [])
648
    {
649
        return array_key_exists('_editable', $input);
650
    }
651
652
    /**
653
     * Handle editable update.
654
     *
655
     * @param array $input
656
     *
657
     * @return array
658
     */
659
    protected function handleEditable(array $input = [])
660
    {
661
        if (array_key_exists('_editable', $input)) {
662
            $name = $input['name'];
663
            $value = $input['value'];
664
665
            array_forget($input, ['pk', 'value', 'name']);
666
            array_set($input, $name, $value);
667
        }
668
669
        return $input;
670
    }
671
672
    /**
673
     * @param array $input
674
     *
675
     * @return array
676
     */
677
    protected function handleFileDelete(array $input = [])
678
    {
679
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
680
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
681
            unset($input['key']);
682
        }
683
684
        Input::replace($input);
685
686
        return $input;
687
    }
688
689
    /**
690
     * Handle orderable update.
691
     *
692
     * @param int   $id
693
     * @param array $input
694
     *
695
     * @return bool
696
     */
697
    protected function handleOrderable($id, array $input = [])
698
    {
699
        if (array_key_exists('_orderable', $input)) {
700
            $model = $this->model->find($id);
701
702
            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...
703
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
704
705
                return true;
706
            }
707
        }
708
709
        return false;
710
    }
711
712
    /**
713
     * Update relation data.
714
     *
715
     * @param array $relationsData
716
     *
717
     * @return void
718
     */
719
    protected function updateRelation($relationsData)
720
    {
721
        foreach ($relationsData as $name => $values) {
722
            if (!method_exists($this->model, $name)) {
723
                continue;
724
            }
725
726
            $relation = $this->model->$name();
727
728
            $oneToOneRelation = $relation instanceof Relations\HasOne
729
                || $relation instanceof Relations\MorphOne
730
                || $relation instanceof Relations\BelongsTo;
731
732
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
733
734
            if (empty($prepared)) {
735
                continue;
736
            }
737
738
            switch (get_class($relation)) {
739
                case Relations\BelongsToMany::class:
740
                case Relations\MorphToMany::class:
741
                    if (isset($prepared[$name])) {
742
                        $relation->sync($prepared[$name]);
743
                    }
744
                    break;
745 View Code Duplication
                case Relations\HasOne::class:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
746
747
                    $related = $this->model->$name;
748
749
                    // if related is empty
750
                    if (is_null($related)) {
751
                        $related = $relation->getRelated();
752
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
753
                    }
754
755
                    foreach ($prepared[$name] as $column => $value) {
756
                        $related->setAttribute($column, $value);
757
                    }
758
759
                    $related->save();
760
                    break;
761 View Code Duplication
                case Relations\BelongsTo::class:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
762
763
                    $parent = $this->model->$name;
764
765
                    // if related is empty
766
                    if (is_null($parent)) {
767
                        $parent = $relation->getRelated();
768
                    }
769
770
                    foreach ($prepared[$name] as $column => $value) {
771
                        $parent->setAttribute($column, $value);
772
                    }
773
774
                    $parent->save();
775
776
                    // When in creating, associate two models
777
                    if (!$this->model->{$relation->getForeignKey()}) {
778
                        $this->model->{$relation->getForeignKey()} = $parent->getKey();
779
780
                        $this->model->save();
781
                    }
782
783
                    break;
784
                case Relations\MorphOne::class:
785
                    $related = $this->model->$name;
786
                    if (is_null($related)) {
787
                        $related = $relation->make();
788
                    }
789
                    foreach ($prepared[$name] as $column => $value) {
790
                        $related->setAttribute($column, $value);
791
                    }
792
                    $related->save();
793
                    break;
794
                case Relations\HasMany::class:
795
                case Relations\MorphMany::class:
796
797
                    foreach ($prepared[$name] as $related) {
798
                        /** @var Relations\Relation $relation */
799
                        $relation = $this->model()->$name();
800
801
                        $keyName = $relation->getRelated()->getKeyName();
802
803
                        $instance = $relation->findOrNew(array_get($related, $keyName));
804
805
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
806
                            $instance->delete();
807
808
                            continue;
809
                        }
810
811
                        array_forget($related, static::REMOVE_FLAG_NAME);
812
813
                        $instance->fill($related);
814
815
                        $instance->save();
816
                    }
817
818
                    break;
819
            }
820
        }
821
    }
822
823
    /**
824
     * Prepare input data for update.
825
     *
826
     * @param array $updates
827
     * @param bool  $oneToOneRelation If column is one-to-one relation.
828
     *
829
     * @return array
830
     */
831
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
832
    {
833
        $prepared = [];
834
835
        /** @var Field $field */
836
        foreach ($this->builder->fields() as $field) {
837
            $columns = $field->column();
838
839
            // If column not in input array data, then continue.
840
            if (!array_has($updates, $columns)) {
841
                continue;
842
            }
843
844
            if ($this->invalidColumn($columns, $oneToOneRelation)) {
845
                continue;
846
            }
847
848
            $value = $this->getDataByColumn($updates, $columns);
849
850
            $value = $field->prepare($value);
851
852 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...
853
                foreach ($columns as $name => $column) {
854
                    array_set($prepared, $column, $value[$name]);
855
                }
856
            } elseif (is_string($columns)) {
857
                array_set($prepared, $columns, $value);
858
            }
859
        }
860
861
        return $prepared;
862
    }
863
864
    /**
865
     * @param string|array $columns
866
     * @param bool         $oneToOneRelation
867
     *
868
     * @return bool
869
     */
870
    protected function invalidColumn($columns, $oneToOneRelation = false)
871
    {
872
        foreach ((array) $columns as $column) {
873
            if ((!$oneToOneRelation && Str::contains($column, '.')) ||
874
                ($oneToOneRelation && !Str::contains($column, '.'))) {
875
                return true;
876
            }
877
        }
878
879
        return false;
880
    }
881
882
    /**
883
     * Prepare input data for insert.
884
     *
885
     * @param $inserts
886
     *
887
     * @return array
888
     */
889
    protected function prepareInsert($inserts)
890
    {
891
        if ($this->isHasOneRelation($inserts)) {
892
            $inserts = array_dot($inserts);
893
        }
894
895
        foreach ($inserts as $column => $value) {
896
            if (is_null($field = $this->getFieldByColumn($column))) {
897
                unset($inserts[$column]);
898
                continue;
899
            }
900
901
            $inserts[$column] = $field->prepare($value);
902
        }
903
904
        $prepared = [];
905
906
        foreach ($inserts as $key => $value) {
907
            array_set($prepared, $key, $value);
908
        }
909
910
        return $prepared;
911
    }
912
913
    /**
914
     * Is input data is has-one relation.
915
     *
916
     * @param array $inserts
917
     *
918
     * @return bool
919
     */
920
    protected function isHasOneRelation($inserts)
921
    {
922
        $first = current($inserts);
923
924
        if (!is_array($first)) {
925
            return false;
926
        }
927
928
        if (is_array(current($first))) {
929
            return false;
930
        }
931
932
        return Arr::isAssoc($first);
933
    }
934
935
    /**
936
     * Set after getting editing model callback.
937
     *
938
     * @param Closure $callback
939
     *
940
     * @return void
941
     */
942
    public function editing(Closure $callback)
943
    {
944
        $this->editing[] = $callback;
945
    }
946
947
    /**
948
     * Set submitted callback.
949
     *
950
     * @param Closure $callback
951
     *
952
     * @return void
953
     */
954
    public function submitted(Closure $callback)
955
    {
956
        $this->submitted[] = $callback;
957
    }
958
959
    /**
960
     * Set saving callback.
961
     *
962
     * @param Closure $callback
963
     *
964
     * @return void
965
     */
966
    public function saving(Closure $callback)
967
    {
968
        $this->saving[] = $callback;
969
    }
970
971
    /**
972
     * Set saved callback.
973
     *
974
     * @param Closure $callback
975
     *
976
     * @return void
977
     */
978
    public function saved(Closure $callback)
979
    {
980
        $this->saved[] = $callback;
981
    }
982
983
    /**
984
     * Ignore fields to save.
985
     *
986
     * @param string|array $fields
987
     *
988
     * @return $this
989
     */
990
    public function ignore($fields)
991
    {
992
        $this->ignored = array_merge($this->ignored, (array) $fields);
993
994
        return $this;
995
    }
996
997
    /**
998
     * @param array        $data
999
     * @param string|array $columns
1000
     *
1001
     * @return array|mixed
1002
     */
1003 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...
1004
    {
1005
        if (is_string($columns)) {
1006
            return array_get($data, $columns);
1007
        }
1008
1009
        if (is_array($columns)) {
1010
            $value = [];
1011
            foreach ($columns as $name => $column) {
1012
                if (!array_has($data, $column)) {
1013
                    continue;
1014
                }
1015
                $value[$name] = array_get($data, $column);
1016
            }
1017
1018
            return $value;
1019
        }
1020
    }
1021
1022
    /**
1023
     * Find field object by column.
1024
     *
1025
     * @param $column
1026
     *
1027
     * @return mixed
1028
     */
1029
    protected function getFieldByColumn($column)
1030
    {
1031
        return $this->builder->fields()->first(
1032
            function (Field $field) use ($column) {
1033
                if (is_array($field->column())) {
1034
                    return in_array($column, $field->column());
1035
                }
1036
1037
                return $field->column() == $column;
1038
            }
1039
        );
1040
    }
1041
1042
    /**
1043
     * Set original data for each field.
1044
     *
1045
     * @return void
1046
     */
1047
    protected function setFieldOriginalValue()
1048
    {
1049
//        static::doNotSnakeAttributes($this->model);
1050
1051
        $values = $this->model->toArray();
1052
1053
        $this->builder->fields()->each(function (Field $field) use ($values) {
1054
            $field->setOriginal($values);
1055
        });
1056
    }
1057
1058
    /**
1059
     * Set all fields value in form.
1060
     *
1061
     * @param $id
1062
     *
1063
     * @return void
1064
     */
1065
    protected function setFieldValue($id)
1066
    {
1067
        $relations = $this->getRelations();
1068
1069
        $builder = $this->model()->newQuery();
1070
1071
        if ($this->isSoftDeletes) {
1072
            $builder->withTrashed();
1073
        }
1074
1075
        $this->model = $builder->with($relations)->findOrFail($id);
1076
1077
        $this->callEditing();
1078
1079
//        static::doNotSnakeAttributes($this->model);
1080
1081
        $data = $this->model->toArray();
1082
1083
        $this->builder->fields()->each(function (Field $field) use ($data) {
1084
            if (!in_array($field->column(), $this->ignored)) {
1085
                $field->fill($data);
1086
            }
1087
        });
1088
    }
1089
1090
    /**
1091
     * Don't snake case attributes.
1092
     *
1093
     * @param Model $model
1094
     *
1095
     * @return void
1096
     */
1097
    protected static function doNotSnakeAttributes(Model $model)
1098
    {
1099
        $class = get_class($model);
1100
1101
        $class::$snakeAttributes = false;
1102
    }
1103
1104
    /**
1105
     * Get validation messages.
1106
     *
1107
     * @param array $input
1108
     *
1109
     * @return MessageBag|bool
1110
     */
1111
    public function validationMessages($input)
1112
    {
1113
        $failedValidators = [];
1114
1115
        /** @var Field $field */
1116
        foreach ($this->builder->fields() as $field) {
1117
            if (!$validator = $field->getValidator($input)) {
1118
                continue;
1119
            }
1120
1121
            if (($validator instanceof Validator) && !$validator->passes()) {
1122
                $failedValidators[] = $validator;
1123
            }
1124
        }
1125
1126
        $message = $this->mergeValidationMessages($failedValidators);
1127
1128
        return $message->any() ? $message : false;
1129
    }
1130
1131
    /**
1132
     * Merge validation messages from input validators.
1133
     *
1134
     * @param \Illuminate\Validation\Validator[] $validators
1135
     *
1136
     * @return MessageBag
1137
     */
1138
    protected function mergeValidationMessages($validators)
1139
    {
1140
        $messageBag = new MessageBag();
1141
1142
        foreach ($validators as $validator) {
1143
            $messageBag = $messageBag->merge($validator->messages());
1144
        }
1145
1146
        return $messageBag;
1147
    }
1148
1149
    /**
1150
     * Get all relations of model from callable.
1151
     *
1152
     * @return array
1153
     */
1154
    public function getRelations()
1155
    {
1156
        $relations = $columns = [];
1157
1158
        /** @var Field $field */
1159
        foreach ($this->builder->fields() as $field) {
1160
            $columns[] = $field->column();
1161
        }
1162
1163
        foreach (array_flatten($columns) as $column) {
1164
            if (str_contains($column, '.')) {
1165
                list($relation) = explode('.', $column);
1166
1167
                if (method_exists($this->model, $relation) &&
1168
                    $this->model->$relation() instanceof Relations\Relation
1169
                ) {
1170
                    $relations[] = $relation;
1171
                }
1172
            } elseif (method_exists($this->model, $column) &&
1173
                !method_exists(Model::class, $column)
1174
            ) {
1175
                $relations[] = $column;
1176
            }
1177
        }
1178
1179
        return array_unique($relations);
1180
    }
1181
1182
    /**
1183
     * Set action for form.
1184
     *
1185
     * @param string $action
1186
     *
1187
     * @return $this
1188
     */
1189
    public function setAction($action)
1190
    {
1191
        $this->builder()->setAction($action);
1192
1193
        return $this;
1194
    }
1195
1196
    /**
1197
     * Set field and label width in current form.
1198
     *
1199
     * @param int $fieldWidth
1200
     * @param int $labelWidth
1201
     *
1202
     * @return $this
1203
     */
1204
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1205
    {
1206
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1207
            /* @var Field $field  */
1208
            $field->setWidth($fieldWidth, $labelWidth);
1209
        });
1210
1211
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1212
1213
        return $this;
1214
    }
1215
1216
    /**
1217
     * Set view for form.
1218
     *
1219
     * @param string $view
1220
     *
1221
     * @return $this
1222
     */
1223
    public function setView($view)
1224
    {
1225
        $this->builder()->setView($view);
1226
1227
        return $this;
1228
    }
1229
1230
    /**
1231
     * Set title for form.
1232
     *
1233
     * @param string $title
1234
     *
1235
     * @return $this
1236
     */
1237
    public function setTitle($title = '')
1238
    {
1239
        $this->builder()->setTitle($title);
1240
1241
        return $this;
1242
    }
1243
1244
    /**
1245
     * Add a row in form.
1246
     *
1247
     * @param Closure $callback
1248
     *
1249
     * @return $this
1250
     */
1251
    public function row(Closure $callback)
1252
    {
1253
        $this->rows[] = new Row($callback, $this);
1254
1255
        return $this;
1256
    }
1257
1258
    /**
1259
     * Tools setting for form.
1260
     *
1261
     * @param Closure $callback
1262
     */
1263
    public function tools(Closure $callback)
1264
    {
1265
        $callback->call($this, $this->builder->getTools());
1266
    }
1267
1268
    /**
1269
     * Disable form submit.
1270
     *
1271
     * @return $this
1272
     *
1273
     * @deprecated
1274
     */
1275
    public function disableSubmit()
1276
    {
1277
        $this->builder()->getFooter()->disableSubmit();
1278
1279
        return $this;
1280
    }
1281
1282
    /**
1283
     * Disable form reset.
1284
     *
1285
     * @return $this
1286
     *
1287
     * @deprecated
1288
     */
1289
    public function disableReset()
1290
    {
1291
        $this->builder()->getFooter()->disableReset();
1292
1293
        return $this;
1294
    }
1295
1296
    /**
1297
     * Disable View Checkbox on footer.
1298
     *
1299
     * @return $this
1300
     */
1301
    public function disableViewCheck()
1302
    {
1303
        $this->builder()->getFooter()->disableViewCheck();
1304
1305
        return $this;
1306
    }
1307
1308
    /**
1309
     * Disable Editing Checkbox on footer.
1310
     *
1311
     * @return $this
1312
     */
1313
    public function disableEditingCheck()
1314
    {
1315
        $this->builder()->getFooter()->disableEditingCheck();
1316
1317
        return $this;
1318
    }
1319
1320
    /**
1321
     * Footer setting for form.
1322
     *
1323
     * @param Closure $callback
1324
     */
1325
    public function footer(Closure $callback)
1326
    {
1327
        call_user_func($callback, $this->builder()->getFooter());
1328
    }
1329
1330
    /**
1331
     * Get current resource route url.
1332
     *
1333
     * @param int $slice
1334
     *
1335
     * @return string
1336
     */
1337
    public function resource($slice = -2)
1338
    {
1339
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1340
1341
        if ($slice != 0) {
1342
            $segments = array_slice($segments, 0, $slice);
1343
        }
1344
        // # fix #1768
1345
        if ($segments[0] == 'http:' && (config('admin.https') || config('admin.secure'))) {
1346
            $segments[0] = 'https:';
1347
        }
1348
1349
        return implode('/', $segments);
1350
    }
1351
1352
    /**
1353
     * Render the form contents.
1354
     *
1355
     * @return string
1356
     */
1357
    public function render()
1358
    {
1359
        try {
1360
            return $this->builder->render();
1361
        } catch (\Exception $e) {
1362
            return Handler::renderException($e);
1363
        }
1364
    }
1365
1366
    /**
1367
     * Get or set input data.
1368
     *
1369
     * @param string $key
1370
     * @param null   $value
1371
     *
1372
     * @return array|mixed
1373
     */
1374
    public function input($key, $value = null)
1375
    {
1376
        if (is_null($value)) {
1377
            return array_get($this->inputs, $key);
1378
        }
1379
1380
        return array_set($this->inputs, $key, $value);
1381
    }
1382
1383
    /**
1384
     * Register builtin fields.
1385
     *
1386
     * @return void
1387
     */
1388
    public static function registerBuiltinFields()
1389
    {
1390
        $map = [
1391
            'button'         => Field\Button::class,
1392
            'checkbox'       => Field\Checkbox::class,
1393
            'color'          => Field\Color::class,
1394
            'currency'       => Field\Currency::class,
1395
            'date'           => Field\Date::class,
1396
            'dateRange'      => Field\DateRange::class,
1397
            'datetime'       => Field\Datetime::class,
1398
            'dateTimeRange'  => Field\DatetimeRange::class,
1399
            'datetimeRange'  => Field\DatetimeRange::class,
1400
            'decimal'        => Field\Decimal::class,
1401
            'display'        => Field\Display::class,
1402
            'divider'        => Field\Divide::class,
1403
            'divide'         => Field\Divide::class,
1404
            'embeds'         => Field\Embeds::class,
1405
            'editor'         => Field\Editor::class,
1406
            'email'          => Field\Email::class,
1407
            'file'           => Field\File::class,
1408
            'hasMany'        => Field\HasMany::class,
1409
            'hidden'         => Field\Hidden::class,
1410
            'id'             => Field\Id::class,
1411
            'image'          => Field\Image::class,
1412
            'ip'             => Field\Ip::class,
1413
            'map'            => Field\Map::class,
1414
            'mobile'         => Field\Mobile::class,
1415
            'month'          => Field\Month::class,
1416
            'multipleSelect' => Field\MultipleSelect::class,
1417
            'number'         => Field\Number::class,
1418
            'password'       => Field\Password::class,
1419
            'radio'          => Field\Radio::class,
1420
            'rate'           => Field\Rate::class,
1421
            'select'         => Field\Select::class,
1422
            'slider'         => Field\Slider::class,
1423
            'switch'         => Field\SwitchField::class,
1424
            'text'           => Field\Text::class,
1425
            'textarea'       => Field\Textarea::class,
1426
            'time'           => Field\Time::class,
1427
            'timeRange'      => Field\TimeRange::class,
1428
            'url'            => Field\Url::class,
1429
            'year'           => Field\Year::class,
1430
            'html'           => Field\Html::class,
1431
            'tags'           => Field\Tags::class,
1432
            'icon'           => Field\Icon::class,
1433
            'multipleFile'   => Field\MultipleFile::class,
1434
            'multipleImage'  => Field\MultipleImage::class,
1435
            'captcha'        => Field\Captcha::class,
1436
            'listbox'        => Field\Listbox::class,
1437
        ];
1438
1439
        foreach ($map as $abstract => $class) {
1440
            static::extend($abstract, $class);
1441
        }
1442
    }
1443
1444
    /**
1445
     * Register custom field.
1446
     *
1447
     * @param string $abstract
1448
     * @param string $class
1449
     *
1450
     * @return void
1451
     */
1452
    public static function extend($abstract, $class)
1453
    {
1454
        static::$availableFields[$abstract] = $class;
1455
    }
1456
1457
    /**
1458
     * Set form field alias.
1459
     *
1460
     * @param string $field
1461
     * @param string $alias
1462
     *
1463
     * @return void
1464
     */
1465
    public static function alias($field, $alias)
1466
    {
1467
        static::$fieldAlias[$alias] = $field;
1468
    }
1469
1470
    /**
1471
     * Remove registered field.
1472
     *
1473
     * @param array|string $abstract
1474
     */
1475
    public static function forget($abstract)
1476
    {
1477
        array_forget(static::$availableFields, $abstract);
1478
    }
1479
1480
    /**
1481
     * Find field class.
1482
     *
1483
     * @param string $method
1484
     *
1485
     * @return bool|mixed
1486
     */
1487
    public static function findFieldClass($method)
1488
    {
1489
        // If alias exists.
1490
        if (isset(static::$fieldAlias[$method])) {
1491
            $method = static::$fieldAlias[$method];
1492
        }
1493
1494
        $class = array_get(static::$availableFields, $method);
1495
1496
        if (class_exists($class)) {
1497
            return $class;
1498
        }
1499
1500
        return false;
1501
    }
1502
1503
    /**
1504
     * Collect assets required by registered field.
1505
     *
1506
     * @return array
1507
     */
1508
    public static function collectFieldAssets()
1509
    {
1510
        if (!empty(static::$collectedAssets)) {
1511
            return static::$collectedAssets;
1512
        }
1513
1514
        $css = collect();
1515
        $js = collect();
1516
1517
        foreach (static::$availableFields as $field) {
1518
            if (!method_exists($field, 'getAssets')) {
1519
                continue;
1520
            }
1521
1522
            $assets = call_user_func([$field, 'getAssets']);
1523
1524
            $css->push(array_get($assets, 'css'));
1525
            $js->push(array_get($assets, 'js'));
1526
        }
1527
1528
        return static::$collectedAssets = [
1529
            'css' => $css->flatten()->unique()->filter()->toArray(),
1530
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1531
        ];
1532
    }
1533
1534
    /**
1535
     * Getter.
1536
     *
1537
     * @param string $name
1538
     *
1539
     * @return array|mixed
1540
     */
1541
    public function __get($name)
1542
    {
1543
        return $this->input($name);
1544
    }
1545
1546
    /**
1547
     * Setter.
1548
     *
1549
     * @param string $name
1550
     * @param $value
1551
     */
1552
    public function __set($name, $value)
1553
    {
1554
        $this->input($name, $value);
1555
    }
1556
1557
    /**
1558
     * Generate a Field object and add to form builder if Field exists.
1559
     *
1560
     * @param string $method
1561
     * @param array  $arguments
1562
     *
1563
     * @return Field
1564
     */
1565
    public function __call($method, $arguments)
1566
    {
1567
        if ($className = static::findFieldClass($method)) {
1568
            $column = array_get($arguments, 0, ''); //[0];
1569
1570
            $element = new $className($column, array_slice($arguments, 1));
1571
1572
            $this->pushField($element);
1573
1574
            return $element;
1575
        }
1576
1577
        admin_error('Error', "Field type [$method] does not exist.");
1578
1579
        return new Field\Nullable();
1580
    }
1581
}
1582