Completed
Push — master ( c8a423...a23ea0 )
by Song
02:21
created

src/Form.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\HasHooks;
10
use Encore\Admin\Form\Row;
11
use Encore\Admin\Form\Tab;
12
use Illuminate\Contracts\Support\Renderable;
13
use Illuminate\Database\Eloquent\Model;
14
use Illuminate\Database\Eloquent\Relations;
15
use Illuminate\Database\Eloquent\SoftDeletes;
16
use Illuminate\Http\Request;
17
use Illuminate\Support\Arr;
18
use Illuminate\Support\Facades\DB;
19
use Illuminate\Support\MessageBag;
20
use Illuminate\Support\Str;
21
use Illuminate\Validation\Validator;
22
use Spatie\EloquentSortable\Sortable;
23
use Symfony\Component\HttpFoundation\Response;
24
25
/**
26
 * Class Form.
27
 *
28
 * @method Field\Text           text($column, $label = '')
29
 * @method Field\Checkbox       checkbox($column, $label = '')
30
 * @method Field\Radio          radio($column, $label = '')
31
 * @method Field\Select         select($column, $label = '')
32
 * @method Field\MultipleSelect multipleSelect($column, $label = '')
33
 * @method Field\Textarea       textarea($column, $label = '')
34
 * @method Field\Hidden         hidden($column, $label = '')
35
 * @method Field\Id             id($column, $label = '')
36
 * @method Field\Ip             ip($column, $label = '')
37
 * @method Field\Url            url($column, $label = '')
38
 * @method Field\Color          color($column, $label = '')
39
 * @method Field\Email          email($column, $label = '')
40
 * @method Field\Mobile         mobile($column, $label = '')
41
 * @method Field\Slider         slider($column, $label = '')
42
 * @method Field\File           file($column, $label = '')
43
 * @method Field\Image          image($column, $label = '')
44
 * @method Field\Date           date($column, $label = '')
45
 * @method Field\Datetime       datetime($column, $label = '')
46
 * @method Field\Time           time($column, $label = '')
47
 * @method Field\Year           year($column, $label = '')
48
 * @method Field\Month          month($column, $label = '')
49
 * @method Field\DateRange      dateRange($start, $end, $label = '')
50
 * @method Field\DateTimeRange  datetimeRange($start, $end, $label = '')
51
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
52
 * @method Field\Number         number($column, $label = '')
53
 * @method Field\Currency       currency($column, $label = '')
54
 * @method Field\HasMany        hasMany($relationName, $label = '', $callback)
55
 * @method Field\SwitchField    switch($column, $label = '')
56
 * @method Field\Display        display($column, $label = '')
57
 * @method Field\Rate           rate($column, $label = '')
58
 * @method Field\Divider        divider($title = '')
59
 * @method Field\Password       password($column, $label = '')
60
 * @method Field\Decimal        decimal($column, $label = '')
61
 * @method Field\Html           html($html, $label = '')
62
 * @method Field\Tags           tags($column, $label = '')
63
 * @method Field\Icon           icon($column, $label = '')
64
 * @method Field\Embeds         embeds($column, $label = '', $callback)
65
 * @method Field\MultipleImage  multipleImage($column, $label = '')
66
 * @method Field\MultipleFile   multipleFile($column, $label = '')
67
 * @method Field\Captcha        captcha($column, $label = '')
68
 * @method Field\Listbox        listbox($column, $label = '')
69
 * @method Field\Table          table($column, $label, $builder)
70
 * @method Field\Timezone       timezone($column, $label = '')
71
 * @method Field\KeyValue       keyValue($column, $label = '')
72
 * @method Field\ListField      list($column, $label = '')
73
 */
