Completed
Pull Request — master (#2381)
by leo
03:33
created

Form::saved()   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 1
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\Http\Request;
15
use Illuminate\Support\Arr;
16
use Illuminate\Support\Facades\DB;
17
use Illuminate\Support\Facades\Input;
18
use Illuminate\Support\MessageBag;
19
use Illuminate\Support\Str;
20
use Illuminate\Validation\Validator;
21
use Spatie\EloquentSortable\Sortable;
22
use Symfony\Component\HttpFoundation\Response;
23
24
/**
25
 * Class Form.
26
 *
27
 * @method Field\Text           text($column, $label = '')
28
 * @method Field\Checkbox       checkbox($column, $label = '')
29
 * @method Field\Radio          radio($column, $label = '')
30
 * @method Field\Select         select($column, $label = '')
31
 * @method Field\MultipleSelect multipleSelect($column, $label = '')
32
 * @method Field\Textarea       textarea($column, $label = '')
33
 * @method Field\Hidden         hidden($column, $label = '')
34
 * @method Field\Id             id($column, $label = '')
35
 * @method Field\Ip             ip($column, $label = '')
36
 * @method Field\Url            url($column, $label = '')
37
 * @method Field\Color          color($column, $label = '')
38
 * @method Field\Email          email($column, $label = '')
39
 * @method Field\Mobile         mobile($column, $label = '')
40
 * @method Field\Slider         slider($column, $label = '')
41
 * @method Field\Map            map($latitude, $longitude, $label = '')
42
 * @method Field\Editor         editor($column, $label = '')
43
 * @method Field\File           file($column, $label = '')
44
 * @method Field\Image          image($column, $label = '')
45
 * @method Field\Date           date($column, $label = '')
46
 * @method Field\Datetime       datetime($column, $label = '')
47
 * @method Field\Time           time($column, $label = '')
48
 * @method Field\Year           year($column, $label = '')
49
 * @method Field\Month          month($column, $label = '')
50
 * @method Field\DateRange      dateRange($start, $end, $label = '')
51
 * @method Field\DateTimeRange  datetimeRange($start, $end, $label = '')
52
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
53
 * @method Field\Number         number($column, $label = '')
54
 * @method Field\Currency       currency($column, $label = '')
55
 * @method Field\HasMany        hasMany($relationName, $callback)
56
 * @method Field\SwitchField    switch($column, $label = '')
57
 * @method Field\Display        display($column, $label = '')
58
 * @method Field\Rate           rate($column, $label = '')
59
 * @method Field\Divide         divider()
60
 * @method Field\Password       password($column, $label = '')
61
 * @method Field\Decimal        decimal($column, $label = '')
62
 * @method Field\Html           html($html, $label = '')
63
 * @method Field\Tags           tags($column, $label = '')
64
 * @method Field\Icon           icon($column, $label = '')
65
 * @method Field\Embeds         embeds($column, $label = '')
66
 * @method Field\MultipleImage  multipleImage($column, $label = '')
67
 * @method Field\MultipleFile   multipleFile($column, $label = '')
68
 * @method Field\Captcha        captcha($column, $label = '')
69
 * @method Field\Listbox        listbox($column, $label = '')
70
 */
71
class Form implements Renderable
72
{
73
    /**
74
     * Eloquent model of the form.
75
     *
76
     * @var Model
77
     */
78
    protected $model;
79
80
    /**
81
     * @var \Illuminate\Validation\Validator
82
     */
83
    protected $validator;
84
85
    /**
86
     * @var Builder
87
     */
88
    protected $builder;
89
90
    /**
91
     * Submitted callback.
92
     *
93
     * @var Closure[]
94
     */
95
    protected $submitted = [];
96
97
    /**
98
     * Saving callback.
99
     *
100
     * @var Closure[]
101
     */
102
    protected $saving = [];
103
104
    /**
105
     * Saved callback.
106
     *
107
     * @var Closure[]
108
     */
109
    protected $saved = [];
110
111
    /**
112
     * Data for save to current model from input.
113
     *
114
     * @var array
115
     */
116
    protected $updates = [];
117
118
    /**
119
     * Data for save to model's relations from input.
120
     *
121
     * @var array
122
     */
123
    protected $relations = [];
124
125
    /**
126
     * Input data.
127
     *
128
     * @var array
129
     */
130
    protected $inputs = [];
131
132
    /**
133
     * Available fields.
134
     *
135
     * @var array
136
     */
137
    public static $availableFields = [];
138
139
    /**
140
     * Ignored saving fields.
141
     *
142
     * @var array
143
     */
144
    protected $ignored = [];
145
146
    /**
147
     * Collected field assets.
148
     *
149
     * @var array
150
     */
151
    protected static $collectedAssets = [];
152
153
    /**
154
     * @var Form\Tab
155
     */
156
    protected $tab = null;
157
158
    /**
159
     * Remove flag in `has many` form.
160
     */
161
    const REMOVE_FLAG_NAME = '_remove_';
162
163
    /**
164
     * Field rows in form.
165
     *
166
     * @var array
167
     */
168
    public $rows = [];
169
170
    /**
171
     * Create a new form instance.
172
     *
173
     * @param $model
174
     * @param \Closure $callback
175
     */
176
    public function __construct($model, Closure $callback = null)
177
    {
178
        $this->model = $model;
179
180
        $this->builder = new Builder($this);
181
182
        if ($callback instanceof Closure) {
183
            $callback($this);
184
        }
185
    }
186
187
    /**
188
     * @param Field $field
189
     *
190
     * @return $this
191
     */
192
    public function pushField(Field $field)
193
    {
194
        $field->setForm($this);
195
196
        $this->builder->fields()->push($field);
197
198
        return $this;
199
    }
200
201
    /**
202
     * @return Model
203
     */
204
    public function model()
205
    {
206
        return $this->model;
207
    }
208
209
    /**
210
     * @return Builder
211
     */
212
    public function builder()
213
    {
214
        return $this->builder;
215
    }
216
217
    /**
218
     * Generate a edit form.
219
     *
220
     * @param $id
221
     *
222
     * @return $this
223
     */
224
    public function edit($id)
225
    {
226
        $this->builder->setMode(Builder::MODE_EDIT);
227
        $this->builder->setResourceId($id);
228
229
        $this->setFieldValue($id);
230
231
        return $this;
232
    }
233
234
    /**
235
     * Use tab to split form.
236
     *
237
     * @param string  $title
238
     * @param Closure $content
239
     *
240
     * @return $this
241
     */
242
    public function tab($title, Closure $content, $active = false)
243
    {
244
        $this->getTab()->append($title, $content, $active);
245
246
        return $this;
247
    }
248
249
    /**
250
     * Get Tab instance.
251
     *
252
     * @return Tab
253
     */
254
    public function getTab()
255
    {
256
        if (is_null($this->tab)) {
257
            $this->tab = new Tab($this);
258
        }
259
260
        return $this->tab;
261
    }
262
263
    /**
264
     * Destroy data entity and remove files.
265
     *
266
     * @param $id
267
     *
268
     * @return mixed
269
     */
270
    public function destroy($id)
271
    {
272
        $ids = explode(',', $id);
273
274
        foreach ($ids as $id) {
275
            if (empty($id)) {
276
                continue;
277
            }
278
            $this->deleteFilesAndImages($id);
279
            $this->model->find($id)->delete();
280
        }
281
282
        return true;
283
    }
284
285
    /**
286
     * Remove files or images in record.
287
     *
288
     * @param $id
289
     */
290
    protected function deleteFilesAndImages($id)
291
    {
292
        $data = $this->model->with($this->getRelations())
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...
293
            ->findOrFail($id)->toArray();
294
295
        $this->builder->fields()->filter(function ($field) {
296
            return $field instanceof Field\File;
297
        })->each(function (Field\File $file) use ($data) {
298
            $file->setOriginal($data);
299
300
            $file->destroy();
301
        });
302
    }
303
304
    /**
305
     * Store a new record.
306
     *
307
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
308
     */
309
    public function store()
310
    {
311
        $data = Input::all();
312
313
        // Handle validation errors.
314
        if ($validationMessages = $this->validationMessages($data)) {
315
            return back()->withInput()->withErrors($validationMessages);
316
        }
317
318
        if (($response = $this->prepare($data)) instanceof Response) {
319
            return $response;
320
        }
321
322 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...
323
            $inserts = $this->prepareInsert($this->updates);
324
325
            foreach ($inserts as $column => $value) {
326
                $this->model->setAttribute($column, $value);
327
            }
328
329
            $this->model->save();
330
331
            $this->updateRelation($this->relations);
332
        });
333
334
        if (($response = $this->callSaved()) instanceof Response) {
335
            return $response;
336
        }
337
338
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
339
            return $response;
340
        }
