Completed
Push — master ( b0d73a...d2854c )
by Song
02:52
created

Form::view()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 9
rs 9.9666
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 = array_merge($this->removeIgnoredFields($data), $this->inputs);
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
     * @return \Illuminate\Http\RedirectResponse
552
     */
553
    protected function redirectAfterUpdate($key)
554
    {
555
        $resourcesPath = $this->resource(-1);
556
557
        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 557 which is incompatible with the return type documented by Encore\Admin\Form::redirectAfterUpdate of type Illuminate\Http\RedirectResponse.
Loading history...
558
    }
559
560
    /**
561
     * Get RedirectResponse after data saving.
562
     *
563
     * @param string $resourcesPath
564
     * @param string $key
565
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
566
     */
567
    protected function redirectAfterSaving($resourcesPath, $key)
568
    {
569
        if (request('after-save') == 1) {
570
            // continue editing
571
            $url = rtrim($resourcesPath, '/') . "/{$key}/edit";
572
        } elseif (request('after-save') == 2) {
573
            // view resource
574
            $url = rtrim($resourcesPath, '/') . "/{$key}";
575
        } else {
576
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
577
        }
578
579
        admin_toastr(trans('admin.save_succeeded'));
580
581
        return redirect($url);
582
    }
583
584
    /**
585
     * Check if request is from editable.
586
     *
587
     * @param array $input
588
     *
589
     * @return bool
590
     */
591
    protected function isEditable(array $input = [])
592
    {
593
        return array_key_exists('_editable', $input);
594
    }
595
596
    /**
597
     * Handle editable update.
598
     *
599
     * @param array $input
600
     *
601
     * @return array
602
     */
603
    protected function handleEditable(array $input = [])
604
    {
605
        if (array_key_exists('_editable', $input)) {
606
            $name = $input['name'];
607
            $value = $input['value'];
608
609
            array_forget($input, ['pk', 'value', 'name']);
610
            array_set($input, $name, $value);
611
        }
612
613
        return $input;
614
    }
615
616
    /**
617
     * @param array $input
618
     *
619
     * @return array
620
     */
621
    protected function handleFileDelete(array $input = [])
622
    {
623
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
624
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
625
            unset($input['key']);
626
        }
627
628
        Input::replace($input);
629
630
        return $input;
631
    }
632
633
    /**
634
     * Handle orderable update.
635
     *
636
     * @param int   $id
637
     * @param array $input
638
     *
639
     * @return bool
640
     */
641
    protected function handleOrderable($id, array $input = [])
642
    {
643
        if (array_key_exists('_orderable', $input)) {
644
            $model = $this->model->find($id);
645
646
            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...
647
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
648
649
                return true;
650
            }
651
        }
652
653
        return false;
654
    }
655
656
    /**
657
     * Update relation data.
658
     *
659
     * @param array $relationsData
660
     *
661
     * @return void
662
     */
663
    protected function updateRelation($relationsData)
