Completed
Pull Request — master (#2254)
by
unknown
11:30
created

Form::input()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
306
            ->findOrFail($id)->toArray();
307
308
        $this->builder->fields()->filter(function ($field) {
309
            return $field instanceof Field\File;
310
        })->each(function (File $file) use ($data) {
311
            $file->setOriginal($data);
312
313
            $file->destroy();
314
        });
315
    }
316
317
    /**
318
     * Store a new record.
319
     *
320
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
321
     */
322
    public function store()
323
    {
324
        $data = Input::all();
325
326
        // Handle validation errors.
327
        if ($validationMessages = $this->validationMessages($data)) {
328
            return back()->withInput()->withErrors($validationMessages);
329
        }
330
331
        if (($response = $this->prepare($data)) instanceof Response) {
332
            return $response;
333
        }
334
335 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...
336
            $inserts = $this->prepareInsert($this->updates);
337
338
            foreach ($inserts as $column => $value) {
339
                $this->model->setAttribute($column, $value);
340
            }
341
342
            $this->model->save();
343
344
            $this->updateRelation($this->relations);
345
        });
346
347
        if (($response = $this->complete($this->saved)) instanceof Response) {
348
            return $response;
349
        }
350
351
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
352
            return $response;
353
        }
354
355
        return $this->redirectAfterStore();
356
    }
357
358
    /**
359
     * Get RedirectResponse after store.
360
     *
361
     * @return \Illuminate\Http\RedirectResponse
362
     */
363 View Code Duplication
    protected function redirectAfterStore()
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...
364
    {
365
        admin_toastr(trans('admin.save_succeeded'));
366
367
        $url = Input::get(Builder::PREVIOUS_URL_KEY) ?: $this->resource(0);
368
369
        return redirect($url);
370
    }
371
372
    /**
373
     * Get ajax response.
374
     *
375
     * @param string $message
376
     *
377
     * @return bool|\Illuminate\Http\JsonResponse
378
     */
379
    protected function ajaxResponse($message)
380
    {
381
        $request = Request::capture();
382
383
        // ajax but not pjax
384
        if ($request->ajax() && !$request->pjax()) {
385
            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...
386
                'status'  => true,
387
                'message' => $message,
388
            ]);
389
        }
390
391
        return false;
392
    }
393
394
    /**
395
     * Prepare input data for insert or update.
396
     *
397
     * @param array $data
398
     *
399
     * @return mixed
400
     */
401
    protected function prepare($data = [])
402
    {
403
        if (($response = $this->callSubmitted()) instanceof Response) {
404
            return $response;
405
        }
406
407
        $this->inputs = $this->removeIgnoredFields($data);
408
409
        if (($response = $this->callSaving()) instanceof Response) {
410
            return $response;
411
        }
412
413
        $this->relations = $this->getRelationInputs($this->inputs);
414
415
        $this->updates = array_except($this->inputs, array_keys($this->relations));
416
    }
417
418
    /**
419
     * Remove ignored fields from input.
420
     *
421
     * @param array $input
422
     *
423
     * @return array
424
     */
425
    protected function removeIgnoredFields($input)
426
    {
427
        array_forget($input, $this->ignored);
428
429
        return $input;
430
    }
431
432
    /**
433
     * Get inputs for relations.
434
     *
435
     * @param array $inputs
436
     *
437
     * @return array
438
     */
439
    protected function getRelationInputs($inputs = [])
440
    {
441
        $relations = [];
442
443
        foreach ($inputs as $column => $value) {
444
            if (method_exists($this->model, $column)) {
445
                $relation = call_user_func([$this->model, $column]);
446
447
                if ($relation instanceof Relation) {
448
                    $relations[$column] = $value;
449
                }
450
            }
451
        }
452
453
        return $relations;
454
    }
455
456
    /**
457
     * Call submitted callback.
458
     *
459
     * @return mixed
460
     */
461
    protected function callSubmitted()
462
    {
463
        if ($this->submitted instanceof Closure) {
464
            return call_user_func($this->submitted, $this);
465
        }
466
    }
467
468
    /**
469
     * Call saving callback.
470
     *
471
     * @return mixed
472
     */
473
    protected function callSaving()
474
    {
475
        if ($this->saving instanceof Closure) {
476
            return call_user_func($this->saving, $this);
477
        }
478
    }