74
class Form implements Renderable
75
{
76
    use HasHooks;
77
78
    /**
79
     * Remove flag in `has many` form.
80
     */
81
    const REMOVE_FLAG_NAME = '_remove_';
82
83
    /**
84
     * Eloquent model of the form.
85
     *
86
     * @var Model
87
     */
88
    protected $model;
89
90
    /**
91
     * @var \Illuminate\Validation\Validator
92
     */
93
    protected $validator;
94
95
    /**
96
     * @var Builder
97
     */
98
    protected $builder;
99
100
    /**
101
     * Data for save to current model from input.
102
     *
103
     * @var array
104
     */
105
    protected $updates = [];
106
107
    /**
108
     * Data for save to model's relations from input.
109
     *
110
     * @var array
111
     */
112
    protected $relations = [];
113
114
    /**
115
     * Input data.
116
     *
117
     * @var array
118
     */
119
    protected $inputs = [];
120
121
    /**
122
     * Available fields.
123
     *
124
     * @var array
125
     */
126
    public static $availableFields = [
127
        'button'         => Field\Button::class,
128
        'checkbox'       => Field\Checkbox::class,
129
        'color'          => Field\Color::class,
130
        'currency'       => Field\Currency::class,
131
        'date'           => Field\Date::class,
132
        'dateRange'      => Field\DateRange::class,
133
        'datetime'       => Field\Datetime::class,
134
        'dateTimeRange'  => Field\DatetimeRange::class,
135
        'datetimeRange'  => Field\DatetimeRange::class,
136
        'decimal'        => Field\Decimal::class,
137
        'display'        => Field\Display::class,
138
        'divider'        => Field\Divider::class,
139
        'embeds'         => Field\Embeds::class,
140
        'email'          => Field\Email::class,
141
        'file'           => Field\File::class,
142
        'hasMany'        => Field\HasMany::class,
143
        'hidden'         => Field\Hidden::class,
144
        'id'             => Field\Id::class,
145
        'image'          => Field\Image::class,
146
        'ip'             => Field\Ip::class,
147
        'mobile'         => Field\Mobile::class,
148
        'month'          => Field\Month::class,
149
        'multipleSelect' => Field\MultipleSelect::class,
150
        'number'         => Field\Number::class,
151
        'password'       => Field\Password::class,
152
        'radio'          => Field\Radio::class,
153
        'rate'           => Field\Rate::class,
154
        'select'         => Field\Select::class,
155
        'slider'         => Field\Slider::class,
156
        'switch'         => Field\SwitchField::class,
157
        'text'           => Field\Text::class,
158
        'textarea'       => Field\Textarea::class,
159
        'time'           => Field\Time::class,
160
        'timeRange'      => Field\TimeRange::class,
161
        'url'            => Field\Url::class,
162
        'year'           => Field\Year::class,
163
        'html'           => Field\Html::class,
164
        'tags'           => Field\Tags::class,
165
        'icon'           => Field\Icon::class,
166
        'multipleFile'   => Field\MultipleFile::class,
167
        'multipleImage'  => Field\MultipleImage::class,
168
        'captcha'        => Field\Captcha::class,
169
        'listbox'        => Field\Listbox::class,
170
        'table'          => Field\Table::class,
171
        'timezone'       => Field\Timezone::class,
172
        'keyValue'       => Field\KeyValue::class,
173
        'list'           => Field\ListField::class,
174
    ];
175
176
    /**
177
     * Form field alias.
178
     *
179
     * @var array
180
     */
181
    public static $fieldAlias = [];
182
183
    /**
184
     * Ignored saving fields.
185
     *
186
     * @var array
187
     */
188
    protected $ignored = [];
189
190
    /**
191
     * Collected field assets.
192
     *
193
     * @var array
194
     */
195
    protected static $collectedAssets = [];
196
197
    /**
198
     * @var Form\Tab
199
     */
200
    protected $tab = null;
201
202
    /**
203
     * Field rows in form.
204
     *
205
     * @var array
206
     */
207
    public $rows = [];
208
209
    /**
210
     * @var bool
211
     */
212
    protected $isSoftDeletes = false;
213
214
    /**
215
     * Initialization closure array.
216
     *
217
     * @var []Closure
218
     */
219
    protected static $initCallbacks;
220
221
    /**
222
     * Create a new form instance.
223
     *
224
     * @param $model
225
     * @param \Closure $callback
226
     */
227
    public function __construct($model, Closure $callback = null)
228
    {
229
        $this->model = $model;
230
231
        $this->builder = new Builder($this);
232
233
        if ($callback instanceof Closure) {
234
            $callback($this);
235
        }
236
237
        $this->isSoftDeletes = in_array(SoftDeletes::class, class_uses_deep($this->model));
238
239
        $this->callInitCallbacks();
240
    }
241
242
    /**
243
     * Initialize with user pre-defined default disables, etc.
244
     *
245
     * @param Closure $callback
246
     */
247
    public static function init(Closure $callback = null)
248
    {
249
        static::$initCallbacks[] = $callback;
250
    }
251
252
    /**
253
     * Call the initialization closure array in sequence.
254
     */
255
    protected function callInitCallbacks()
256
    {
257
        if (empty(static::$initCallbacks)) {
258
            return;
259
        }
260
261
        foreach (static::$initCallbacks as $callback) {
262
            call_user_func($callback, $this);
263
        }
264
    }
265
266
    /**
267
     * @param Field $field
268
     *
269
     * @return $this
270
     */
271
    public function pushField(Field $field)
272
    {
273
        $field->setForm($this);
274
275
        $this->builder->fields()->push($field);
276
277
        return $this;
278
    }
279
280
    /**
281
     * @return Model
282
     */
283
    public function model()
284
    {
285
        return $this->model;
286
    }
287
288
    /**
289
     * @return Builder
290
     */
291
    public function builder()
292
    {
293
        return $this->builder;
294
    }
295
296
    /**
297
     * Generate a edit form.
298
     *
299
     * @param $id
300
     *
301
     * @return $this
302
     */
303
    public function edit($id)
304
    {
305
        $this->builder->setMode(Builder::MODE_EDIT);
306
        $this->builder->setResourceId($id);
307
308
        $this->setFieldValue($id);
309
310
        return $this;
311
    }
312
313
    /**
314
     * Use tab to split form.
315
     *
316
     * @param string  $title
317
     * @param Closure $content
318
     *
319
     * @return $this
320
     */
321
    public function tab($title, Closure $content, $active = false)
322
    {
323
        $this->getTab()->append($title, $content, $active);
324
325
        return $this;
326
    }
327
328
    /**
329
     * Get Tab instance.
330
     *
331
     * @return Tab
332
     */
333
    public function getTab()
334
    {
335
        if (is_null($this->tab)) {
336
            $this->tab = new Tab($this);
337
        }
338
339
        return $this->tab;
340
    }
341
342
    /**
343
     * Destroy data entity and remove files.
344
     *
345
     * @param $id
346
     *
347
     * @return mixed
348
     */
349
    public function destroy($id)
350
    {
351
        try {
352
            if (($ret = $this->callDeleting($id)) instanceof Response) {
353
                return $ret;
354
            }
355
356
            collect(explode(',', $id))->filter()->each(function ($id) {
357
                $builder = $this->model()->newQuery();
358
359
                if ($this->isSoftDeletes) {
360
                    $builder = $builder->withTrashed();
361
                }
362
363
                $model = $builder->with($this->getRelations())->findOrFail($id);
364
365
                if ($this->isSoftDeletes && $model->trashed()) {
366
                    $this->deleteFiles($model, true);
367
                    $model->forceDelete();
368
369
                    return;
370
                }
371
372
                $this->deleteFiles($model);
373
                $model->delete();
374
            });
375
376
            if (($ret = $this->callDeleted()) instanceof Response) {
377
                return $ret;
378
            }
379
380
            $response = [
381
                'status'  => true,
382
                'message' => trans('admin.delete_succeeded'),
383
            ];
384
        } catch (\Exception $exception) {
385
            $response = [
386
                'status'  => false,
387
                'message' => $exception->getMessage() ?: trans('admin.delete_failed'),
388
            ];
389
        }
390
391
        return response()->json($response);
392
    }
393
394
    /**
395
     * Remove files in record.
396
     *
397
     * @param Model $model
398
     * @param bool  $forceDelete
399
     */
400
    protected function deleteFiles(Model $model, $forceDelete = false)
401
    {
402
        // If it's a soft delete, the files in the data will not be deleted.
403
        if (!$forceDelete && $this->isSoftDeletes) {
404
            return;
405
        }
406
407
        $data = $model->toArray();
408
409
        $this->builder->fields()->filter(function ($field) {
410
            return $field instanceof Field\File;
411
        })->each(function (Field\File $file) use ($data) {
412
            $file->setOriginal($data);
413
414
            $file->destroy();
415
        });
416
    }
417
418
    /**
419
     * Store a new record.
420
     *
421
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
422
     */
423
    public function store()
424
    {
425
        $data = \request()->all();
426
427
        // Handle validation errors.
428
        if ($validationMessages = $this->validationMessages($data)) {
429
            return $this->responseValidationError($validationMessages);
430
        }
431
432
        if (($response = $this->prepare($data)) instanceof Response) {
433
            return $response;
434
        }
435
436 View Code Duplication
        DB::transaction(function () {
437
            $inserts = $this->prepareInsert($this->updates);
438
439
            foreach ($inserts as $column => $value) {
440
                $this->model->setAttribute($column, $value);
441
            }
442
443
            $this->model->save();
444
445
            $this->updateRelation($this->relations);
446
        });
447
448
        if (($response = $this->callSaved()) instanceof Response) {
449
            return $response;
450
        }
451
452
        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
453
            return $response;
454
        }
455
456
        return $this->redirectAfterStore();
457
    }