341
342
        return $this->redirectAfterStore();
343
    }
344
345
    /**
346
     * Get ajax response.
347
     *
348
     * @param string $message
349
     *
350
     * @return bool|\Illuminate\Http\JsonResponse
351
     */
352
    protected function ajaxResponse($message)
353
    {
354
        $request = Request::capture();
355
356
        // ajax but not pjax
357
        if ($request->ajax() && !$request->pjax()) {
358
            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...
359
                'status'  => true,
360
                'message' => $message,
361
            ]);
362
        }
363
364
        return false;
365
    }
366
367
    /**
368
     * Prepare input data for insert or update.
369
     *
370
     * @param array $data
371
     *
372
     * @return mixed
373
     */
374
    protected function prepare($data = [])
375
    {
376
        if (($response = $this->callSubmitted()) instanceof Response) {
377
            return $response;
378
        }
379
380
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);
381
382
        if (($response = $this->callSaving()) instanceof Response) {
383
            return $response;
384
        }
385
386
        $this->relations = $this->getRelationInputs($this->inputs);
387
388
        $this->updates = array_except($this->inputs, array_keys($this->relations));
389
    }
390
391
    /**
392
     * Remove ignored fields from input.
393
     *
394
     * @param array $input
395
     *
396
     * @return array
397
     */