479
480
    /**
481
     * Callback after saving a Model.
482
     *
483
     * @param Closure|null $callback
484
     *
485
     * @return mixed|null
486
     */
487
    protected function complete(Closure $callback = null)
488
    {
489
        if ($callback instanceof Closure) {
490
            return $callback($this);
491
        }
492
    }
493
494
    /**
495
     * Handle update.
496
     *
497
     * @param int $id
498
     *
499
     * @return \Symfony\Component\HttpFoundation\Response
500
     */
501
    public function update($id, $data = null)
502
    {
503
        $data = ($data) ?: Input::all();
504
505
        $isEditable = $this->isEditable($data);
506
507
        $data = $this->handleEditable($data);
508
509
        $data = $this->handleFileDelete($data);
510
511
        if ($this->handleOrderable($id, $data)) {
512
            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 512 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
513
                'status'  => true,
514
                'message' => trans('admin.update_succeeded'),
515
            ]);
516
        }
517
518
        /* @var Model $this->model */
519
        $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...
520
521
        $this->setFieldOriginalValue();
522
523
        // Handle validation errors.
524
        if ($validationMessages = $this->validationMessages($data)) {
525
            if (!$isEditable) {
526
                return back()->withInput()->withErrors($validationMessages);
527
            } else {
528
                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...
529
            }
530
        }
531
532
        if (($response = $this->prepare($data)) instanceof Response) {
533
            return $response;
534
        }
535
536 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...
537
            $updates = $this->prepareUpdate($this->updates);
538
539
            foreach ($updates as $column => $value) {
540
                /* @var Model $this->model */
541
                $this->model->setAttribute($column, $value);
542
            }
543
544
            $this->model->save();
545
546
            $this->updateRelation($this->relations);
547
        });
548
549
        if (($result = $this->complete($this->saved)) instanceof Response) {
550
            return $result;
551
        }
552
553
        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 554 which is incompatible with the return type documented by Encore\Admin\Form::update of type Symfony\Component\HttpFoundation\Response.
Loading history...
554
            return $response;
555
        }
556
557
        return $this->redirectAfterUpdate();
558
    }
559
560
    /**
561
     * Get RedirectResponse after update.
562
     *
563
     * @return \Illuminate\Http\RedirectResponse
564
     */
565 View Code Duplication
    protected function redirectAfterUpdate()
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...
566
    {
567
        admin_toastr(trans('admin.update_succeeded'));
568
569
        $url = Input::get(Builder::PREVIOUS_URL_KEY) ?: $this->resource(-1);
570
571
        return redirect($url);
572
    }
573
574
    /**
575
     * Check if request is from editable.
576
     *
577
     * @param array $input
578
     *
579
     * @return bool
580
     */
581
    protected function isEditable(array $input = [])
582
    {
583
        return array_key_exists('_editable', $input);
584
    }
585
586
    /**
587
     * Handle editable update.
588
     *
589
     * @param array $input
590
     *
591
     * @return array
592
     */
593
    protected function handleEditable(array $input = [])
594
    {
595
        if (array_key_exists('_editable', $input)) {
596
            $name = $input['name'];
597
            $value = $input['value'];
598
599
            array_forget($input, ['pk', 'value', 'name']);
600
            array_set($input, $name, $value);
601
        }
602
603
        return $input;
604
    }
605
606
    /**
607
     * @param array $input
608
     *
609
     * @return array
610
     */
611
    protected function handleFileDelete(array $input = [])
612
    {
613
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
614
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
615
            unset($input['key']);
616
        }
617
618
        Input::replace($input);
619
620
        return $input;
621
    }
622
623
    /**
624
     * Handle orderable update.
625
     *
626
     * @param int   $id
627
     * @param array $input
628
     *
629
     * @return bool
630
     */
631
    protected function handleOrderable($id, array $input = [])
632
    {
633
        if (array_key_exists('_orderable', $input)) {
634
            $model = $this->model->find($id);
635
636
            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...
637
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
638
639
                return true;
640
            }
641
        }
642
643
        return false;
644
    }
645
646
    /**
647
     * Update relation data.
648
     *
649
     * @param array $relationsData
650
     *
651
     * @return void
652
     */
653
    protected function updateRelation($relationsData)
