Completed
Push — master ( 71b25d...5cfdf4 )
by Song
02:44
created

Form::destroy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 11
rs 9.9
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
     * Data for save to current model from input.
114
     *
115
     * @var array
116
     */
117
    protected $updates = [];
118
119
    /**
120
     * Data for save to model's relations from input.
121
     *
122
     * @var array
123
     */
124
    protected $relations = [];
125
126
    /**
127
     * Input data.
128
     *
129
     * @var array
130
     */
131
    protected $inputs = [];
132
133
    /**
134
     * Available fields.
135
     *
136
     * @var array
137
     */
138
    public static $availableFields = [];
139
140
    /**
141
     * Form field alias.
142
     *
143
     * @var array
144
     */
145
    public static $fieldAlias = [];
146
147
    /**
148
     * Ignored saving fields.
149
     *
150
     * @var array
151
     */
152
    protected $ignored = [];
153
154
    /**
155
     * Collected field assets.
156
     *
157
     * @var array
158
     */
159
    protected static $collectedAssets = [];
160
161
    /**
162
     * @var Form\Tab
163
     */
164
    protected $tab = null;
165
166
    /**
167
     * Remove flag in `has many` form.
168
     */
169
    const REMOVE_FLAG_NAME = '_remove_';
170
171
    /**
172
     * Field rows in form.
173
     *
174
     * @var array
175
     */
176
    public $rows = [];
177
178
    /**
179
     * Create a new form instance.
180
     *
181
     * @param $model
182
     * @param \Closure $callback
183
     */
184
    public function __construct($model, Closure $callback = null)
185
    {
186
        $this->model = $model;
187
188
        $this->builder = new Builder($this);
189
190
        if ($callback instanceof Closure) {
191
            $callback($this);
192
        }
193
    }
194
195
    /**
196
     * @param Field $field
197
     *
198
     * @return $this
199
     */
200
    public function pushField(Field $field)
201
    {
202
        $field->setForm($this);
203
204
        $this->builder->fields()->push($field);
205
206
        return $this;
207
    }
208
209
    /**
210
     * @return Model
211
     */
212
    public function model()
213
    {
214
        return $this->model;
215
    }
216
217
    /**
218
     * @return Builder
219
     */
220
    public function builder()
221
    {
222
        return $this->builder;
223
    }
224
225
    /**
226
     * Generate a edit form.
227
     *
228
     * @param $id
229
     *
230
     * @return $this
231
     */
232
    public function edit($id)
233
    {
234
        $this->builder->setMode(Builder::MODE_EDIT);
235
        $this->builder->setResourceId($id);
236
237
        $this->setFieldValue($id);
238
239
        return $this;
240
    }
241
242
    /**
243
     * Use tab to split form.
244
     *
245
     * @param string  $title
246
     * @param Closure $content
247
     *
248
     * @return $this
249
     */
250
    public function tab($title, Closure $content, $active = false)
251
    {
252
        $this->getTab()->append($title, $content, $active);
253
254
        return $this;
255
    }
256
257
    /**
258
     * Get Tab instance.
259
     *
260
     * @return Tab
261
     */
262
    public function getTab()
263
    {
264
        if (is_null($this->tab)) {
265
            $this->tab = new Tab($this);
266
        }
267
268
        return $this->tab;
269
    }
270
271
    /**
272
     * Destroy data entity and remove files.
273
     *
274
     * @param $id
275
     *
276
     * @return mixed
277
     */
278
    public function destroy($id)
279
    {
280
        $ids = explode(',', $id);
281
282
        collect($ids)->filter()->each(function ($id) {
283
            $this->deleteFiles($id);
284
            $this->model()->find($id)->delete();
285
        });
286
287
        return true;
288
    }
289
290
    /**
291
     * Remove files in record.
292
     *
293
     * @param $id
294
     */
295
    protected function deleteFiles($id)
296
    {
297
        // If it's a soft delete, the files in the data will not be deleted.
298
        if (in_array(SoftDeletes::class, class_uses($this->model))) {
299
            return;
300
        }
301
302
        $data = $this
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...
303
            ->model()
304
            ->with($this->getRelations())
305
            ->findOrFail($id)->toArray();
306
307
        $this->builder->fields()->filter(function ($field) {
308
            return $field instanceof Field\File;
309
        })->each(function (Field\File $file) use ($data) {
310
            $file->setOriginal($data);
311
312
            $file->destroy();
313
        });
314
    }
315
316
    /**
317
     * Store a new record.
318
     *
319
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
320
     */
321
    public function store()