398
    protected function removeIgnoredFields($input)
399
    {
400
        array_forget($input, $this->ignored);
401
402
        return $input;
403
    }
404
405
    /**
406
     * Get inputs for relations.
407
     *
408
     * @param array $inputs
409
     *
410
     * @return array
411
     */
412
    protected function getRelationInputs($inputs = [])
413
    {
414
        $relations = [];
415
416
        foreach ($inputs as $column => $value) {
417
            if (method_exists($this->model, $column)) {
418
                $relation = call_user_func([$this->model, $column]);
419
420
                if ($relation instanceof Relations\Relation) {
421
                    $relations[$column] = $value;
422
                }
423
            }
424
        }
425
426
        return $relations;
427
    }
428
429
    /**
430
     * Call submitted callback.
431
     *
432
     * @return mixed
433
     */
434
    protected function callSubmitted()
435
    {
436
        foreach ($this->submitted as $func) {
437
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
438
                return $ret;
439
            }
440
        }
441
    }
442
443
    /**
444
     * Call saving callback.
445
     *
446
     * @return mixed
447
     */
448
    protected function callSaving()
449
    {
450
        foreach ($this->saving as $func) {
451
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
452
                return $ret;
453
            }
454
        }
455
    }
456
457
    /**
458
     * Callback after saving a Model.
459
     *
460
     * @return mixed|null
461
     */
462
    protected function callSaved()
463
    {
464
        foreach ($this->saved as $func) {
465
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
466
                return $ret;
467
            }
468
        }
469
    }
470
471
    /**
472
     * Handle update.
473
     *
474
     * @param int $id
475
     *
476
     * @return \Symfony\Component\HttpFoundation\Response
477
     */
478
    public function update($id, $data = null)
479
    {
480
        $data = ($data) ?: Input::all();
481
482
        $isEditable = $this->isEditable($data);
483
484
        $data = $this->handleEditable($data);
485
486
        $data = $this->handleFileDelete($data);
487
488
        if ($this->handleOrderable($id, $data)) {
489
            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 489 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
490
                'status'  => true,
491
                'message' => trans('admin.update_succeeded'),
492
            ]);
493
        }
494
495
        /* @var Model $this->model */
496
        $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...
497
498
        $this->setFieldOriginalValue();
499
500
        // Handle validation errors.
501
        if ($validationMessages = $this->validationMessages($data)) {
502
            if (!$isEditable) {
503
                return back()->withInput()->withErrors($validationMessages);
504
            } else {
505
                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...
506
            }
507
        }
508
509
        if (($response = $this->prepare($data)) instanceof Response) {
510
            return $response;
511
        }
512
513 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...
514
            $updates = $this->prepareUpdate($this->updates);
515
516
            foreach ($updates as $column => $value) {
517
                /* @var Model $this->model */
518
                $this->model->setAttribute($column, $value);
519
            }
520
521
            $this->model->save();
522
523
            $this->updateRelation($this->relations);
524
        });
525
526
        if (($result = $this->callSaved()) instanceof Response) {
527
            return $result;
528
        }
529
530
        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 531 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
531
            return $response;
532
        }
533
534
        return $this->redirectAfterUpdate($id);
535
    }
536
537
    /**
538
     * Get RedirectResponse after store.
539
     *
540
     * @return \Illuminate\Http\RedirectResponse
541
     */
542
    protected function redirectAfterStore()
543
    {
544
        $resourcesPath = $this->resource(0);
545
546
        $key = $this->model->getKey();
547
548
        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 548 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterStore of type Illuminate\Http\RedirectResponse.
Loading history...
549
    }
550
551
    /**
552
     * Get RedirectResponse after update.
553
     *
554
     * @param mixed $key
555
     *
556
     * @return \Illuminate\Http\RedirectResponse
557
     */