458
459
    /**
460
     * @param MessageBag $message
461
     *
462
     * @return $this|\Illuminate\Http\JsonResponse
463
     */
464
    protected function responseValidationError(MessageBag $message)
465
    {
466
        if (\request()->ajax() && !\request()->pjax()) {
467
            return response()->json([
468
                'status'     => false,
469
                'validation' => $message,
470
                'message'    => $message->first(),
471
            ]);
472
        }
473
474
        return back()->withInput()->withErrors($message);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return back()->withInput()->withErrors($message); (Illuminate\Http\RedirectResponse) is incompatible with the return type documented by Encore\Admin\Form::responseValidationError of type Encore\Admin\Form|Illuminate\Http\JsonResponse.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
475
    }
476
477
    /**
478
     * Get ajax response.
479
     *
480
     * @param string $message
481
     *
482
     * @return bool|\Illuminate\Http\JsonResponse
483
     */
484
    protected function ajaxResponse($message)
485
    {
486
        $request = Request::capture();
487
488
        // ajax but not pjax
489
        if ($request->ajax() && !$request->pjax()) {
490
            return response()->json([
491
                'status'  => true,
492
                'message' => $message,
493
            ]);
494
        }
495
496
        return false;
497
    }
498
499
    /**
500
     * Prepare input data for insert or update.
501
     *
502
     * @param array $data
503
     *
504
     * @return mixed
505
     */
506
    protected function prepare($data = [])
507
    {
508
        if (($response = $this->callSubmitted()) instanceof Response) {
509
            return $response;
510
        }
511
512
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);
513
514
        if (($response = $this->callSaving()) instanceof Response) {
515
            return $response;
516
        }
517
518
        $this->relations = $this->getRelationInputs($this->inputs);
519
520
        $this->updates = Arr::except($this->inputs, array_keys($this->relations));
521
    }
522
523
    /**
524
     * Remove ignored fields from input.
525
     *
526
     * @param array $input
527
     *
528
     * @return array
529
     */
530
    protected function removeIgnoredFields($input)
531
    {
532
        Arr::forget($input, $this->ignored);
533
534
        return $input;
535
    }
536
537
    /**
538
     * Get inputs for relations.
539
     *
540
     * @param array $inputs
541
     *
542
     * @return array
543
     */
544
    protected function getRelationInputs($inputs = [])
545
    {
546
        $relations = [];
547
548
        foreach ($inputs as $column => $value) {
549
            if (!method_exists($this->model, $column)) {
550
                continue;
551
            }
552
553
            $relation = call_user_func([$this->model, $column]);
554
555
            if ($relation instanceof Relations\Relation) {
556
                $relations[$column] = $value;
557
            }
558
        }
559
560
        return $relations;
561
    }
562
563
    /**
564
     * Handle update.
565
     *
566
     * @param int  $id
567
     * @param null $data
568
     *
569
     * @return bool|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|mixed|null|Response
570
     */
571
    public function update($id, $data = null)
