Completed
Pull Request — master (#2907)
by
unknown
02:15
created

Form::pushField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
396
                'status'  => true,
397
                'message' => $message,
398
            ]);
399
        }
400
401
        return false;
402
    }
403
404
    /**
405
     * Prepare input data for insert or update.
406
     *
407
     * @param array $data
408
     *
409
     * @return mixed
410
     */
411
    protected function prepare($data = [])
412
    {
413
        if (($response = $this->callSubmitted()) instanceof Response) {
414
            return $response;
415
        }
416
417
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);
418
419
        if (($response = $this->callSaving()) instanceof Response) {
420
            return $response;
421
        }
422
423
        $this->relations = $this->getRelationInputs($this->inputs);
424
425
        $this->updates = array_except($this->inputs, array_keys($this->relations));
426
    }
427
428
    /**
429
     * Remove ignored fields from input.
430
     *
431
     * @param array $input
432
     *
433
     * @return array
434
     */
435
    protected function removeIgnoredFields($input)
436
    {
437
        array_forget($input, $this->ignored);
438
439
        return $input;
440
    }
441
442
    /**
443
     * Get inputs for relations.
444
     *
445
     * @param array $inputs
446
     *
447
     * @return array
448
     */
449
    protected function getRelationInputs($inputs = [])
450
    {
451
        $relations = [];
452
453
        foreach ($inputs as $column => $value) {
454
            if (method_exists($this->model, $column)) {
455
                $relation = call_user_func([$this->model, $column]);
456
457
                if ($relation instanceof Relations\Relation) {
458
                    $relations[$column] = $value;
459
                }
460
            }
461
        }
462
463
        return $relations;
464
    }
465
466
    /**
467
     * Call editing callbacks.
468
     *
469
     * @return void
470
     */
471
    protected function callEditing()
472
    {
473
        foreach ($this->editing as $func) {
474
            call_user_func($func, $this);
475
        }
476
    }
477
478
    /**
479
     * Call submitted callback.
480
     *
481
     * @return mixed
482
     */
483
    protected function callSubmitted()
484
    {
485
        foreach ($this->submitted as $func) {
486
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
487
                return $ret;
488
            }
489
        }
490
    }
491
492
    /**
493
     * Call saving callback.
494
     *
495
     * @return mixed
496
     */
497
    protected function callSaving()
498
    {
499
        foreach ($this->saving as $func) {
500
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
501
                return $ret;
502
            }
503
        }
504
    }
505
506
    /**
507
     * Callback after saving a Model.
508
     *
509
     * @return mixed|null
510
     */
511
    protected function callSaved()
512
    {
513
        foreach ($this->saved as $func) {
514
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
515
                return $ret;
516
            }
517
        }
518
    }
519
520
    /**
521
     * Handle update.
522
     *
523
     * @param int $id
524
     *
525
     * @return \Symfony\Component\HttpFoundation\Response
526
     */
527
    public function update($id, $data = null)
528
    {
529
        $data = ($data) ?: Input::all();
530
531
        $isEditable = $this->isEditable($data);
532
533
        $data = $this->handleEditable($data);
534
535
        $data = $this->handleFileDelete($data);
536
537
        if ($this->handleOrderable($id, $data)) {
538
            return response([
0 ignored issues
show
Bug Compatibility introduced by
The expression response(array('status' ...n.update_succeeded'))); of type Illuminate\Http\Response...Routing\ResponseFactory adds the type Illuminate\Contracts\Routing\ResponseFactory to the return on line 538 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
539
                'status'  => true,
540
                'message' => trans('admin.update_succeeded'),
541
            ]);
542
        }
543
544
        /* @var Model $this->model */
545
        $this->model = $this->model->with($this->getRelations())->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
546
547
        $this->setFieldOriginalValue();
548
549
        // Handle validation errors.
550
        if ($validationMessages = $this->validationMessages($data)) {
551
            if (!$isEditable) {
552
                return back()->withInput()->withErrors($validationMessages);
553
            } else {
554
                return response()->json(['errors' => array_dot($validationMessages->getMessages())], 422);
0 ignored issues
show
Bug introduced by
The method json does only exist in Illuminate\Contracts\Routing\ResponseFactory, but not in Illuminate\Http\Response.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
555
            }
556
        }
557
558
        if (($response = $this->prepare($data)) instanceof Response) {
559
            return $response;
560
        }
561
562 View Code Duplication
        DB::transaction(function () {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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