664
    {
665
        foreach ($relationsData as $name => $values) {
666
            if (!method_exists($this->model, $name)) {
667
                continue;
668
            }
669
670
            $relation = $this->model->$name();
671
672
            $oneToOneRelation = $relation instanceof Relations\HasOne
673
                || $relation instanceof Relations\MorphOne;
674
675
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
676
677
            if (empty($prepared)) {
678
                continue;
679
            }
680
681
            switch (get_class($relation)) {
682
                case Relations\BelongsToMany::class:
683
                case Relations\MorphToMany::class:
684
                    if (isset($prepared[$name])) {
685
                        $relation->sync($prepared[$name]);
686
                    }
687
                    break;
688
                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...
689
690
                    $related = $this->model->$name;
691
692
                    // if related is empty
693
                    if (is_null($related)) {
694
                        $related = $relation->getRelated();
695
                        $related->{$relation->getForeignKeyName()} = $this->model->{$this->model->getKeyName()};
696
                    }
697
698
                    foreach ($prepared[$name] as $column => $value) {
699
                        $related->setAttribute($column, $value);
700
                    }
701
702
                    $related->save();
703
                    break;
704
                case Relations\MorphOne::class:
705
                    $related = $this->model->$name;
706
                    if (is_null($related)) {
707
                        $related = $relation->make();
708
                    }
709
                    foreach ($prepared[$name] as $column => $value) {
710
                        $related->setAttribute($column, $value);
711
                    }
712
                    $related->save();
713
                    break;
714
                case Relations\HasMany::class:
715
                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...
716
717
                    foreach ($prepared[$name] as $related) {
718
                        /** @var Relations\Relation $relation */
719
                        $relation = $this->model()->$name();
720
721
                        $keyName = $relation->getRelated()->getKeyName();
722
723
                        $instance = $relation->findOrNew(array_get($related, $keyName));
724
725
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
726
                            $instance->delete();
727
728
                            continue;
729
                        }
730
731
                        array_forget($related, static::REMOVE_FLAG_NAME);
732
733
                        $instance->fill($related);
734
735
                        $instance->save();
736
                    }
737
738
                    break;
739
            }
740
        }
741
    }
742
743
    /**
744
     * Prepare input data for update.
745
     *
746
     * @param array $updates
747
     * @param bool  $oneToOneRelation If column is one-to-one relation.
748
     *
749
     * @return array
750
     */
751
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
752
    {
753
        $prepared = [];
754
755
        /** @var Field $field */
756
        foreach ($this->builder->fields() as $field) {
757
            $columns = $field->column();
758
759
            // If column not in input array data, then continue.
760
            if (!array_has($updates, $columns)) {
761
                continue;
762
            }
763
764
            if ($this->invalidColumn($columns, $oneToOneRelation)) {
765
                continue;
766
            }
767
768
            $value = $this->getDataByColumn($updates, $columns);
769
770
            $value = $field->prepare($value);
771
772 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...
773
                foreach ($columns as $name => $column) {
774
                    array_set($prepared, $column, $value[$name]);
775
                }
776
            } elseif (is_string($columns)) {
777
                array_set($prepared, $columns, $value);
778
            }
779
        }
780
781
        return $prepared;
782
    }
783
784
    /**
785
     * @param string|array $columns
786
     * @param bool         $oneToOneRelation
787
     *
788
     * @return bool
789
     */
790
    protected function invalidColumn($columns, $oneToOneRelation = false)
791
    {
792
        foreach ((array) $columns as $column) {
793
            if ((!$oneToOneRelation && Str::contains($column, '.')) ||
794
                ($oneToOneRelation && !Str::contains($column, '.'))) {
795
                return true;
796
            }
797
        }
798
799
        return false;
800
    }
801
802
    /**
803
     * Prepare input data for insert.
804
     *
805
     * @param $inserts
806
     *
807
     * @return array
808
     */
809
    protected function prepareInsert($inserts)
810
    {
811
        if ($this->isHasOneRelation($inserts)) {
812
            $inserts = array_dot($inserts);
813
        }
814
815
        foreach ($inserts as $column => $value) {
816
            if (is_null($field = $this->getFieldByColumn($column))) {
817
                unset($inserts[$column]);
818
                continue;
819
            }
820
821
            $inserts[$column] = $field->prepare($value);
822
        }
823
824
        $prepared = [];
825
826
        foreach ($inserts as $key => $value) {
827
            array_set($prepared, $key, $value);
828
        }
829
830
        return $prepared;
831
    }
832
833
    /**
834
     * Is input data is has-one relation.
835
     *
836
     * @param array $inserts
837
     *
838
     * @return bool
839
     */
840
    protected function isHasOneRelation($inserts)