558
    protected function redirectAfterUpdate($key)
559
    {
560
        $resourcesPath = $this->resource(-1);
561
562
        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 562 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterUpdate of type Illuminate\Http\RedirectResponse.
Loading history...
563
    }
564
565
    /**
566
     * Get RedirectResponse after data saving.
567
     *
568
     * @param string $resourcesPath
569
     * @param string $key
570
     *
571
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
572
     */
573
    protected function redirectAfterSaving($resourcesPath, $key)
574
    {
575
        if (request('after-save') == 1) {
576
            // continue editing
577
            $url = rtrim($resourcesPath, '/')."/{$key}/edit";
578
        } elseif (request('after-save') == 2) {
579
            // view resource
580
            $url = rtrim($resourcesPath, '/')."/{$key}";
581
        } else {
582
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
583
        }
584
585
        admin_toastr(trans('admin.save_succeeded'));
586
587
        return redirect($url);
588
    }
589
590
    /**
591
     * Check if request is from editable.
592
     *
593
     * @param array $input
594
     *
595
     * @return bool
596
     */
597
    protected function isEditable(array $input = [])
598
    {
599
        return array_key_exists('_editable', $input);
600
    }
601
602
    /**
603
     * Handle editable update.
604
     *
605
     * @param array $input
606
     *
607
     * @return array
608
     */
609
    protected function handleEditable(array $input = [])
610
    {
611
        if (array_key_exists('_editable', $input)) {
612
            $name = $input['name'];
613
            $value = $input['value'];
614
615
            array_forget($input, ['pk', 'value', 'name']);
616
            array_set($input, $name, $value);
617
        }
618
619
        return $input;
620
    }
621
622
    /**
623
     * @param array $input
624
     *
625
     * @return array
626
     */
627
    protected function handleFileDelete(array $input = [])
628
    {
629
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
630
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
631
            unset($input['key']);
632
        }
633
634
        Input::replace($input);
635
636
        return $input;
637
    }
638
639
    /**
640
     * Handle orderable update.
641
     *
642
     * @param int   $id
643
     * @param array $input
644
     *
645
     * @return bool
646
     */
647
    protected function handleOrderable($id, array $input = [])
648
    {
649
        if (array_key_exists('_orderable', $input)) {
650
            $model = $this->model->find($id);
651
652
            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...
653
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
654
655
                return true;
656
            }
657
        }
658
659
        return false;
660
    }
661
662
    /**
663
     * Update relation data.
664
     *
665
     * @param array $relationsData
666
     *
667
     * @return void
668
     */
669
    protected function updateRelation($relationsData)
670
    {
671
        foreach ($relationsData as $name => $values) {
672
            if (!method_exists($this->model, $name)) {
673
                continue;
674
            }
675
676
            $relation = $this->model->$name();
677
678
            $oneToOneRelation = $relation instanceof Relations\HasOne
679
                || $relation instanceof Relations\MorphOne
680
                || $relation instanceof Relations\BelongsTo;
681
682
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
683
684
            if (empty($prepared)) {
685
                continue;
686
            }
687
688
            switch (get_class($relation)) {
689
                case Relations\BelongsToMany::class:
690
                case Relations\MorphToMany::class:
691
                    if (isset($prepared[$name])) {
692
                        $relation->sync($prepared[$name]);
693
                    }
694
                    break;
695
                case Relations\HasOne::class:
696
                case Relations\BelongsTo::class:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
697
698
                    $related = $this->model->$name;
699
700
                    // if related is empty
701
                    if (is_null($related)) {
702
                        $related = $relation->getRelated();
703
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
704
                    }
705
706
                    foreach ($prepared[$name] as $column => $value) {
707
                        $related->setAttribute($column, $value);
708
                    }
709
710
                    $related->save();
711
                    break;
712
                case Relations\MorphOne::class:
713
                    $related = $this->model->$name;
714
                    if (is_null($related)) {
715
                        $related = $relation->make();
716
                    }
717
                    foreach ($prepared[$name] as $column => $value) {
718
                        $related->setAttribute($column, $value);
719
                    }
720
                    $related->save();
721
                    break;
722
                case Relations\HasMany::class:
723
                case Relations\MorphMany::class:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
724
725
                    foreach ($prepared[$name] as $related) {
726
                        /** @var Relations\Relation $relation */
727
                        $relation = $this->model()->$name();
728
729
                        $keyName = $relation->getRelated()->getKeyName();
730
731
                        $instance = $relation->findOrNew(array_get($related, $keyName));
732
733
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
734
                            $instance->delete();
735
736
                            continue;
737
                        }
738
739
                        array_forget($related, static::REMOVE_FLAG_NAME);
740
741
                        $instance->fill($related);
742
743
                        $instance->save();
744
                    }
745
746
                    break;
747
            }
748
        }