322
    {
323
        $data = Input::all();
324
325
        // Handle validation errors.
326
        if ($validationMessages = $this->validationMessages($data)) {
327
            return back()->withInput()->withErrors($validationMessages);
328
        }
329
330
        if (($response = $this->prepare($data)) instanceof Response) {
331
            return $response;
332
        }
333
334 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...
335
            $inserts = $this->prepareInsert($this->updates);
336
337
            foreach ($inserts as $column => $value) {
338
                $this->model->setAttribute($column, $value);
339
            }
340
341
            $this->model->save();
342
343
            $this->updateRelation($this->relations);
344
        });
345
346
        if (($response = $this->callSaved()) instanceof Response) {
347
            return $response;
348
        }
349
350
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
351
            return $response;
352
        }
353
354
        return $this->redirectAfterStore();
355
    }
356
357
    /**
358
     * Get ajax response.
359
     *
360
     * @param string $message
361
     *
362
     * @return bool|\Illuminate\Http\JsonResponse
363
     */
364
    protected function ajaxResponse($message)
365
    {
366
        $request = Request::capture();
367
368
        // ajax but not pjax
369
        if ($request->ajax() && !$request->pjax()) {
370
            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...
371
                'status'  => true,
372
                'message' => $message,
373
            ]);
374
        }
375
376
        return false;
377
    }
378
379
    /**
380
     * Prepare input data for insert or update.
381
     *
382
     * @param array $data
383
     *
384
     * @return mixed
385
     */
386
    protected function prepare($data = [])
387
    {
388
        if (($response = $this->callSubmitted()) instanceof Response) {
389
            return $response;
390
        }
391
392
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);
393
394
        if (($response = $this->callSaving()) instanceof Response) {
395
            return $response;
396
        }
397
398
        $this->relations = $this->getRelationInputs($this->inputs);
399
400
        $this->updates = array_except($this->inputs, array_keys($this->relations));
401
    }
402
403
    /**
404
     * Remove ignored fields from input.
405
     *
406
     * @param array $input
407
     *
408
     * @return array
409
     */
410
    protected function removeIgnoredFields($input)
411
    {
412
        array_forget($input, $this->ignored);
413
414
        return $input;
415
    }
416
417
    /**
418
     * Get inputs for relations.
419
     *
420
     * @param array $inputs
421
     *
422
     * @return array
423
     */
424
    protected function getRelationInputs($inputs = [])
425
    {
426
        $relations = [];
427
428
        foreach ($inputs as $column => $value) {
429
            if (method_exists($this->model, $column)) {
430
                $relation = call_user_func([$this->model, $column]);
431
432
                if ($relation instanceof Relations\Relation) {
433
                    $relations[$column] = $value;
434
                }
435
            }
436
        }
437
438
        return $relations;
439
    }
440
441
    /**
442
     * Call submitted callback.
443
     *
444
     * @return mixed
445
     */
446
    protected function callSubmitted()
447
    {
448
        foreach ($this->submitted as $func) {
449
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
450
                return $ret;
451
            }
452
        }
453
    }
454
455
    /**
456
     * Call saving callback.
457
     *
458
     * @return mixed
459
     */
460
    protected function callSaving()
461
    {
462
        foreach ($this->saving as $func) {
463
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
464
                return $ret;
465
            }
466
        }
467
    }
468
469
    /**
470
     * Callback after saving a Model.
471
     *
472
     * @return mixed|null
473
     */
474
    protected function callSaved()
475
    {
476
        foreach ($this->saved as $func) {
477
            if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) {
478
                return $ret;
479
            }
480
        }
481
    }
482
483
    /**
484
     * Handle update.
485
     *
486
     * @param int $id
487
     *
488
     * @return \Symfony\Component\HttpFoundation\Response
489
     */
490
    public function update($id, $data = null)
491
    {
492
        $data = ($data) ?: Input::all();
493
494
        $isEditable = $this->isEditable($data);
495
496
        $data = $this->handleEditable($data);
497
498
        $data = $this->handleFileDelete($data);
499
500
        if ($this->handleOrderable($id, $data)) {
501
            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 501 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
502
                'status'  => true,
503
                'message' => trans('admin.update_succeeded'),
504
            ]);
505
        }
506
507
        /* @var Model $this->model */
508
        $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...
509
510
        $this->setFieldOriginalValue();
511
512
        // Handle validation errors.
513
        if ($validationMessages = $this->validationMessages($data)) {
514
            if (!$isEditable) {
515
                return back()->withInput()->withErrors($validationMessages);
516
            } else {
517
                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...
518
            }
519
        }
520
521
        if (($response = $this->prepare($data)) instanceof Response) {
522
            return $response;
523
        }
524
525 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...
526
            $updates = $this->prepareUpdate($this->updates);
527
528
            foreach ($updates as $column => $value) {
529
                /* @var Model $this->model */
530
                $this->model->setAttribute($column, $value);
531
            }
