Completed
Pull Request — master (#1782)
by
unknown
02:55
created

Form::complete()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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