654
    {
655
        foreach ($relationsData as $name => $values) {
656
            if (!method_exists($this->model, $name)) {
657
                continue;
658
            }
659
660
            $relation = $this->model->$name();
661
662
            $oneToOneRelation = $relation instanceof \Illuminate\Database\Eloquent\Relations\HasOne
663
                || $relation instanceof \Illuminate\Database\Eloquent\Relations\MorphOne;
664
665
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
666
667
            if (empty($prepared)) {
668
                continue;
669
            }
670
671
            switch (get_class($relation)) {
672
                case \Illuminate\Database\Eloquent\Relations\BelongsToMany::class:
673
                case \Illuminate\Database\Eloquent\Relations\MorphToMany::class:
674
                    if (isset($prepared[$name])) {
675
                        $relation->sync($prepared[$name]);
676
                    }
677
                    break;
678
                case \Illuminate\Database\Eloquent\Relations\HasOne::class:
679
680
                    $related = $this->model->$name;
681
682
                    // if related is empty
683
                    if (is_null($related)) {
684
                        $related = $relation->getRelated();
685
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
686
                    }
687
688
                    foreach ($prepared[$name] as $column => $value) {
689
                        $related->setAttribute($column, $value);
690
                    }
691
692
                    $related->save();
693
                    break;
694
                case \Illuminate\Database\Eloquent\Relations\MorphOne::class:
695
                    $related = $this->model->$name;
696
                    if (is_null($related)) {
697
                        $related = $relation->make();
698
                    }
699
                    foreach ($prepared[$name] as $column => $value) {
700
                        $related->setAttribute($column, $value);
701
                    }
702
                    $related->save();
703
                    break;
704
                case \Illuminate\Database\Eloquent\Relations\HasMany::class:
705
                case \Illuminate\Database\Eloquent\Relations\MorphMany::class:
706
707
                    foreach ($prepared[$name] as $related) {
708
                        $relation = $this->model()->$name();
709
710
                        $keyName = $relation->getRelated()->getKeyName();
711
712
                        $instance = $relation->findOrNew(array_get($related, $keyName));
713
714
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
715
                            $instance->delete();
716
717
                            continue;
718
                        }
719
720
                        array_forget($related, static::REMOVE_FLAG_NAME);
721
722
                        $instance->fill($related);
723
724
                        $instance->save();
725
                    }
726
727
                    break;
728
            }
729
        }
730
    }
731
732
    /**
733
     * Prepare input data for update.
734
     *
735
     * @param array $updates
736
     * @param bool  $oneToOneRelation If column is one-to-one relation.
737
     *
738
     * @return array
739
     */
740
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
741
    {
742
        $prepared = [];
743
744
        foreach ($this->builder->fields() as $field) {
745
            $columns = $field->column();
746
747
            // If column not in input array data, then continue.
748
            if (!array_has($updates, $columns)) {
749
                continue;
750
            }
751
752
            if ($this->invalidColumn($columns, $oneToOneRelation)) {
753
                continue;
754
            }
755
756
            $value = $this->getDataByColumn($updates, $columns);
757
758
            $value = $field->prepare($value);
759
760 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...
761
                foreach ($columns as $name => $column) {
762
                    array_set($prepared, $column, $value[$name]);
763
                }
764
            } elseif (is_string($columns)) {
765
                array_set($prepared, $columns, $value);
766
            }
767
        }
768
769
        return $prepared;
770
    }
771
772
    /**
773
     * @param string|array $columns
774
     * @param bool         $oneToOneRelation
775
     *
776
     * @return bool
777
     */
778
    protected function invalidColumn($columns, $oneToOneRelation = false)
779
    {
780
        foreach ((array) $columns as $column) {
781
            if ((!$oneToOneRelation && Str::contains($column, '.')) ||
782
                ($oneToOneRelation && !Str::contains($column, '.'))) {
783
                return true;
784
            }
785
        }
786
787
        return false;
788
    }
789
790
    /**
791
     * Prepare input data for insert.
792
     *
793
     * @param $inserts
794
     *
795
     * @return array
796
     */
797
    protected function prepareInsert($inserts)
798
    {
799
        if ($this->isHasOneRelation($inserts)) {
800
            $inserts = array_dot($inserts);
801
        }
802
803
        foreach ($inserts as $column => $value) {
804
            if (is_null($field = $this->getFieldByColumn($column))) {
805
                unset($inserts[$column]);
806
                continue;
807
            }
808
809
            $inserts[$column] = $field->prepare($value);
810
        }
811
812
        $prepared = [];
813
814
        foreach ($inserts as $key => $value) {
815
            array_set($prepared, $key, $value);
816
        }
817
818
        return $prepared;
819
    }