841
    {
842
        $first = current($inserts);
843
844
        if (!is_array($first)) {
845
            return false;
846
        }
847
848
        if (is_array(current($first))) {
849
            return false;
850
        }
851
852
        return Arr::isAssoc($first);
853
    }
854
855
    /**
856
     * Set submitted callback.
857
     *
858
     * @param Closure $callback
859
     *
860
     * @return void
861
     */
862
    public function submitted(Closure $callback)
863
    {
864
        $this->submitted = $callback;
865
    }
866
867
    /**
868
     * Set saving callback.
869
     *
870
     * @param Closure $callback
871
     *
872
     * @return void
873
     */
874
    public function saving(Closure $callback)
875
    {
876
        $this->saving = $callback;
877
    }
878
879
    /**
880
     * Set saved callback.
881
     *
882
     * @param Closure $callback
883
     *
884
     * @return void
885
     */
886
    public function saved(Closure $callback)
887
    {
888
        $this->saved = $callback;
889
    }
890
891
    /**
892
     * Ignore fields to save.
893
     *
894
     * @param string|array $fields
895
     *
896
     * @return $this
897
     */
898
    public function ignore($fields)
899
    {
900
        $this->ignored = array_merge($this->ignored, (array) $fields);
901
902
        return $this;
903
    }
904
905
    /**
906
     * @param array        $data
907
     * @param string|array $columns
908
     *
909
     * @return array|mixed
910
     */
911 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...
912
    {
913
        if (is_string($columns)) {
914
            return array_get($data, $columns);
915
        }
916
917
        if (is_array($columns)) {
918
            $value = [];
919
            foreach ($columns as $name => $column) {
920
                if (!array_has($data, $column)) {
921
                    continue;
922
                }
923
                $value[$name] = array_get($data, $column);
924
            }
925
926
            return $value;
927
        }
928
    }
929
930
    /**
931
     * Find field object by column.
932
     *
933
     * @param $column
934
     *
935
     * @return mixed
936
     */
937
    protected function getFieldByColumn($column)
938
    {
939
        return $this->builder->fields()->first(
940
            function (Field $field) use ($column) {
941
                if (is_array($field->column())) {
942
                    return in_array($column, $field->column());
943
                }
944
945
                return $field->column() == $column;
946
            }
947
        );
948
    }
949
950
    /**
951
     * Set original data for each field.
952
     *
953
     * @return void
954
     */
955
    protected function setFieldOriginalValue()
956
    {
957
//        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...
958
959
        $values = $this->model->toArray();
960
961
        $this->builder->fields()->each(function (Field $field) use ($values) {
962
            $field->setOriginal($values);
963
        });
964
    }
965
966
    /**
967
     * Set all fields value in form.
968
     *
969
     * @param $id
970
     *
971
     * @return void
972
     */
973
    protected function setFieldValue($id)
974
    {
975
        $relations = $this->getRelations();
976
977
        $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...
978
979
//        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...
980
981
        $data = $this->model->toArray();
982
983
        $this->builder->fields()->each(function (Field $field) use ($data) {
984
            if (!in_array($field->column(), $this->ignored)) {
985
                $field->fill($data);
986
            }
987
        });
988
    }
989
990
    /**
991
     * Don't snake case attributes.
992
     *
993
     * @param Model $model
994
     *
995
     * @return void
996
     */
997
    protected static function doNotSnakeAttributes(Model $model)
998
    {
999
        $class = get_class($model);
1000
1001
        $class::$snakeAttributes = false;
1002
    }
1003
1004
    /**
1005
     * Get validation messages.
1006
     *
1007
     * @param array $input
1008
     *
1009
     * @return MessageBag|bool
1010
     */
1011
    protected function validationMessages($input)