749
    }
750
751
    /**
752
     * Prepare input data for update.
753
     *
754
     * @param array $updates
755
     * @param bool  $oneToOneRelation If column is one-to-one relation.
756
     *
757
     * @return array
758
     */
759
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
760
    {
761
        $prepared = [];
762
763
        /** @var Field $field */
764
        foreach ($this->builder->fields() as $field) {
765
            $columns = $field->column();
766
767
            // If column not in input array data, then continue.
768
            if (!array_has($updates, $columns)) {
769
                continue;
770
            }
771
772
            if ($this->invalidColumn($columns, $oneToOneRelation)) {
773
                continue;
774
            }
775
776
            $value = $this->getDataByColumn($updates, $columns);
777
778
            $value = $field->prepare($value);
779
780 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...
781
                foreach ($columns as $name => $column) {
782
                    array_set($prepared, $column, $value[$name]);
783
                }
784
            } elseif (is_string($columns)) {
785
                array_set($prepared, $columns, $value);
786
            }
787
        }
788
789
        return $prepared;
790
    }
791
792
    /**
793
     * @param string|array $columns
794
     * @param bool         $oneToOneRelation
795
     *
796
     * @return bool
797
     */
798
    protected function invalidColumn($columns, $oneToOneRelation = false)
799
    {
800
        foreach ((array) $columns as $column) {
801
            if ((!$oneToOneRelation && Str::contains($column, '.')) ||
802
                ($oneToOneRelation && !Str::contains($column, '.'))) {
803
                return true;
804
            }
805
        }
806
807
        return false;
808
    }
809
810
    /**
811
     * Prepare input data for insert.
812
     *
813
     * @param $inserts
814
     *
815
     * @return array
816
     */
817
    protected function prepareInsert($inserts)
818
    {
819
        if ($this->isHasOneRelation($inserts)) {
820
            $inserts = array_dot($inserts);
821
        }
822
823
        foreach ($inserts as $column => $value) {
824
            if (is_null($field = $this->getFieldByColumn($column))) {
825
                unset($inserts[$column]);
826
                continue;
827
            }
828
829
            $inserts[$column] = $field->prepare($value);
830
        }
831
832
        $prepared = [];
833
834
        foreach ($inserts as $key => $value) {
835
            array_set($prepared, $key, $value);
836
        }
837
838
        return $prepared;
839
    }
840
841
    /**
842
     * Is input data is has-one relation.
843
     *
844
     * @param array $inserts
845
     *
846
     * @return bool
847
     */
848
    protected function isHasOneRelation($inserts)
849
    {
850
        $first = current($inserts);
851
852
        if (!is_array($first)) {
853
            return false;
854
        }
855
856
        if (is_array(current($first))) {
857
            return false;
858
        }
859
860
        return Arr::isAssoc($first);
861
    }
862
863
    /**
864
     * Set submitted callback.
865
     *
866
     * @param Closure $callback
867
     *
868
     * @return void
869
     */
870
    public function submitted(Closure $callback)
871
    {
872
        $this->submitted[] = $callback;
873
    }
874
875
    /**
876
     * Set saving callback.
877
     *
878
     * @param Closure $callback
879
     *
880
     * @return void
881
     */
882
    public function saving(Closure $callback)
883
    {
884
        $this->saving[] = $callback;
885
    }
886
887
    /**
888
     * Set saved callback.
889
     *
890
     * @param Closure $callback
891
     *
892
     * @return void
893
     */
894
    public function saved(Closure $callback)
895
    {
896
        $this->saved[] = $callback;
897
    }
898
899
    /**
900
     * Ignore fields to save.
901
     *
902
     * @param string|array $fields
903
     *
904
     * @return $this
905
     */
906
    public function ignore($fields)
907
    {
908
        $this->ignored = array_merge($this->ignored, (array) $fields);
909
910
        return $this;
911
    }
912
913
    /**
914
     * @param array        $data
915
     * @param string|array $columns
916
     *
917
     * @return array|mixed
918
     */
919 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...
920
    {
921
        if (is_string($columns)) {
922
            return array_get($data, $columns);
923
        }
924
925
        if (is_array($columns)) {
926
            $value = [];
927
            foreach ($columns as $name => $column) {
928
                if (!array_has($data, $column)) {
929
                    continue;
930
                }
931
                $value[$name] = array_get($data, $column);
932
            }
933
934
            return $value;
935
        }
936
    }