572
    {
573
        $data = ($data) ?: request()->all();
574
575
        $isEditable = $this->isEditable($data);
576
577
        if (($data = $this->handleColumnUpdates($id, $data)) instanceof Response) {
578
            return $data;
579
        }
580
581
        /* @var Model $this->model */
582
        $builder = $this->model();
583
584
        if ($this->isSoftDeletes) {
585
            $builder = $builder->withTrashed();
586
        }
587
588
        $this->model = $builder->with($this->getRelations())->findOrFail($id);
589
590
        $this->setFieldOriginalValue();
591
592
        // Handle validation errors.
593
        if ($validationMessages = $this->validationMessages($data)) {
594
            if (!$isEditable) {
595
                return back()->withInput()->withErrors($validationMessages);
596
            }
597
598
            return response()->json(['errors' => Arr::dot($validationMessages->getMessages())], 422);
599
        }
600
601
        if (($response = $this->prepare($data)) instanceof Response) {
602
            return $response;
603
        }
604
605 View Code Duplication
        DB::transaction(function () {
606
            $updates = $this->prepareUpdate($this->updates);
607
608
            foreach ($updates as $column => $value) {
609
                /* @var Model $this->model */
610
                $this->model->setAttribute($column, $value);
611
            }
612
613
            $this->model->save();
614
615
            $this->updateRelation($this->relations);
616
        });
617
618
        if (($result = $this->callSaved()) instanceof Response) {
619
            return $result;
620
        }
621
622
        if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) {
623
            return $response;
624
        }
625
626
        return $this->redirectAfterUpdate($id);
627
    }
628
629
    /**
630
     * Get RedirectResponse after store.
631
     *
632
     * @return \Illuminate\Http\RedirectResponse
633
     */
634
    protected function redirectAfterStore()
635
    {
636
        $resourcesPath = $this->resource(0);
637
638
        $key = $this->model->getKey();
639
640
        return $this->redirectAfterSaving($resourcesPath, $key);
641
    }
642
643
    /**
644
     * Get RedirectResponse after update.
645
     *
646
     * @param mixed $key
647
     *
648
     * @return \Illuminate\Http\RedirectResponse
649
     */
650
    protected function redirectAfterUpdate($key)
651
    {
652
        $resourcesPath = $this->resource(-1);
653
654
        return $this->redirectAfterSaving($resourcesPath, $key);
655
    }
656
657
    /**
658
     * Get RedirectResponse after data saving.
659
     *
660
     * @param string $resourcesPath
661
     * @param string $key
662
     *
663
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
664
     */
665
    protected function redirectAfterSaving($resourcesPath, $key)
666
    {
667
        if (request('after-save') == 1) {
668
            // continue editing
669
            $url = rtrim($resourcesPath, '/')."/{$key}/edit";
670
        } elseif (request('after-save') == 2) {
671
            // continue creating
672
            $url = rtrim($resourcesPath, '/').'/create';
673
        } elseif (request('after-save') == 3) {
674
            // view resource
675
            $url = rtrim($resourcesPath, '/')."/{$key}";
676
        } else {
677
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
678
        }
679
680
        admin_toastr(trans('admin.save_succeeded'));
681
682
        return redirect($url);
683
    }
684
685
    /**
686
     * Check if request is from editable.
687
     *
688
     * @param array $input
689
     *
690
     * @return bool
691
     */
692
    protected function isEditable(array $input = [])
693
    {
694
        return array_key_exists('_editable', $input);
695
    }
696
697
    /**
698
     * Handle updates for single column.
699
     *
700
     * @param int   $id
701
     * @param array $data
702
     *
703
     * @return array|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|Response
704
     */
705
    protected function handleColumnUpdates($id, $data)
706
    {
707
        $data = $this->handleEditable($data);
708
709
        $data = $this->handleFileDelete($data);
710
711
        $data = $this->handleFileSort($data);
712
713
        if ($this->handleOrderable($id, $data)) {
714
            return response([
715
                'status'  => true,
716
                'message' => trans('admin.update_succeeded'),
717
            ]);
718
        }
719
720
        return $data;
721
    }
722
723
    /**
724
     * Handle editable update.
725
     *
726
     * @param array $input
727
     *
728
     * @return array
729
     */
730
    protected function handleEditable(array $input = [])
731
    {
732
        if (array_key_exists('_editable', $input)) {
733
            $name = $input['name'];
734
            $value = $input['value'];
735
736
            Arr::forget($input, ['pk', 'value', 'name']);
737
            Arr::set($input, $name, $value);
738
        }
739
740
        return $input;
741
    }
742
743
    /**
744
     * @param array $input
745
     *
746
     * @return array
747
     */
748
    protected function handleFileDelete(array $input = [])
749
    {
750
        if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) {
751
            $input[Field::FILE_DELETE_FLAG] = $input['key'];
752
            unset($input['key']);
753
        }
754
755
        request()->replace($input);
756
757
        return $input;
758
    }
759
760
    /**
761
     * @param array $input
762
     *
763
     * @return array
764
     */
765
    protected function handleFileSort(array $input = [])
766
    {
767
        if (!array_key_exists(Field::FILE_SORT_FLAG, $input)) {
768
            return $input;
769
        }
770
771
        $sorts = array_filter($input[Field::FILE_SORT_FLAG]);
772
773
        if (empty($sorts)) {
774
            return $input;
775
        }
776
777
        foreach ($sorts as $column => $order) {
778
            $input[$column] = $order;
779
        }
780
781
        request()->replace($input);
782
783
        return $input;
784
    }
785
786
    /**
787
     * Handle orderable update.
788
     *
789
     * @param int   $id
790
     * @param array $input
791
     *
792
     * @return bool
793
     */
794
    protected function handleOrderable($id, array $input = [])
795
    {
796
        if (array_key_exists('_orderable', $input)) {
797
            $model = $this->model->find($id);
798
799
            if ($model instanceof Sortable) {
800
                $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown();
801
802
                return true;
803
            }
804
        }
805
806
        return false;
807
    }