1012
    {
1013
        $failedValidators = [];
1014
1015
        /** @var Field $field */
1016
        foreach ($this->builder->fields() as $field) {
1017
            if (!$validator = $field->getValidator($input)) {
1018
                continue;
1019
            }
1020
1021
            if (($validator instanceof Validator) && !$validator->passes()) {
1022
                $failedValidators[] = $validator;
1023
            }
1024
        }
1025
1026
        $message = $this->mergeValidationMessages($failedValidators);
1027
1028
        return $message->any() ? $message : false;
1029
    }
1030
1031
    /**
1032
     * Merge validation messages from input validators.
1033
     *
1034
     * @param \Illuminate\Validation\Validator[] $validators
1035
     *
1036
     * @return MessageBag
1037
     */
1038
    protected function mergeValidationMessages($validators)
1039
    {
1040
        $messageBag = new MessageBag();
1041
1042
        foreach ($validators as $validator) {
1043
            $messageBag = $messageBag->merge($validator->messages());
1044
        }
1045
1046
        return $messageBag;
1047
    }
1048
1049
    /**
1050
     * Get all relations of model from callable.
1051
     *
1052
     * @return array
1053
     */
1054
    public function getRelations()
1055
    {
1056
        $relations = $columns = [];
1057
1058
        /** @var Field $field */
1059
        foreach ($this->builder->fields() as $field) {
1060
            $columns[] = $field->column();
1061
        }
1062
1063
        foreach (array_flatten($columns) as $column) {
1064
            if (str_contains($column, '.')) {
1065
                list($relation) = explode('.', $column);
1066
1067
                if (method_exists($this->model, $relation) &&
1068
                    $this->model->$relation() instanceof Relations\Relation
1069
                ) {
1070
                    $relations[] = $relation;
1071
                }
1072
            } elseif (method_exists($this->model, $column) &&
1073
                !method_exists(Model::class, $column)
1074
            ) {
1075
                $relations[] = $column;
1076
            }
1077
        }
1078
1079
        return array_unique($relations);
1080
    }
1081
1082
    /**
1083
     * Set action for form.
1084
     *
1085
     * @param string $action
1086
     *
1087
     * @return $this
1088
     */
1089
    public function setAction($action)
1090
    {
1091
        $this->builder()->setAction($action);
1092
1093
        return $this;
1094
    }
1095
1096
    /**
1097
     * Set field and label width in current form.
1098
     *
1099
     * @param int $fieldWidth
1100
     * @param int $labelWidth
1101
     *
1102
     * @return $this
1103
     */
1104
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1105
    {
1106
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1107
            /* @var Field $field  */
1108
            $field->setWidth($fieldWidth, $labelWidth);
1109
        });
1110
1111
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1112
1113
        return $this;
1114
    }
1115
1116
    /**
1117
     * Set view for form.
1118
     *
1119
     * @param string $view
1120
     *
1121
     * @return $this
1122
     */
1123
    public function setView($view)
1124
    {
1125
        $this->builder()->setView($view);
1126
1127
        return $this;
1128
    }
1129
1130
    /**
1131
     * Set title for form.
1132
     *
1133
     * @param string $title
1134
     *
1135
     * @return $this
1136
     */
1137
    public function setTitle($title = '')
1138
    {
1139
        $this->builder()->setTitle($title);
1140
1141
        return $this;
1142
    }
1143
1144
    /**
1145
     * Add a row in form.
1146
     *
1147
     * @param Closure $callback
1148
     *
1149
     * @return $this
1150
     */
1151
    public function row(Closure $callback)
1152
    {
1153
        $this->rows[] = new Row($callback, $this);
1154
1155
        return $this;
1156
    }
1157
1158
    /**
1159
     * Tools setting for form.
1160
     *
1161
     * @param Closure $callback
1162
     */
1163
    public function tools(Closure $callback)
1164
    {
1165
        $callback->call($this, $this->builder->getTools());
1166
    }
1167
1168
    /**
1169
     * Disable form submit.
1170
     *
1171
     * @return $this
1172
     *
1173
     * @deprecated
1174
     */
1175
    public function disableSubmit()