937
938
    /**
939
     * Find field object by column.
940
     *
941
     * @param $column
942
     *
943
     * @return mixed
944
     */
945
    protected function getFieldByColumn($column)
946
    {
947
        return $this->builder->fields()->first(
948
            function (Field $field) use ($column) {
949
                if (is_array($field->column())) {
950
                    return in_array($column, $field->column());
951
                }
952
953
                return $field->column() == $column;
954
            }
955
        );
956
    }
957
958
    /**
959
     * Set original data for each field.
960
     *
961
     * @return void
962
     */
963
    protected function setFieldOriginalValue()
964
    {
965
//        static::doNotSnakeAttributes($this->model);
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
966
967
        $values = $this->model->toArray();
968
969
        $this->builder->fields()->each(function (Field $field) use ($values) {
970
            $field->setOriginal($values);
971
        });
972
    }
973
974
    /**
975
     * Set all fields value in form.
976
     *
977
     * @param $id
978
     *
979
     * @return void
980
     */
981
    protected function setFieldValue($id)
982
    {
983
        $relations = $this->getRelations();
984
985
        $this->model = $this->model->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...
986
987
//        static::doNotSnakeAttributes($this->model);
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
988
989
        $data = $this->model->toArray();
990
991
        $this->builder->fields()->each(function (Field $field) use ($data) {
992
            if (!in_array($field->column(), $this->ignored)) {
993
                $field->fill($data);
994
            }
995
        });
996
    }
997
998
    /**
999
     * Don't snake case attributes.
1000
     *
1001
     * @param Model $model
1002
     *
1003
     * @return void
1004
     */
1005
    protected static function doNotSnakeAttributes(Model $model)
1006
    {
1007
        $class = get_class($model);
1008
1009
        $class::$snakeAttributes = false;
1010
    }
1011
1012
    /**
1013
     * Get validation messages.
1014
     *
1015
     * @param array $input
1016
     *
1017
     * @return MessageBag|bool
1018
     */
1019
    protected function validationMessages($input)
1020
    {
1021
        $failedValidators = [];
1022
1023
        /** @var Field $field */
1024
        foreach ($this->builder->fields() as $field) {
1025
            if (!$validator = $field->getValidator($input)) {
1026
                continue;
1027
            }
1028
1029
            if (($validator instanceof Validator) && !$validator->passes()) {
1030
                $failedValidators[] = $validator;
1031
            }
1032
        }
1033
1034
        $message = $this->mergeValidationMessages($failedValidators);
1035
1036
        return $message->any() ? $message : false;
1037
    }
1038
1039
    /**
1040
     * Merge validation messages from input validators.
1041
     *
1042
     * @param \Illuminate\Validation\Validator[] $validators
1043
     *
1044
     * @return MessageBag
1045
     */
1046
    protected function mergeValidationMessages($validators)
1047
    {
1048
        $messageBag = new MessageBag();
1049
1050
        foreach ($validators as $validator) {
1051
            $messageBag = $messageBag->merge($validator->messages());
1052
        }
1053
1054
        return $messageBag;
1055
    }
1056
1057
    /**
1058
     * Get all relations of model from callable.
1059
     *
1060
     * @return array
1061
     */
1062
    public function getRelations()
1063
    {
1064
        $relations = $columns = [];
1065
1066
        /** @var Field $field */
1067
        foreach ($this->builder->fields() as $field) {
1068
            $columns[] = $field->column();
1069
        }
1070
1071
        foreach (array_flatten($columns) as $column) {
1072
            if (str_contains($column, '.')) {
1073
                list($relation) = explode('.', $column);
1074
1075
                if (method_exists($this->model, $relation) &&
1076
                    $this->model->$relation() instanceof Relations\Relation
1077
                ) {
1078
                    $relations[] = $relation;
1079
                }
1080
            } elseif (method_exists($this->model, $column) &&
1081
                !method_exists(Model::class, $column)
1082
            ) {
1083
                $relations[] = $column;
1084
            }
1085
        }
1086
1087
        return array_unique($relations);
1088
    }
1089
1090
    /**
1091
     * Set action for form.
1092
     *
1093
     * @param string $action
1094
     *
1095
     * @return $this
1096
     */
1097
    public function setAction($action)
1098
    {
1099
        $this->builder()->setAction($action);
1100
1101
        return $this;
1102
    }
1103
1104
    /**
1105
     * Set field and label width in current form.
1106
     *
1107
     * @param int $fieldWidth
1108
     * @param int $labelWidth
1109
     *
1110
     * @return $this
1111
     */