820
821
    /**
822
     * Is input data is has-one relation.
823
     *
824
     * @param array $inserts
825
     *
826
     * @return bool
827
     */
828
    protected function isHasOneRelation($inserts)
829
    {
830
        $first = current($inserts);
831
832
        if (!is_array($first)) {
833
            return false;
834
        }
835
836
        if (is_array(current($first))) {
837
            return false;
838
        }
839
840
        return Arr::isAssoc($first);
841
    }
842
843
    /**
844
     * Set submitted callback.
845
     *
846
     * @param Closure $callback
847
     *
848
     * @return void
849
     */
850
    public function submitted(Closure $callback)
851
    {
852
        $this->submitted = $callback;
853
    }
854
855
    /**
856
     * Set saving callback.
857
     *
858
     * @param Closure $callback
859
     *
860
     * @return void
861
     */
862
    public function saving(Closure $callback)
863
    {
864
        $this->saving = $callback;
865
    }
866
867
    /**
868
     * Set saved callback.
869
     *
870
     * @param callable $callback
871
     *
872
     * @return void
873
     */
874
    public function saved(Closure $callback)
875
    {
876
        $this->saved = $callback;
877
    }
878
879
    /**
880
     * Ignore fields to save.
881
     *
882
     * @param string|array $fields
883
     *
884
     * @return $this
885
     */
886
    public function ignore($fields)
887
    {
888
        $this->ignored = array_merge($this->ignored, (array) $fields);
889
890
        return $this;
891
    }
892
893
    /**
894
     * @param array        $data
895
     * @param string|array $columns
896
     *
897
     * @return array|mixed
898
     */
899 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...
900
    {
901
        if (is_string($columns)) {
902
            return array_get($data, $columns);
903
        }
904
905
        if (is_array($columns)) {
906
            $value = [];
907
            foreach ($columns as $name => $column) {
908
                if (!array_has($data, $column)) {
909
                    continue;
910
                }
911
                $value[$name] = array_get($data, $column);
912
            }
913
914
            return $value;
915
        }
916
    }
917
918
    /**
919
     * Find field object by column.
920
     *
921
     * @param $column
922
     *
923
     * @return mixed
924
     */
925
    protected function getFieldByColumn($column)
926
    {
927
        return $this->builder->fields()->first(
928
            function (Field $field) use ($column) {
929
                if (is_array($field->column())) {
930
                    return in_array($column, $field->column());
931
                }
932
933
                return $field->column() == $column;
934
            }
935
        );
936
    }
937
938
    /**
939
     * Set original data for each field.
940
     *
941
     * @return void
942
     */
943
    protected function setFieldOriginalValue()
944
    {
945
//        static::doNotSnakeAttributes($this->model);
946
947
        $values = $this->model->toArray();
948
949
        $this->builder->fields()->each(function (Field $field) use ($values) {
950
            $field->setOriginal($values);
951
        });
952
    }
953
954
    /**
955
     * Set all fields value in form.
956
     *
957
     * @param $id
958
     *
959
     * @return void
960
     */
961
    protected function setFieldValue($id)
962
    {
963
        $relations = $this->getRelations();
964
965
        $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...
966
967
//        static::doNotSnakeAttributes($this->model);
968
969
        $data = $this->model->toArray();
970
971
        $this->builder->fields()->each(function (Field $field) use ($data) {
972
            if (!in_array($field->column(), $this->ignored)) {
973
                $field->fill($data);
974
            }
975
        });
976
    }
977
978
    /**
979
     * Don't snake case attributes.
980
     *
981
     * @param Model $model
982
     *
983
     * @return void
984
     */
985
    protected static function doNotSnakeAttributes(Model $model)
986
    {
987
        $class = get_class($model);
988
989
        $class::$snakeAttributes = false;
990
    }
991
992
    /**
993
     * Get validation messages.
994
     *
995
     * @param array $input
996
     *
997
     * @return MessageBag|bool
998
     */
999
    protected function validationMessages($input)