1176
    {
1177
        $this->builder()->getFooter()->disableSubmit();
1178
1179
        return $this;
1180
    }
1181
1182
    /**
1183
     * Disable form reset.
1184
     *
1185
     * @return $this
1186
     *
1187
     * @deprecated
1188
     */
1189
    public function disableReset()
1190
    {
1191
        $this->builder()->getFooter()->disableReset();
1192
1193
        return $this;
1194
    }
1195
1196
    /**
1197
     * Footer setting for form.
1198
     *
1199
     * @param Closure $callback
1200
     */
1201
    public function footer(Closure $callback)
1202
    {
1203
        call_user_func($callback, $this->builder()->getFooter());
1204
    }
1205
1206
    /**
1207
     * Get current resource route url.
1208
     *
1209
     * @param int $slice
1210
     *
1211
     * @return string
1212
     */
1213
    public function resource($slice = -2)
1214
    {
1215
        $segments = explode('/', trim(app('request')->getUri(), '/'));
1216
1217
        if ($slice != 0) {
1218
            $segments = array_slice($segments, 0, $slice);
1219
        }
1220
        // # fix #1768
1221
        if ($segments[0] == 'http:' && config('admin.secure') == true) {
1222
            $segments[0] = 'https:';
1223
        }
1224
1225
        return implode('/', $segments);
1226
    }
1227
1228
    /**
1229
     * Render the form contents.
1230
     *
1231
     * @return string
1232
     */
1233
    public function render()
1234
    {
1235
        try {
1236
            return $this->builder->render();
1237
        } catch (\Exception $e) {
1238
            return Handler::renderException($e);
1239
        }
1240
    }
1241
1242
    /**
1243
     * Get or set input data.
1244
     *
1245
     * @param string $key
1246
     * @param null   $value
1247
     *
1248
     * @return array|mixed
1249
     */
1250
    public function input($key, $value = null)
1251
    {
1252
        if (is_null($value)) {
1253
            return array_get($this->inputs, $key);
1254
        }
1255
1256
        return array_set($this->inputs, $key, $value);
1257
    }
1258
1259
    /**
1260
     * Register builtin fields.
1261
     *
1262
     * @return void
1263
     */
1264
    public static function registerBuiltinFields()
1265
    {
1266
        $map = [
1267
            'button'         => Field\Button::class,
1268
            'checkbox'       => Field\Checkbox::class,
1269
            'color'          => Field\Color::class,
1270
            'currency'       => Field\Currency::class,
1271
            'date'           => Field\Date::class,
1272
            'dateRange'      => Field\DateRange::class,
1273
            'datetime'       => Field\Datetime::class,
1274
            'dateTimeRange'  => Field\DatetimeRange::class,
1275
            'datetimeRange'  => Field\DatetimeRange::class,
1276
            'decimal'        => Field\Decimal::class,
1277
            'display'        => Field\Display::class,
1278
            'divider'        => Field\Divide::class,
1279
            'divide'         => Field\Divide::class,
1280
            'embeds'         => Field\Embeds::class,
1281
            'editor'         => Field\Editor::class,
1282
            'email'          => Field\Email::class,
1283
            'file'           => Field\File::class,
1284
            'hasMany'        => Field\HasMany::class,
1285
            'hidden'         => Field\Hidden::class,
1286
            'id'             => Field\Id::class,
1287
            'image'          => Field\Image::class,
1288
            'ip'             => Field\Ip::class,
1289
            'map'            => Field\Map::class,
1290
            'mobile'         => Field\Mobile::class,
1291
            'month'          => Field\Month::class,
1292
            'multipleSelect' => Field\MultipleSelect::class,
1293
            'number'         => Field\Number::class,
1294
            'password'       => Field\Password::class,
1295
            'radio'          => Field\Radio::class,
1296
            'rate'           => Field\Rate::class,
1297
            'select'         => Field\Select::class,
1298
            'slider'         => Field\Slider::class,
1299
            'switch'         => Field\SwitchField::class,
1300
            'text'           => Field\Text::class,
1301
            'textarea'       => Field\Textarea::class,
1302
            'time'           => Field\Time::class,
1303
            'timeRange'      => Field\TimeRange::class,
1304
            'url'            => Field\Url::class,
1305
            'year'           => Field\Year::class,
1306
            'html'           => Field\Html::class,
1307
            'tags'           => Field\Tags::class,
1308
            'icon'           => Field\Icon::class,
1309
            'multipleFile'   => Field\MultipleFile::class,
1310
            'multipleImage'  => Field\MultipleImage::class,
1311
            'captcha'        => Field\Captcha::class,
1312
            'listbox'        => Field\Listbox::class,
1313
        ];
1314
1315
        foreach ($map as $abstract => $class) {
1316
            static::extend($abstract, $class);
1317
        }
1318
    }