1112
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1113
    {
1114
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1115
            /* @var Field $field  */
1116
            $field->setWidth($fieldWidth, $labelWidth);
1117
        });
1118
1119
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1120
1121
        return $this;
1122
    }
1123
1124
    /**
1125
     * Set view for form.
1126
     *
1127
     * @param string $view
1128
     *
1129
     * @return $this
1130
     */
1131
    public function setView($view)
1132
    {
1133
        $this->builder()->setView($view);
1134
1135
        return $this;
1136
    }
1137
1138
    /**
1139
     * Set title for form.
1140
     *
1141
     * @param string $title
1142
     *
1143
     * @return $this
1144
     */
1145
    public function setTitle($title = '')
1146
    {
1147
        $this->builder()->setTitle($title);
1148
1149
        return $this;
1150
    }
1151
1152
    /**
1153
     * Add a row in form.
1154
     *
1155
     * @param Closure $callback
1156
     *
1157
     * @return $this
1158
     */
1159
    public function row(Closure $callback)
1160
    {
1161
        $this->rows[] = new Row($callback, $this);
1162
1163
        return $this;
1164
    }
1165
1166
    /**
1167
     * Tools setting for form.
1168
     *
1169
     * @param Closure $callback
1170
     */
1171
    public function tools(Closure $callback)
1172
    {
1173
        $callback->call($this, $this->builder->getTools());
1174
    }
1175
1176
    /**
1177
     * Disable form submit.
1178
     *
1179
     * @return $this
1180
     *
1181
     * @deprecated
1182
     */
1183
    public function disableSubmit()
1184
    {
1185
        $this->builder()->getFooter()->disableSubmit();
1186
1187
        return $this;
1188
    }
1189
1190
    /**
1191
     * Disable form reset.
1192
     *
1193
     * @return $this
1194
     *
1195
     * @deprecated
1196
     */
1197
    public function disableReset()
1198
    {
1199
        $this->builder()->getFooter()->disableReset();
1200
1201
        return $this;
1202
    }
1203
1204
    /**
1205
     * Footer setting for form.
1206
     *
1207
     * @param Closure $callback
1208
     */
1209
    public function footer(Closure $callback)
1210
    {
1211
        call_user_func($callback, $this->builder()->getFooter());
1212
    }
1213
1214
    /**
1215
     * Get current resource route url.
1216
     *
1217
     * @param int $slice
1218
     *
1219
     * @return string
1220
     */
1221
    public function resource($slice = -2)
1222
    {
1223
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1224
1225
        if ($slice != 0) {
1226
            $segments = array_slice($segments, 0, $slice);
1227
        }
1228
        // # fix #1768
1229
        if ($segments[0] == 'http:' && config('admin.secure') == true) {
1230
            $segments[0] = 'https:';
1231
        }
1232
1233
        return implode('/', $segments);
1234
    }
1235
1236
    /**
1237
     * Render the form contents.
1238
     *
1239
     * @return string
1240
     */
1241
    public function render()
1242
    {
1243
        try {
1244
            return $this->builder->render();
1245
        } catch (\Exception $e) {
1246
            return Handler::renderException($e);
1247
        }
1248
    }
1249
1250
    /**
1251
     * Get or set input data.
1252
     *
1253
     * @param string $key
1254
     * @param null   $value
1255
     *
1256
     * @return array|mixed
1257
     */
1258
    public function input($key, $value = null)
1259
    {
1260
        if (is_null($value)) {
1261
            return array_get($this->inputs, $key);
1262
        }
1263
1264
        return array_set($this->inputs, $key, $value);
1265
    }
1266
1267
    /**
1268
     * Register builtin fields.
1269
     *
1270
     * @return void
1271
     */
1272
    public static function registerBuiltinFields()