532
533
            $this->model->save();
534
535
            $this->updateRelation($this->relations);
536
        });
537
538
        if (($result = $this->callSaved()) instanceof Response) {
539
            return $result;
540
        }
541
542
        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 543 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
543
            return $response;
544
        }
545
546
        return $this->redirectAfterUpdate($id);
547
    }
548
549
    /**
550
     * Get RedirectResponse after store.
551
     *
552
     * @return \Illuminate\Http\RedirectResponse
553
     */
554
    protected function redirectAfterStore()
555
    {
556
        $resourcesPath = $this->resource(0);
557
558
        $key = $this->model->getKey();
559
560
        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 560 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterStore of type Illuminate\Http\RedirectResponse.
Loading history...
561
    }
562
563
    /**
564
     * Get RedirectResponse after update.
565
     *
566
     * @param mixed $key
567
     *
568
     * @return \Illuminate\Http\RedirectResponse
569
     */
570
    protected function redirectAfterUpdate($key)
571
    {
572
        $resourcesPath = $this->resource(-1);
573
574
        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 574 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterUpdate of type Illuminate\Http\RedirectResponse.
Loading history...
575
    }
576
577
    /**
578
     * Get RedirectResponse after data saving.
579
     *
580
     * @param string $resourcesPath
581
     * @param string $key
582
     *
583
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
584
     */
585
    protected function redirectAfterSaving($resourcesPath, $key)
586
    {
587
        if (request('after-save') == 1) {
588
            // continue editing
589
            $url = rtrim($resourcesPath, '/')."/{$key}/edit";
590
        } elseif (request('after-save') == 2) {
591
            // view resource
592
            $url = rtrim($resourcesPath, '/')."/{$key}";
593
        } else {
594
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
595
        }
596
597
        admin_toastr(trans('admin.save_succeeded'));
598
599
        return redirect($url);
600
    }
601
602
    /**
603
     * Check if request is from editable.
604
     *
605
     * @param array $input
606
     *
607
     * @return bool
608
     */
609
    protected function isEditable(array $input = [])
610
    {
611
        return array_key_exists('_editable', $input);
612
    }
613
614
    /**
615
     * Handle editable update.
616
     *
617
     * @param array $input
618
     *
619
     * @return array
620
     */
621
    protected function handleEditable(array $input = [])
622
    {
623
        if (array_key_exists('_editable', $input)) {
624
            $name = $input['name'];
625
            $value = $input['value'];
626
627
            array_forget($input, ['pk', 'value', 'name']);
628
            array_set($input, $name, $value);
629
        }
630
631
        return $input;
632
    }
633
634
    /**
635
     * @param array $input
636
     *
637
     * @return array
638
     */
639
    protected function handleFileDelete(array $input = [])
640
    {
641
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
642
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
643
            unset($input['key']);
644
        }
645
646
        Input::replace($input);
647
648
        return $input;
649
    }
650
651
    /**
652
     * Handle orderable update.
653
     *
654
     * @param int   $id
655
     * @param array $input
656
     *
657
     * @return bool
658
     */
659
    protected function handleOrderable($id, array $input = [])
660
    {
661
        if (array_key_exists('_orderable', $input)) {
662
            $model = $this->model->find($id);
663
664
            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...
665
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
666
667
                return true;
668
            }
669
        }
670
671
        return false;
672
    }
673
674
    /**
675
     * Update relation data.
676
     *
677
     * @param array $relationsData
678
     *
679
     * @return void
680
     */
681
    protected function updateRelation($relationsData)
682
    {
683
        foreach ($relationsData as $name => $values) {
684
            if (!method_exists($this->model, $name)) {
685
                continue;
686
            }
687
688
            $relation = $this->model->$name();
689
690
            $oneToOneRelation = $relation instanceof Relations\HasOne
691
                || $relation instanceof Relations\MorphOne
692
                || $relation instanceof Relations\BelongsTo;
693
694
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
695
696
            if (empty($prepared)) {
697
                continue;
698
            }
699
700
            switch (get_class($relation)) {
701
                case Relations\BelongsToMany::class:
702
                case Relations\MorphToMany::class:
703
                    if (isset($prepared[$name])) {
704
                        $relation->sync($prepared[$name]);
705
                    }
706
                    break;
707 View Code Duplication
                case Relations\HasOne::class:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
708
709
                    $related = $this->model->$name;
710
711
                    // if related is empty
712
                    if (is_null($related)) {
713
                        $related = $relation->getRelated();
714
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
715
                    }
716
717
                    foreach ($prepared[$name] as $column => $value) {
718
                        $related->setAttribute($column, $value);
719
                    }
720
721
                    $related->save();
722
                    break;
723 View Code Duplication
                case Relations\BelongsTo::class:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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