808
809
    /**
810
     * Update relation data.
811
     *
812
     * @param array $relationsData
813
     *
814
     * @return void
815
     */
816
    protected function updateRelation($relationsData)
817
    {
818
        foreach ($relationsData as $name => $values) {
819
            if (!method_exists($this->model, $name)) {
820
                continue;
821
            }
822
823
            $relation = $this->model->$name();
824
825
            $oneToOneRelation = $relation instanceof Relations\HasOne
826
                || $relation instanceof Relations\MorphOne
827
                || $relation instanceof Relations\BelongsTo;
828
829
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
830
831
            if (empty($prepared)) {
832
                continue;
833
            }
834
835
            switch (true) {
836
                case $relation instanceof Relations\BelongsToMany:
837
                case $relation instanceof Relations\MorphToMany:
838
                    if (isset($prepared[$name])) {
839
                        $relation->sync($prepared[$name]);
840
                    }
841
                    break;
842
                case $relation instanceof Relations\HasOne:
843
844
                    $related = $this->model->$name;
845
846
                    // if related is empty
847
                    if (is_null($related)) {
848
                        $related = $relation->getRelated();
849
                        $qualifiedParentKeyName = $relation->getQualifiedParentKeyName();
850
                        $localKey = Arr::last(explode('.', $qualifiedParentKeyName));
851
                        $related->{$relation->getForeignKeyName()} = $this->model->{$localKey};
852
                    }
853
854
                    foreach ($prepared[$name] as $column => $value) {
855
                        $related->setAttribute($column, $value);
856
                    }
857
858
                    $related->save();
859
                    break;
860
                case $relation instanceof Relations\BelongsTo:
861
                case $relation instanceof Relations\MorphTo:
862
863
                    $parent = $this->model->$name;
864
865
                    // if related is empty
866
                    if (is_null($parent)) {
867
                        $parent = $relation->getRelated();
868
                    }
869
870
                    foreach ($prepared[$name] as $column => $value) {
871
                        $parent->setAttribute($column, $value);
872
                    }
873
874
                    $parent->save();
875
876
                    // When in creating, associate two models
877
                    $foreignKeyMethod = (app()->version() < '5.8.0') ? 'getForeignKey' : 'getForeignKeyName';
878
                    if (!$this->model->{$relation->{$foreignKeyMethod}()}) {
879
                        $this->model->{$relation->{$foreignKeyMethod}()} = $parent->getKey();
880
881
                        $this->model->save();
882
                    }
883
884
                    break;
885
                case $relation instanceof Relations\MorphOne:
886
                    $related = $this->model->$name;
887
                    if (is_null($related)) {
888
                        $related = $relation->make();
889
                    }
890
                    foreach ($prepared[$name] as $column => $value) {
891
                        $related->setAttribute($column, $value);
892
                    }
893
                    $related->save();
894
                    break;
895
                case $relation instanceof Relations\HasMany:
896
                case $relation instanceof Relations\MorphMany:
897
898
                    foreach ($prepared[$name] as $related) {
899
                        /** @var Relations\Relation $relation */
900
                        $relation = $this->model()->$name();
901
902
                        $keyName = $relation->getRelated()->getKeyName();
903
904
                        $instance = $relation->findOrNew(Arr::get($related, $keyName));
905
906
                        if ($related[static::REMOVE_FLAG_NAME] == 1) {
907
                            $instance->delete();
908
909
                            continue;
910
                        }
911
912
                        Arr::forget($related, static::REMOVE_FLAG_NAME);
913
914
                        $instance->fill($related);
915
916
                        $instance->save();
917
                    }
918
919
                    break;
920
            }
921
        }
922
    }
923
924
    /**
925
     * Prepare input data for update.
926
     *
927
     * @param array $updates
928
     * @param bool  $oneToOneRelation If column is one-to-one relation.
929
     *
930
     * @return array
931
     */
932
    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
933
    {
934
        $prepared = [];
935
936
        /** @var Field $field */
937
        foreach ($this->builder->fields() as $field) {
938
            $columns = $field->column();
939
940
            // If column not in input array data, then continue.
941
            if (!Arr::has($updates, $columns)) {
942
                continue;
943
            }
944
945
            if ($this->isInvalidColumn($columns, $oneToOneRelation || $field->isJsonType)) {
946
                continue;
947
            }
948
949
            $value = $this->getDataByColumn($updates, $columns);
950
951
            $value = $field->prepare($value);
952
953 View Code Duplication
            if (is_array($columns)) {
954
                foreach ($columns as $name => $column) {
955
                    Arr::set($prepared, $column, $value[$name]);
956
                }
957
            } elseif (is_string($columns)) {
958
                Arr::set($prepared, $columns, $value);
959
            }
960
        }
961
962
        return $prepared;
963
    }
964
965
    /**
966
     * @param string|array $columns
967
     * @param bool         $containsDot
968
     *
969
     * @return bool
970
     */
971
    protected function isInvalidColumn($columns, $containsDot = false)
972
    {
973
        foreach ((array) $columns as $column) {
974
            if ((!$containsDot && Str::contains($column, '.')) ||
975
                ($containsDot && !Str::contains($column, '.'))) {
976
                return true;
977
            }
978
        }
979
980
        return false;
981
    }
982
983
    /**
984
     * Prepare input data for insert.
985
     *
986
     * @param $inserts
987
     *
988
     * @return array
989
     */
990
    protected function prepareInsert($inserts)