1273
    {
1274
        $map = [
1275
            'button'         => Field\Button::class,
1276
            'checkbox'       => Field\Checkbox::class,
1277
            'color'          => Field\Color::class,
1278
            'currency'       => Field\Currency::class,
1279
            'date'           => Field\Date::class,
1280
            'dateRange'      => Field\DateRange::class,
1281
            'datetime'       => Field\Datetime::class,
1282
            'dateTimeRange'  => Field\DatetimeRange::class,
1283
            'datetimeRange'  => Field\DatetimeRange::class,
1284
            'decimal'        => Field\Decimal::class,
1285
            'display'        => Field\Display::class,
1286
            'divider'        => Field\Divide::class,
1287
            'divide'         => Field\Divide::class,
1288
            'embeds'         => Field\Embeds::class,
1289
            'editor'         => Field\Editor::class,
1290
            'email'          => Field\Email::class,
1291
            'file'           => Field\File::class,
1292
            'hasMany'        => Field\HasMany::class,
1293
            'hidden'         => Field\Hidden::class,
1294
            'id'             => Field\Id::class,
1295
            'image'          => Field\Image::class,
1296
            'ip'             => Field\Ip::class,
1297
            'map'            => Field\Map::class,
1298
            'mobile'         => Field\Mobile::class,
1299
            'month'          => Field\Month::class,
1300
            'multipleSelect' => Field\MultipleSelect::class,
1301
            'number'         => Field\Number::class,
1302
            'password'       => Field\Password::class,
1303
            'radio'          => Field\Radio::class,
1304
            'rate'           => Field\Rate::class,
1305
            'select'         => Field\Select::class,
1306
            'slider'         => Field\Slider::class,
1307
            'switch'         => Field\SwitchField::class,
1308
            'text'           => Field\Text::class,
1309
            'textarea'       => Field\Textarea::class,
1310
            'time'           => Field\Time::class,
1311
            'timeRange'      => Field\TimeRange::class,
1312
            'url'            => Field\Url::class,
1313
            'year'           => Field\Year::class,
1314
            'html'           => Field\Html::class,
1315
            'tags'           => Field\Tags::class,
1316
            'icon'           => Field\Icon::class,
1317
            'multipleFile'   => Field\MultipleFile::class,
1318
            'multipleImage'  => Field\MultipleImage::class,
1319
            'captcha'        => Field\Captcha::class,
1320
            'listbox'        => Field\Listbox::class,
1321
        ];
1322
1323
        foreach ($map as $abstract => $class) {
1324
            static::extend($abstract, $class);
1325
        }
1326
    }
1327
1328
    /**
1329
     * Register custom field.
1330
     *
1331
     * @param string $abstract
1332
     * @param string $class
1333
     *
1334
     * @return void
1335
     */
1336
    public static function extend($abstract, $class)
1337
    {
1338
        static::$availableFields[$abstract] = $class;
1339
    }
1340
1341
    /**
1342
     * Remove registered field.
1343
     *
1344
     * @param array|string $abstract
1345
     */
1346
    public static function forget($abstract)
1347
    {
1348
        array_forget(static::$availableFields, $abstract);
1349
    }
1350
1351
    /**
1352
     * Find field class.
1353
     *
1354
     * @param string $method
1355
     *
1356
     * @return bool|mixed
1357
     */
1358 View Code Duplication
    public static function findFieldClass($method)
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...
1359
    {
1360
        $class = array_get(static::$availableFields, $method);
1361
1362
        if (class_exists($class)) {
1363
            return $class;
1364
        }
1365
1366
        return false;
1367
    }
1368
1369
    /**
1370
     * Collect assets required by registered field.
1371
     *
1372
     * @return array
1373
     */
1374
    public static function collectFieldAssets()
1375
    {
1376
        if (!empty(static::$collectedAssets)) {
1377
            return static::$collectedAssets;
1378
        }
1379
1380
        $css = collect();
1381
        $js = collect();
1382
1383
        foreach (static::$availableFields as $field) {
1384
            if (!method_exists($field, 'getAssets')) {
1385
                continue;
1386
            }
1387
1388
            $assets = call_user_func([$field, 'getAssets']);
1389
1390
            $css->push(array_get($assets, 'css'));
1391
            $js->push(array_get($assets, 'js'));
1392
        }
1393
1394
        return static::$collectedAssets = [
1395
            'css' => $css->flatten()->unique()->filter()->toArray(),
1396
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1397
        ];
1398
    }
1399
1400
    /**
1401
     * Getter.
1402
     *
1403
     * @param string $name
1404
     *
1405
     * @return array|mixed
1406
     */
1407
    public function __get($name)
1408
    {
1409
        return $this->input($name);
1410
    }
1411
1412
    /**
1413
     * Setter.
1414
     *
1415
     * @param string $name
1416
     * @param $value
1417
     */
1418
    public function __set($name, $value)
1419
    {
1420
        $this->input($name, $value);
1421
    }
1422
1423
    /**
1424
     * Generate a Field object and add to form builder if Field exists.
1425
     *
1426
     * @param string $method
1427
     * @param array  $arguments
1428
     *
1429
     * @return Field|void
1430
     */
1431
    public function __call($method, $arguments)
1432
    {
1433
        if ($className = static::findFieldClass($method)) {
1434
            $column = array_get($arguments, 0, ''); //[0];
1435
1436
            $element = new $className($column, array_slice($arguments, 1));
1437
1438
            $this->pushField($element);
1439
1440
            return $element;
1441
        }
1442
    }
1443
}
1444