Completed
Pull Request — master (#1350)
by
unknown
02:50
created

Form::getRules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
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\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([
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)
502
    {
503
        $data = Input::all();
504
505
        $data = $this->handleEditable($data);
506
507
        $data = $this->handleFileDelete($data);
508
509
        if ($this->handleOrderable($id, $data)) {
510
            return response([
0 ignored issues
show
Documentation introduced by
array('status' => true, ...min.update_succeeded')) is of type array<string,boolean|obj...r>|string|array|null"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
511
                'status'  => true,
512
                'message' => trans('admin.update_succeeded'),
513
            ]);
514
        }
515
516
        /* @var Model $this->model */
517
        $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...
518
519
        $this->setFieldOriginalValue();
520
521
        // Handle validation errors.
522
        if ($validationMessages = $this->validationMessages($data)) {
523
            return back()->withInput()->withErrors($validationMessages);
524
        }
525
526
        if (($response = $this->prepare($data)) instanceof Response) {
527
            return $response;
528
        }
529
530 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...
531
            $updates = $this->prepareUpdate($this->updates);
532
533
            foreach ($updates as $column => $value) {
534
                /* @var Model $this->model */
535
                $this->model->setAttribute($column, $value);
536
            }
537
538
            $this->model->save();
539
540
            $this->updateRelation($this->relations);
541
        });
542
543
        if (($result = $this->complete($this->saved)) instanceof Response) {
544
            return $result;
545
        }
546
547
        if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) {
548
            return $response;
549
        }
550
551
        return $this->redirectAfterUpdate();
552
    }
553
554
    /**
555
     * Get RedirectResponse after update.
556
     *
557
     * @return \Illuminate\Http\RedirectResponse
558
     */
559 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...
560
    {
561
        admin_toastr(trans('admin.update_succeeded'));
562
563
        $url = Input::get(Builder::PREVIOUS_URL_KEY) ?: $this->resource(-1);
564
565
        return redirect($url);
566
    }
567
568
    /**
569
     * Handle editable update.
570
     *
571
     * @param array $input
572
     *
573
     * @return array
574
     */
575
    protected function handleEditable(array $input = [])
576
    {
577
        if (array_key_exists('_editable', $input)) {
578
            $name = $input['name'];
579
            $value = $input['value'];
580
581
            array_forget($input, ['pk', 'value', 'name']);
582
            array_set($input, $name, $value);
583
        }
584
585
        return $input;
586
    }
587
588
    /**
589
     * @param array $input
590
     *
591
     * @return array
592
     */
593
    protected function handleFileDelete(array $input = [])
594
    {
595
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
596
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
597
            unset($input['key']);
598
        }
599
600
        Input::replace($input);
601
602
        return $input;
603
    }
604
605
    /**
606
     * Handle orderable update.
607
     *
608
     * @param int   $id
609
     * @param array $input
610
     *
611
     * @return bool
612
     */
613
    protected function handleOrderable($id, array $input = [])
614
    {
615
        if (array_key_exists('_orderable', $input)) {
616
            $model = $this->model->find($id);
617
618
            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...
619
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
620
621
                return true;
622
            }
623
        }
624
625
        return false;
626
    }
627
628
    /**
629
     * Update relation data.
630
     *
631
     * @param array $relationsData
632
     *
633
     * @return void
634
     */
635
    protected function updateRelation($relationsData)
636
    {
637
        foreach ($relationsData as $name => $values) {
638
            if (!method_exists($this->model, $name)) {
639
                continue;
640
            }
641
642
            $relation = $this->model->$name();
643
644
            $oneToOneRelation = $relation instanceof \Illuminate\Database\Eloquent\Relations\HasOne
645
                || $relation instanceof \Illuminate\Database\Eloquent\Relations\MorphOne;
646
647
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
648
649
            if (empty($prepared)) {
650
                continue;
651
            }
652
653
            switch (get_class($relation)) {
654
                case \Illuminate\Database\Eloquent\Relations\BelongsToMany::class:
655
                case \Illuminate\Database\Eloquent\Relations\MorphToMany::class:
656
                    if (isset($prepared[$name])) {
657
                        $relation->sync($prepared[$name]);
658
                    }
659
                    break;
660
                case \Illuminate\Database\Eloquent\Relations\HasOne::class:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

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

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

    doSomethingElse(); //wrong
    break;

}

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

Loading history...
661
662
                    $related = $this->model->$name;
663
664
                    // if related is empty
665
                    if (is_null($related)) {
666
                        $related = $relation->getRelated();
667
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
668
                    }
669
670
                    foreach ($prepared[$name] as $column => $value) {
671
                        $related->setAttribute($column, $value);
672
                    }
673
674
                    $related->save();
675
                    break;
676
                case \Illuminate\Database\Eloquent\Relations\MorphOne::class:
677
                    $related = $this->model->$name;
678
                    if (is_null($related)) {
679
                        $related = $relation->make();
680
                    }
681
                    foreach ($prepared[$name] as $column => $value) {
682
                        $related->setAttribute($column, $value);
683
                    }
684
                    $related->save();
685
                    break;
686
                case \Illuminate\Database\Eloquent\Relations\HasMany::class:
687
                case \Illuminate\Database\Eloquent\Relations\MorphMany::class:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

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

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

    doSomethingElse(); //wrong
    break;

}

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

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

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

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

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

Loading history...
928
929
        $values = $this->model->toArray();
930
931
        $this->builder->fields()->each(function (Field $field) use ($values) {
932
            $field->setOriginal($values);
933
        });
934
    }
935
936
    /**
937
     * Set all fields value in form.
938
     *
939
     * @param $id
940
     *
941
     * @return void
942
     */
943
    protected function setFieldValue($id)
944
    {
945
        $relations = $this->getRelations();
946
947
        $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...
948
949
//        static::doNotSnakeAttributes($this->model);
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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