Completed
Pull Request — master (#2254)
by
unknown
02:53
created

Form::disableRemoveReservedFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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