1000
    {
1001
        $failedValidators = [];
1002
1003
        foreach ($this->builder->fields() as $field) {
1004
            if (!$validator = $field->getValidator($input)) {
1005
                continue;
1006
            }
1007
1008
            if (($validator instanceof Validator) && !$validator->passes()) {
1009
                $failedValidators[] = $validator;
1010
            }
1011
        }
1012
1013
        $message = $this->mergeValidationMessages($failedValidators);
1014
1015
        return $message->any() ? $message : false;
1016
    }
1017
1018
    /**
1019
     * Merge validation messages from input validators.
1020
     *
1021
     * @param \Illuminate\Validation\Validator[] $validators
1022
     *
1023
     * @return MessageBag
1024
     */
1025
    protected function mergeValidationMessages($validators)
1026
    {
1027
        $messageBag = new MessageBag();
1028
1029
        foreach ($validators as $validator) {
1030
            $messageBag = $messageBag->merge($validator->messages());
1031
        }
1032
1033
        return $messageBag;
1034
    }
1035
1036
    /**
1037
     * Get all relations of model from callable.
1038
     *
1039
     * @return array
1040
     */
1041
    public function getRelations()
1042
    {
1043
        $relations = $columns = [];
1044
1045
        foreach ($this->builder->fields() as $field) {
1046
            $columns[] = $field->column();
1047
        }
1048
1049
        foreach (array_flatten($columns) as $column) {
1050
            if (str_contains($column, '.')) {
1051
                list($relation) = explode('.', $column);
1052
1053
                if (method_exists($this->model, $relation) &&
1054
                    $this->model->$relation() instanceof Relation
1055
                ) {
1056
                    $relations[] = $relation;
1057
                }
1058
            } elseif (method_exists($this->model, $column) &&
1059
                !method_exists(Model::class, $column)
1060
            ) {
1061
                $relations[] = $column;
1062
            }
1063
        }
1064
1065
        return array_unique($relations);
1066
    }
1067
1068
    /**
1069
     * Set action for form.
1070
     *
1071
     * @param string $action
1072
     *
1073
     * @return $this
1074
     */
1075
    public function setAction($action)
1076
    {
1077
        $this->builder()->setAction($action);
1078
1079
        return $this;
1080
    }
1081
1082
    /**
1083
     * Set field and label width in current form.
1084
     *
1085
     * @param int $fieldWidth
1086
     * @param int $labelWidth
1087
     *
1088
     * @return $this
1089
     */
1090
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1091
    {
1092
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1093
            /* @var Field $field  */
1094
            $field->setWidth($fieldWidth, $labelWidth);
1095
        });
1096
1097
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1098
1099
        return $this;
1100
    }
1101
1102
    /**
1103
     * Set view for form.
1104
     *
1105
     * @param string $view
1106
     *
1107
     * @return $this
1108
     */
1109
    public function setView($view)
1110
    {
1111
        $this->builder()->setView($view);
1112
1113
        return $this;
1114
    }
1115
1116
    /**
1117
     * Set title for form.
1118
     *
1119
     * @param string $title
1120
     *
1121
     * @return $this
1122
     */
1123
    public function setTitle($title = '')
1124
    {
1125
        $this->builder()->setTitle($title);
1126
1127
        return $this;
1128
    }
1129
1130
    /**
1131
     * Add a row in form.
1132
     *
1133
     * @param Closure $callback
1134
     *
1135
     * @return $this
1136
     */
1137
    public function row(Closure $callback)
1138
    {
1139
        $this->rows[] = new Row($callback, $this);
1140
1141
        return $this;
1142
    }
1143
1144
    /**
1145
     * Tools setting for form.
1146
     *
1147
     * @param Closure $callback
1148
     */
1149
    public function tools(Closure $callback)
1150
    {
1151
        $callback->call($this, $this->builder->getTools());
1152
    }
1153
1154
    /**
1155
     * Disable form submit.
1156
     *
1157
     * @return $this
1158
     */
1159
    public function disableSubmit()
1160
    {
1161
        $this->builder()->options(['enableSubmit' => false]);
1162
1163
        return $this;
1164
    }
1165
1166
    /**
1167
     * Disable form reset.
1168
     *
1169
     * @return $this
1170
     */
1171
    public function disableReset()
1172
    {
1173
        $this->builder()->options(['enableReset' => false]);
1174
1175
        return $this;
1176
    }
1177
1178
    /**
1179
     * Disable form remove reserved fields.
1180
     *
1181
     * @return $this
1182
     */
1183
    public function disableRemoveReservedFields()