991
    {
992
        if ($this->isHasOneRelation($inserts)) {
993
            $inserts = Arr::dot($inserts);
994
        }
995
996
        foreach ($inserts as $column => $value) {
997
            if (is_null($field = $this->getFieldByColumn($column))) {
998
                unset($inserts[$column]);
999
                continue;
1000
            }
1001
1002
            $inserts[$column] = $field->prepare($value);
1003
        }
1004
1005
        $prepared = [];
1006
1007
        foreach ($inserts as $key => $value) {
1008
            Arr::set($prepared, $key, $value);
1009
        }
1010
1011
        return $prepared;
1012
    }
1013
1014
    /**
1015
     * Is input data is has-one relation.
1016
     *
1017
     * @param array $inserts
1018
     *
1019
     * @return bool
1020
     */
1021
    protected function isHasOneRelation($inserts)
1022
    {
1023
        $first = current($inserts);
1024
1025
        if (!is_array($first)) {
1026
            return false;
1027
        }
1028
1029
        if (is_array(current($first))) {
1030
            return false;
1031
        }
1032
1033
        return Arr::isAssoc($first);
1034
    }
1035
1036
    /**
1037
     * Ignore fields to save.
1038
     *
1039
     * @param string|array $fields
1040
     *
1041
     * @return $this
1042
     */
1043
    public function ignore($fields)
1044
    {
1045
        $this->ignored = array_merge($this->ignored, (array) $fields);
1046
1047
        return $this;
1048
    }
1049
1050
    /**
1051
     * @param array        $data
1052
     * @param string|array $columns
1053
     *
1054
     * @return array|mixed
1055
     */
1056 View Code Duplication
    protected function getDataByColumn($data, $columns)
1057
    {
1058
        if (is_string($columns)) {
1059
            return Arr::get($data, $columns);
1060
        }
1061
1062
        if (is_array($columns)) {
1063
            $value = [];
1064
            foreach ($columns as $name => $column) {
1065
                if (!Arr::has($data, $column)) {
1066
                    continue;
1067
                }
1068
                $value[$name] = Arr::get($data, $column);
1069
            }
1070
1071
            return $value;
1072
        }
1073
    }
1074
1075
    /**
1076
     * Find field object by column.
1077
     *
1078
     * @param $column
1079
     *
1080
     * @return mixed
1081
     */
1082
    protected function getFieldByColumn($column)
1083
    {
1084
        return $this->builder->fields()->first(
1085
            function (Field $field) use ($column) {
1086
                if (is_array($field->column())) {
1087
                    return in_array($column, $field->column());
1088
                }
1089
1090
                return $field->column() == $column;
1091
            }
1092
        );
1093
    }
1094
1095
    /**
1096
     * Set original data for each field.
1097
     *
1098
     * @return void
1099
     */
1100
    protected function setFieldOriginalValue()
1101
    {
1102
//        static::doNotSnakeAttributes($this->model);
1103
1104
        $values = $this->model->toArray();
1105
1106
        $this->builder->fields()->each(function (Field $field) use ($values) {
1107
            $field->setOriginal($values);
1108
        });
1109
    }
1110
1111
    /**
1112
     * Set all fields value in form.
1113
     *
1114
     * @param $id
1115
     *
1116
     * @return void
1117
     */
1118
    protected function setFieldValue($id)
1119
    {
1120
        $relations = $this->getRelations();
1121
1122
        $builder = $this->model();
1123
1124
        if ($this->isSoftDeletes) {
1125
            $builder = $builder->withTrashed();
1126
        }
1127
1128
        $this->model = $builder->with($relations)->findOrFail($id);
1129
1130
        $this->callEditing();
1131
1132
//        static::doNotSnakeAttributes($this->model);
1133
1134
        $data = $this->model->toArray();
1135
1136
        $this->builder->fields()->each(function (Field $field) use ($data) {
1137
            if (!in_array($field->column(), $this->ignored)) {
1138
                $field->fill($data);
1139
            }
1140
        });
1141
    }
1142
1143
    /**
1144
     * Add a fieldset to form.
1145
     *
1146
     * @param string  $title
1147
     * @param Closure $setCallback
1148
     *
1149
     * @return Field\Fieldset
1150
     */
1151 View Code Duplication
    public function fieldset(string $title, Closure $setCallback)
1152
    {
1153
        $fieldset = new Field\Fieldset();
1154
1155
        $this->html($fieldset->start($title))->plain();
1156
1157
        $setCallback($this);
1158
1159
        $this->html($fieldset->end())->plain();
1160
1161
        return $fieldset;
1162
    }
1163
1164
    /**
1165
     * Don't snake case attributes.
1166
     *
1167
     * @param Model $model
1168
     *
1169
     * @return void
1170
     */
1171
    protected static function doNotSnakeAttributes(Model $model)
1172
    {
1173
        $class = get_class($model);
1174
1175
        $class::$snakeAttributes = false;
1176
    }
1177
1178
    /**
1179
     * Get validation messages.
1180
     *
1181
     * @param array $input
1182
     *
1183
     * @return MessageBag|bool
1184
     */
1185
    public function validationMessages($input)
1186
    {
1187
        $failedValidators = [];
1188
1189
        /** @var Field $field */
1190 View Code Duplication
        foreach ($this->builder->fields() as $field) {
1191
            if (!$validator = $field->getValidator($input)) {
1192
                continue;
1193
            }
1194
1195
            if (($validator instanceof Validator) && !$validator->passes()) {
1196
                $failedValidators[] = $validator;
1197
            }
1198
        }
1199
1200
        $message = $this->mergeValidationMessages($failedValidators);
1201
1202
        return $message->any() ? $message : false;
1203
    }
1204
1205
    /**
1206
     * Merge validation messages from input validators.
1207
     *
1208
     * @param \Illuminate\Validation\Validator[] $validators
1209
     *
1210
     * @return MessageBag
1211
     */