1319
1320
    /**
1321
     * Register custom field.
1322
     *
1323
     * @param string $abstract
1324
     * @param string $class
1325
     *
1326
     * @return void
1327
     */
1328
    public static function extend($abstract, $class)
1329
    {
1330
        static::$availableFields[$abstract] = $class;
1331
    }
1332
1333
    /**
1334
     * Remove registered field.
1335
     *
1336
     * @param array|string $abstract
1337
     */
1338
    public static function forget($abstract)
1339
    {
1340
        array_forget(static::$availableFields, $abstract);
1341
    }
1342
1343
    /**
1344
     * Find field class.
1345
     *
1346
     * @param string $method
1347
     *
1348
     * @return bool|mixed
1349
     */
1350 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...
1351
    {
1352
        $class = array_get(static::$availableFields, $method);
1353
1354
        if (class_exists($class)) {
1355
            return $class;
1356
        }
1357
1358
        return false;
1359
    }
1360
1361
    /**
1362
     * Collect assets required by registered field.
1363
     *
1364
     * @return array
1365
     */
1366
    public static function collectFieldAssets()
1367
    {
1368
        if (!empty(static::$collectedAssets)) {
1369
            return static::$collectedAssets;
1370
        }
1371
1372
        $css = collect();
1373
        $js = collect();
1374
1375
        foreach (static::$availableFields as $field) {
1376
            if (!method_exists($field, 'getAssets')) {
1377
                continue;
1378
            }
1379
1380
            $assets = call_user_func([$field, 'getAssets']);
1381
1382
            $css->push(array_get($assets, 'css'));
1383
            $js->push(array_get($assets, 'js'));
1384
        }
1385
1386
        return static::$collectedAssets = [
1387
            'css' => $css->flatten()->unique()->filter()->toArray(),
1388
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1389
        ];
1390
    }
1391
1392
    /**
1393
     * Getter.
1394
     *
1395
     * @param string $name
1396
     *
1397
     * @return array|mixed
1398
     */
1399
    public function __get($name)
1400
    {
1401
        return $this->input($name);
1402
    }
1403
1404
    /**
1405
     * Setter.
1406
     *
1407
     * @param string $name
1408
     * @param $value
1409
     */
1410
    public function __set($name, $value)
1411
    {
1412
        $this->input($name, $value);
1413
    }
1414
1415
    /**
1416
     * Generate a Field object and add to form builder if Field exists.
1417
     *
1418
     * @param string $method
1419
     * @param array  $arguments
1420
     *
1421
     * @return Field|void
1422
     */
1423
    public function __call($method, $arguments)
1424
    {
1425
        if ($className = static::findFieldClass($method)) {
1426
            $column = array_get($arguments, 0, ''); //[0];
1427
1428
            $element = new $className($column, array_slice($arguments, 1));
1429
1430
            $this->pushField($element);
1431
1432
            return $element;
1433
        }
1434
    }
1435
}
1436