1184
    {
1185
        $this->builder()->options(['enableRemoveReservedFields' => false]);
1186
1187
        return $this;
1188
    }
1189
1190
    /**
1191
     * Get current resource route url.
1192
     *
1193
     * @param int $slice
1194
     *
1195
     * @return string
1196
     */
1197
    public function resource($slice = -2)
1198
    {
1199
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1200
1201
        if ($slice != 0) {
1202
            $segments = array_slice($segments, 0, $slice);
1203
        }
1204
        // # fix #1768
1205
        if ($segments[0] == 'http:' && config('admin.secure') == true) {
1206
            $segments[0] = 'https:';
1207
        }
1208
1209
        return implode('/', $segments);
1210
    }
1211
1212
    /**
1213
     * Render the form contents.
1214
     *
1215
     * @return string
1216
     */
1217
    public function render()
1218
    {
1219
        try {
1220
            return $this->builder->render();
1221
        } catch (\Exception $e) {
1222
            return Handler::renderException($e);
1223
        }
1224
    }
1225
1226
    /**
1227
     * Get or set input data.
1228
     *
1229
     * @param string $key
1230
     * @param null   $value
1231
     *
1232
     * @return array|mixed
1233
     */
1234
    public function input($key, $value = null)
1235
    {
1236
        if (is_null($value)) {
1237
            return array_get($this->inputs, $key);
1238
        }
1239
1240
        return array_set($this->inputs, $key, $value);
1241
    }
1242
1243
    /**
1244
     * Register builtin fields.
1245
     *
1246
     * @return void
1247
     */
1248
    public static function registerBuiltinFields()
1249
    {
1250
        $map = [
1251
            'button'         => \Encore\Admin\Form\Field\Button::class,
1252
            'checkbox'       => \Encore\Admin\Form\Field\Checkbox::class,
1253
            'color'          => \Encore\Admin\Form\Field\Color::class,
1254
            'currency'       => \Encore\Admin\Form\Field\Currency::class,
1255
            'date'           => \Encore\Admin\Form\Field\Date::class,
1256
            'dateRange'      => \Encore\Admin\Form\Field\DateRange::class,
1257
            'datetime'       => \Encore\Admin\Form\Field\Datetime::class,
1258
            'dateTimeRange'  => \Encore\Admin\Form\Field\DatetimeRange::class,
1259
            'datetimeRange'  => \Encore\Admin\Form\Field\DatetimeRange::class,
1260
            'decimal'        => \Encore\Admin\Form\Field\Decimal::class,
1261
            'display'        => \Encore\Admin\Form\Field\Display::class,
1262
            'divider'        => \Encore\Admin\Form\Field\Divide::class,
1263
            'divide'         => \Encore\Admin\Form\Field\Divide::class,
1264
            'embeds'         => \Encore\Admin\Form\Field\Embeds::class,
1265
            'editor'         => \Encore\Admin\Form\Field\Editor::class,
1266
            'email'          => \Encore\Admin\Form\Field\Email::class,
1267
            'file'           => \Encore\Admin\Form\Field\File::class,
1268
            'hasMany'        => \Encore\Admin\Form\Field\HasMany::class,
1269
            'hidden'         => \Encore\Admin\Form\Field\Hidden::class,
1270
            'id'             => \Encore\Admin\Form\Field\Id::class,
1271
            'image'          => \Encore\Admin\Form\Field\Image::class,
1272
            'ip'             => \Encore\Admin\Form\Field\Ip::class,
1273
            'map'            => \Encore\Admin\Form\Field\Map::class,
1274
            'mobile'         => \Encore\Admin\Form\Field\Mobile::class,
1275
            'month'          => \Encore\Admin\Form\Field\Month::class,
1276
            'multipleSelect' => \Encore\Admin\Form\Field\MultipleSelect::class,
1277
            'number'         => \Encore\Admin\Form\Field\Number::class,
1278
            'password'       => \Encore\Admin\Form\Field\Password::class,
1279
            'radio'          => \Encore\Admin\Form\Field\Radio::class,
1280
            'rate'           => \Encore\Admin\Form\Field\Rate::class,
1281
            'select'         => \Encore\Admin\Form\Field\Select::class,
1282
            'slider'         => \Encore\Admin\Form\Field\Slider::class,
1283
            'switch'         => \Encore\Admin\Form\Field\SwitchField::class,
1284
            'text'           => \Encore\Admin\Form\Field\Text::class,
1285
            'textarea'       => \Encore\Admin\Form\Field\Textarea::class,
1286
            'time'           => \Encore\Admin\Form\Field\Time::class,
1287
            'timeRange'      => \Encore\Admin\Form\Field\TimeRange::class,
1288
            'url'            => \Encore\Admin\Form\Field\Url::class,
1289
            'year'           => \Encore\Admin\Form\Field\Year::class,
1290
            'html'           => \Encore\Admin\Form\Field\Html::class,
1291
            'tags'           => \Encore\Admin\Form\Field\Tags::class,
1292
            'icon'           => \Encore\Admin\Form\Field\Icon::class,
1293
            'multipleFile'   => \Encore\Admin\Form\Field\MultipleFile::class,
1294
            'multipleImage'  => \Encore\Admin\Form\Field\MultipleImage::class,
1295
            'captcha'        => \Encore\Admin\Form\Field\Captcha::class,
1296
            'listbox'        => \Encore\Admin\Form\Field\Listbox::class,
1297
        ];
1298
1299
        foreach ($map as $abstract => $class) {
1300
            static::extend($abstract, $class);
1301
        }
1302
    }