1212
    protected function mergeValidationMessages($validators)
1213
    {
1214
        $messageBag = new MessageBag();
1215
1216
        foreach ($validators as $validator) {
1217
            $messageBag = $messageBag->merge($validator->messages());
1218
        }
1219
1220
        return $messageBag;
1221
    }
1222
1223
    /**
1224
     * Get all relations of model from callable.
1225
     *
1226
     * @return array
1227
     */
1228
    public function getRelations()
1229
    {
1230
        $relations = $columns = [];
1231
1232
        /** @var Field $field */
1233
        foreach ($this->builder->fields() as $field) {
1234
            $columns[] = $field->column();
1235
        }
1236
1237
        foreach (Arr::flatten($columns) as $column) {
1238
            if (Str::contains($column, '.')) {
1239
                list($relation) = explode('.', $column);
1240
1241
                if (method_exists($this->model, $relation) &&
1242
                    $this->model->$relation() instanceof Relations\Relation
1243
                ) {
1244
                    $relations[] = $relation;
1245
                }
1246
            } elseif (method_exists($this->model, $column) &&
1247
                !method_exists(Model::class, $column)
1248
            ) {
1249
                $relations[] = $column;
1250
            }
1251
        }
1252
1253
        return array_unique($relations);
1254
    }
1255
1256
    /**
1257
     * Set action for form.
1258
     *
1259
     * @param string $action
1260
     *
1261
     * @return $this
1262
     */
1263
    public function setAction($action)
1264
    {
1265
        $this->builder()->setAction($action);
1266
1267
        return $this;
1268
    }
1269
1270
    /**
1271
     * Set field and label width in current form.
1272
     *
1273
     * @param int $fieldWidth
1274
     * @param int $labelWidth
1275
     *
1276
     * @return $this
1277
     */
1278
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
1279
    {
1280
        $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) {
1281
            /* @var Field $field  */
1282
            $field->setWidth($fieldWidth, $labelWidth);
1283
        });
1284
1285
        $this->builder()->setWidth($fieldWidth, $labelWidth);
1286
1287
        return $this;
1288
    }
1289
1290
    /**
1291
     * Set view for form.
1292
     *
1293
     * @param string $view
1294
     *
1295
     * @return $this
1296
     */
1297
    public function setView($view)
1298
    {
1299
        $this->builder()->setView($view);
1300
1301
        return $this;
1302
    }
1303
1304
    /**
1305
     * Set title for form.
1306
     *
1307
     * @param string $title
1308
     *
1309
     * @return $this
1310
     */
1311
    public function setTitle($title = '')
1312
    {
1313
        $this->builder()->setTitle($title);
1314
1315
        return $this;
1316
    }
1317
1318
    /**
1319
     * Add a row in form.
1320
     *
1321
     * @param Closure $callback
1322
     *
1323
     * @return $this
1324
     */
1325
    public function row(Closure $callback)
1326
    {
1327
        $this->rows[] = new Row($callback, $this);
1328
1329
        return $this;
1330
    }
1331
1332
    /**
1333
     * Tools setting for form.
1334
     *
1335
     * @param Closure $callback
1336
     */
1337
    public function tools(Closure $callback)
1338
    {
1339
        $callback->call($this, $this->builder->getTools());
1340
    }
1341
1342
    /**
1343
     * @param Closure|null $callback
1344
     *
1345
     * @return Form\Tools
1346
     */
1347
    public function header(Closure $callback = null)
1348
    {
1349
        if (func_num_args() == 0) {
1350
            return $this->builder->getTools();
1351
        }
1352
1353
        $callback->call($this, $this->builder->getTools());
1354
    }
1355
1356
    /**
1357
     * Indicates if current form page is creating.
1358
     *
1359
     * @return bool
1360
     */
1361
    public function isCreating()
1362
    {
1363
        return Str::endsWith(\request()->route()->getName(), '.create');
1364
    }
1365
1366
    /**
1367
     * Indicates if current form page is editing.
1368
     *
1369
     * @return bool
1370
     */
1371
    public function isEditing()
1372
    {
1373
        return Str::endsWith(\request()->route()->getName(), '.edit');
1374
    }
1375
1376
    /**
1377
     * Disable form submit.
1378
     *
1379
     * @param bool $disable
1380
     *
1381
     * @return $this
1382
     *
1383
     * @deprecated
1384
     */
1385
    public function disableSubmit(bool $disable = true)
1386
    {
1387
        $this->builder()->getFooter()->disableSubmit($disable);
1388
1389
        return $this;
1390
    }
1391
1392
    /**
1393
     * Disable form reset.
1394
     *
1395
     * @param bool $disable
1396
     *
1397
     * @return $this
1398
     *
1399
     * @deprecated
1400
     */
1401
    public function disableReset(bool $disable = true)
1402
    {
1403
        $this->builder()->getFooter()->disableReset($disable);
1404
1405
        return $this;
1406
    }
1407
1408
    /**
1409
     * Disable View Checkbox on footer.
1410
     *
1411
     * @param bool $disable
1412
     *
1413
     * @return $this
1414
     */
1415
    public function disableViewCheck(bool $disable = true)
1416
    {
1417
        $this->builder()->getFooter()->disableViewCheck($disable);
1418
1419
        return $this;
1420
    }
1421
1422
    /**
1423
     * Disable Editing Checkbox on footer.
1424
     *
1425
     * @param bool $disable
1426
     *
1427
     * @return $this
1428
     */
1429
    public function disableEditingCheck(bool $disable = true)
1430
    {
1431
        $this->builder()->getFooter()->disableEditingCheck($disable);
1432
1433
        return $this;
1434
    }
