Completed
Push — master ( e65397...3264d1 )
by Song
02:22
created

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