1303
1304
    /**
1305
     * Register custom field.
1306
     *
1307
     * @param string $abstract
1308
     * @param string $class
1309
     *
1310
     * @return void
1311
     */
1312
    public static function extend($abstract, $class)
1313
    {
1314
        static::$availableFields[$abstract] = $class;
1315
    }
1316
1317
    /**
1318
     * Remove registered field.
1319
     *
1320
     * @param array|string $abstract
1321
     */
1322
    public static function forget($abstract)
1323
    {
1324
        array_forget(static::$availableFields, $abstract);
1325
    }
1326
1327
    /**
1328
     * Find field class.
1329
     *
1330
     * @param string $method
1331
     *
1332
     * @return bool|mixed
1333
     */
1334 View Code Duplication
    public static function findFieldClass($method)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1335
    {
1336
        $class = array_get(static::$availableFields, $method);
1337
1338
        if (class_exists($class)) {
1339
            return $class;
1340
        }
1341
1342
        return false;
1343
    }
1344
1345
    /**
1346
     * Collect assets required by registered field.
1347
     *
1348
     * @return array
1349
     */
1350
    public static function collectFieldAssets()
1351
    {
1352
        if (!empty(static::$collectedAssets)) {
1353
            return static::$collectedAssets;
1354
        }
1355
1356
        $css = collect();
1357
        $js = collect();
1358
1359
        foreach (static::$availableFields as $field) {
1360
            if (!method_exists($field, 'getAssets')) {
1361
                continue;
1362
            }
1363
1364
            $assets = call_user_func([$field, 'getAssets']);
1365
1366
            $css->push(array_get($assets, 'css'));
1367
            $js->push(array_get($assets, 'js'));
1368
        }
1369
1370
        return static::$collectedAssets = [
1371
            'css' => $css->flatten()->unique()->filter()->toArray(),
1372
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1373
        ];
1374
    }
1375
1376
    /**
1377
     * Getter.
1378
     *
1379
     * @param string $name
1380
     *
1381
     * @return array|mixed
1382
     */
1383
    public function __get($name)
1384
    {
1385
        return $this->input($name);
1386
    }
1387
1388
    /**
1389
     * Setter.
1390
     *
1391
     * @param string $name
1392
     * @param $value
1393
     */
1394
    public function __set($name, $value)
1395
    {
1396
        $this->input($name, $value);
1397
    }
1398
1399
    /**
1400
     * Generate a Field object and add to form builder if Field exists.
1401
     *
1402
     * @param string $method
1403
     * @param array  $arguments
1404
     *
1405
     * @return Field|void
1406
     */
1407 View Code Duplication
    public function __call($method, $arguments)
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...
1408
    {
1409
        if ($className = static::findFieldClass($method)) {
1410
            $column = array_get($arguments, 0, ''); //[0];
1411
1412
            $element = new $className($column, array_slice($arguments, 1));
1413
1414
            $this->pushField($element);
1415
1416
            return $element;
1417
        }
1418
    }
1419
1420
    /**
1421
     * Render the contents of the form when casting to string.
1422
     *
1423
     * @return string
1424
     */
1425
    public function __toString()
1426
    {
1427
        return $this->render();
1428
    }
1429
}
1430