1435
1436
    /**
1437
     * Disable Creating Checkbox on footer.
1438
     *
1439
     * @param bool $disable
1440
     *
1441
     * @return $this
1442
     */
1443
    public function disableCreatingCheck(bool $disable = true)
1444
    {
1445
        $this->builder()->getFooter()->disableCreatingCheck($disable);
1446
1447
        return $this;
1448
    }
1449
1450
    /**
1451
     * Footer setting for form.
1452
     *
1453
     * @param Closure $callback
1454
     */
1455
    public function footer(Closure $callback = null)
1456
    {
1457
        if (func_num_args() == 0) {
1458
            return $this->builder()->getFooter();
1459
        }
1460
1461
        call_user_func($callback, $this->builder()->getFooter());
1462
    }
1463
1464
    /**
1465
     * Get current resource route url.
1466
     *
1467
     * @param int $slice
1468
     *
1469
     * @return string
1470
     */
1471
    public function resource($slice = -2)
1472
    {
1473
        $segments = explode('/', trim(\request()->getUri(), '/'));
1474
1475
        if ($slice != 0) {
1476
            $segments = array_slice($segments, 0, $slice);
1477
        }
1478
1479
        return implode('/', $segments);
1480
    }
1481
1482
    /**
1483
     * Render the form contents.
1484
     *
1485
     * @return string
1486
     */
1487
    public function render()
1488
    {
1489
        try {
1490
            return $this->builder->render();
1491
        } catch (\Exception $e) {
1492
            return Handler::renderException($e);
1493
        }
1494
    }
1495
1496
    /**
1497
     * Get or set input data.
1498
     *
1499
     * @param string $key
1500
     * @param null   $value
1501
     *
1502
     * @return array|mixed
1503
     */
1504
    public function input($key, $value = null)
1505
    {
1506
        if (is_null($value)) {
1507
            return Arr::get($this->inputs, $key);
1508
        }
1509
1510
        return Arr::set($this->inputs, $key, $value);
1511
    }
1512
1513
    /**
1514
     * Register custom field.
1515
     *
1516
     * @param string $abstract
1517
     * @param string $class
1518
     *
1519
     * @return void
1520
     */
1521
    public static function extend($abstract, $class)
1522
    {
1523
        static::$availableFields[$abstract] = $class;
1524
    }
1525
1526
    /**
1527
     * Set form field alias.
1528
     *
1529
     * @param string $field
1530
     * @param string $alias
1531
     *
1532
     * @return void
1533
     */
1534
    public static function alias($field, $alias)
1535
    {
1536
        static::$fieldAlias[$alias] = $field;
1537
    }
1538
1539
    /**
1540
     * Remove registered field.
1541
     *
1542
     * @param array|string $abstract
1543
     */
1544
    public static function forget($abstract)
1545
    {
1546
        Arr::forget(static::$availableFields, $abstract);
1547
    }
1548
1549
    /**
1550
     * Find field class.
1551
     *
1552
     * @param string $method
1553
     *
1554
     * @return bool|mixed
1555
     */
1556
    public static function findFieldClass($method)
1557
    {
1558
        // If alias exists.
1559
        if (isset(static::$fieldAlias[$method])) {
1560
            $method = static::$fieldAlias[$method];
1561
        }
1562
1563
        $class = Arr::get(static::$availableFields, $method);
1564
1565
        if (class_exists($class)) {
1566
            return $class;
1567
        }
1568
1569
        return false;
1570
    }
1571
1572
    /**
1573
     * Collect assets required by registered field.
1574
     *
1575
     * @return array
1576
     */
1577
    public static function collectFieldAssets()
1578
    {
1579
        if (!empty(static::$collectedAssets)) {
1580
            return static::$collectedAssets;
1581
        }
1582
1583
        $css = collect();
1584
        $js = collect();
1585
1586
        foreach (static::$availableFields as $field) {
1587
            if (!method_exists($field, 'getAssets')) {
1588
                continue;
1589
            }
1590
1591
            $assets = call_user_func([$field, 'getAssets']);
1592
1593
            $css->push(Arr::get($assets, 'css'));
1594
            $js->push(Arr::get($assets, 'js'));
1595
        }
1596
1597
        return static::$collectedAssets = [
1598
            'css' => $css->flatten()->unique()->filter()->toArray(),
1599
            'js'  => $js->flatten()->unique()->filter()->toArray(),
1600
        ];
1601
    }
1602
1603
    /**
1604
     * Getter.
1605
     *
1606
     * @param string $name
1607
     *
1608
     * @return array|mixed
1609
     */
1610
    public function __get($name)
1611
    {
1612
        return $this->input($name);
1613
    }
1614
1615
    /**
1616
     * Setter.
1617
     *
1618
     * @param string $name
1619
     * @param mixed  $value
1620
     *
1621
     * @return array
1622
     */
1623
    public function __set($name, $value)
1624
    {
1625
        return Arr::set($this->inputs, $name, $value);
1626
    }
1627
1628
    /**
1629
     * Generate a Field object and add to form builder if Field exists.
1630
     *
1631
     * @param string $method
1632
     * @param array  $arguments
1633
     *
1634
     * @return Field
1635
     */
1636
    public function __call($method, $arguments)
1637
    {
1638
        if ($className = static::findFieldClass($method)) {
1639
            $column = Arr::get($arguments, 0, ''); //[0];
1640
1641
            $element = new $className($column, array_slice($arguments, 1));
1642
1643
            $this->pushField($element);
1644
1645
            return $element;
1646
        }
1647
1648
        admin_error('Error', "Field type [$method] does not exist.");
1649
1650
        return new Field\Nullable();
1651
    }
1652
}
1653