Completed
Push — master ( a62cb8...9489a3 )
by Song
03:42
created

Form::updateRelation()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 17
nc 7
nop 1
dl 0
loc 29
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin;
4
5
use Closure;
6
use Encore\Admin\Exception\Handle;
7
use Encore\Admin\Form\Builder;
8
use Encore\Admin\Form\Field;
9
use Encore\Admin\Form\Field\File;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\Relation;
12
use Illuminate\Support\Arr;
13
use Illuminate\Support\Facades\DB;
14
use Illuminate\Support\Facades\Input;
15
use Illuminate\Support\Facades\Validator;
16
use Symfony\Component\HttpFoundation\File\UploadedFile;
17
18
/**
19
 * Class Form.
20
 *
21
 * @method Field\Text           text($column, $label = '')
22
 * @method Field\Checkbox       checkbox($column, $label = '')
23
 * @method Field\Radio          radio($column, $label = '')
24
 * @method Field\Select         select($column, $label = '')
25
 * @method Field\MultipleSelect multipleSelect($column, $label = '')
26
 * @method Field\Textarea       textarea($column, $label = '')
27
 * @method Field\Hidden         hidden($column, $label = '')
28
 * @method Field\Id             id($column, $label = '')
29
 * @method Field\Ip             ip($column, $label = '')
30
 * @method Field\Url            url($column, $label = '')
31
 * @method Field\Color          color($column, $label = '')
32
 * @method Field\Email          email($column, $label = '')
33
 * @method Field\Mobile         mobile($column, $label = '')
34
 * @method Field\Slider         slider($column, $label = '')
35
 * @method Field\Map            map($latitude, $longitude, $label = '')
36
 * @method Field\Editor         editor($column, $label = '')
37
 * @method Field\File           file($column, $label = '')
38
 * @method Field\Image          image($column, $label = '')
39
 * @method Field\Date           date($column, $label = '')
40
 * @method Field\Datetime       datetime($column, $label = '')
41
 * @method Field\Time           time($column, $label = '')
42
 * @method Field\DateRange      dateRange($start, $end, $label = '')
43
 * @method Field\DateTimeRange  dateTimeRange($start, $end, $label = '')
44
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
45
 * @method Field\Number         number($column, $label = '')
46
 * @method Field\Currency       currency($column, $label = '')
47
 * @method Field\Json           json($column, $label = '')
48
 * @method Field\HasMany        hasMany($relationName, $callback)
49
 * @method Field\SwitchField    switch($column, $label = '')
50
 * @method Field\Display        display($column, $label = '')
51
 * @method Field\Rate           rate($column, $label = '')
52
 * @method Field\Divide         divide()
53
 * @method Field\Password       password($column, $label = '')
54
 */
55
class Form
56
{
57
    /**
58
     * Eloquent model of the form.
59
     *
60
     * @var
61
     */
62
    protected $model;
63
64
    /**
65
     * @var \Illuminate\Validation\Validator
66
     */
67
    protected $validator;
68
69
    /**
70
     * @var Builder
71
     */
72
    protected $builder;
73
74
    /**
75
     * Saving callback.
76
     *
77
     * @var Closure
78
     */
79
    protected $saving;
80
81
    /**
82
     * Saved callback.
83
     *
84
     * @var Closure
85
     */
86
    protected $saved;
87
88
    /**
89
     * Data for save to current model from input.
90
     *
91
     * @var array
92
     */
93
    protected $updates = [];
94
95
    /**
96
     * Data for save to model's relations from input.
97
     *
98
     * @var array
99
     */
100
    protected $relations = [];
101
102
    /**
103
     * Input data.
104
     *
105
     * @var array
106
     */
107
    protected $inputs = [];
108
109
    /**
110
     * @var callable
111
     */
112
    protected $callable;
113
114
    /**
115
     * @param \$model
116
     * @param \Closure $callback
117
     */
118
    public function __construct($model, Closure $callback)
119
    {
120
        $this->model = $model;
121
122
        $this->builder = new Builder($this);
123
124
        $this->callable = $callback;
125
126
        $callback($this);
127
    }
128
129
    /**
130
     * Set up the form.
131
     */
132
    protected function setUp()
133
    {
134
        call_user_func($this->callable, $this);
135
    }
136
137
    /**
138
     * @param Field $field
139
     *
140
     * @return $this
141
     */
142
    public function pushField(Field $field)
143
    {
144
        $field->setForm($this);
145
146
        $this->builder->fields()->push($field);
147
148
        return $this;
149
    }
150
151
    /**
152
     * @return Model
153
     */
154
    public function model()
155
    {
156
        return $this->model;
157
    }
158
159
    /**
160
     * @return Builder
161
     */
162
    public function builder()
163
    {
164
        return $this->builder;
165
    }
166
167
    /**
168
     * Generate a edit form.
169
     *
170
     * @param $id
171
     *
172
     * @return $this
173
     */
174
    public function edit($id)
175
    {
176
        $this->builder->setMode(Builder::MODE_EDIT);
177
        $this->builder->setResourceId($id);
178
179
        $this->setFieldValue($id);
180
181
        return $this;
182
    }
183
184
    /**
185
     * @param $id
186
     *
187
     * @return $this
188
     */
189
    public function view($id)
190
    {
191
        $this->builder->setMode(Builder::MODE_VIEW);
192
        $this->builder->setResourceId($id);
193
194
        $this->setFieldValue($id);
195
196
        return $this;
197
    }
198
199
    /**
200
     * Destroy data entity and remove files.
201
     *
202
     * @param $id
203
     *
204
     * @return mixed
205
     */
206
    public function destroy($id)
207
    {
208
        $ids = explode(',', $id);
209
210
        foreach ($ids as $id) {
211
            $this->deleteFilesAndImages($id);
212
            $this->model->find($id)->delete();
213
        }
214
215
        return true;
216
    }
217
218
    /**
219
     * Remove files or images in record.
220
     *
221
     * @param $id
222
     */
223
    protected function deleteFilesAndImages($id)
224
    {
225
        $data = $this->model->with($this->getRelations())
226
            ->findOrFail($id)->toArray();
227
228
        $this->builder->fields()->filter(function ($field) {
229
            return $field instanceof Field\File;
230
        })->each(function (File $file) use ($data) {
231
            $file->setOriginal($data);
232
233
            $file->destroy();
234
        });
235
    }
236
237
    /**
238
     * Store a new record.
239
     *
240
     * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
241
     */
242
    public function store()
243
    {
244
        $data = Input::all();
245
246
        if (!$this->validate($data)) {
247
            return back()->withInput()->withErrors($this->validator->messages());
248
        }
249
250
        $this->prepare($data, $this->saving);
251
252 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...
253
            $inserts = $this->prepareInsert($this->updates);
254
255
            foreach ($inserts as $column => $value) {
256
                $this->model->setAttribute($column, $value);
257
            }
258
259
            $this->model->save();
260
261
            $this->saveRelation($this->relations);
262
        });
263
264
        $this->complete($this->saved);
265
266
        return redirect($this->resource());
267
    }
268
269
    /**
270
     * Prepare input data for insert or update.
271
     *
272
     * @param array    $data
273
     * @param callable $callback
274
     */
275
    protected function prepare($data = [], Closure $callback = null)
276
    {
277
        $this->inputs = $data;
278
279
        if ($callback instanceof Closure) {
280
            $callback($this);
281
        }
282
283
        $this->updates = array_filter($this->inputs, function ($val) {
284
            return is_string($val) or ($val instanceof UploadedFile);
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
285
        });
286
287
        $this->relations = array_filter($this->inputs, 'is_array');
288
    }
289
290
    /**
291
     * Callback after saving a Model.
292
     *
293
     * @param Closure|null $callback
294
     *
295
     * @return void
296
     */
297
    protected function complete(Closure $callback = null)
298
    {
299
        if ($callback instanceof Closure) {
300
            $callback($this);
301
        }
302
    }
303
304
    /**
305
     * Save relations data.
306
     *
307
     * @param array $relations
308
     *
309
     * @return void
310
     */
311
    protected function saveRelation($relations)
312
    {
313
        foreach ($relations as $name => $values) {
314
            if (!method_exists($this->model, $name)) {
315
                continue;
316
            }
317
318
            $values = $this->prepareInsert([$name => $values]);
319
320
            $relation = $this->model->$name();
321
322
            switch (get_class($relation)) {
323
                case \Illuminate\Database\Eloquent\Relations\BelongsToMany::class:
324
                    $relation->attach($values[$name]);
325
                    break;
326
                case \Illuminate\Database\Eloquent\Relations\HasOne::class:
327
                    $related = $relation->getRelated();
328
                    foreach ($values[$name] as $column => $value) {
329
                        $related->setAttribute($column, $value);
330
                    }
331
332
                    $relation->save($related);
333
                    break;
334
            }
335
        }
336
    }
337
338
    /**
339
     * @param $id
340
     *
341
     * @return $this|\Illuminate\Http\RedirectResponse
342
     */
343
    public function update($id)
344
    {
345
        $data = Input::all();
346
347
        if (!$this->validate($data)) {
348
            return back()->withInput()->withErrors($this->validator->messages());
349
        }
350
351
        $this->model = $this->model->with($this->getRelations())->findOrFail($id);
352
353
        $this->setFieldOriginalValue();
354
355
        $this->prepare($data, $this->saving);
356
357 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...
358
            $updates = $this->prepareUpdate($this->updates);
359
360
            foreach ($updates as $column => $value) {
361
                $this->model->setAttribute($column, $value);
362
            }
363
364
            $this->model->save();
365
366
            $this->updateRelation($this->relations);
367
        });
368
369
        $this->complete($this->saved);
370
371
        return redirect($this->resource());
372
    }
373
374
    /**
375
     * Update relation data.
376
     *
377
     * @param array $relations
378
     *
379
     * @return void
380
     */
381
    protected function updateRelation($relations)
382
    {
383
        foreach ($relations as $name => $values) {
384
            if (!method_exists($this->model, $name)) {
385
                continue;
386
            }
387
388
            $prepared = $this->prepareUpdate([$name => $values]);
389
390
            if (empty($prepared)) {
391
                continue;
392
            }
393
394
            $relation = $this->model->$name();
395
396
            switch (get_class($relation)) {
397
                case \Illuminate\Database\Eloquent\Relations\BelongsToMany::class:
398
                    $relation->sync($prepared[$name]);
399
                    break;
400
                case \Illuminate\Database\Eloquent\Relations\HasOne::class:
401
                    foreach ($prepared[$name] as $column => $value) {
402
                        $this->model->$name->setAttribute($column, $value);
403
                    }
404
405
                    $this->model->$name->save();
406
                    break;
407
            }
408
        }
409
    }
410
411
    /**
412
     * Prepare input data for update.
413
     *
414
     * @param $updates
415
     *
416
     * @return array
417
     */
418
    protected function prepareUpdate($updates)
419
    {
420
        $prepared = [];
421
422
        foreach ($this->builder->fields() as $field) {
0 ignored issues
show
Bug introduced by
The expression $this->builder->fields() of type object<Illuminate\Support\Collection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
423
            $columns = $field->column();
424
425
            $value = static::getDataByColumn($updates, $columns);
426
427
            if ($value !== '' && empty($value) && !$field instanceof File) {
428
                continue;
429
            }
430
431
            if (method_exists($field, 'prepare')) {
432
                $value = $field->prepare($value);
433
            }
434
435
            if ($value != $field->original()) {
436
                if (is_array($columns)) {
437
                    foreach ($columns as $name => $column) {
438
                        array_set($prepared, $column, $value[$name]);
439
                    }
440
                } elseif (is_string($columns)) {
441
                    array_set($prepared, $columns, $value);
442
                }
443
            }
444
        }
445
446
        return $prepared;
447
    }
448
449
    /**
450
     * Prepare input data for insert.
451
     *
452
     * @param $inserts
453
     *
454
     * @return array
455
     */
456
    protected function prepareInsert($inserts)
457
    {
458
        $first = current($inserts);
459
460
        if (is_array($first) && Arr::isAssoc($first)) {
461
            $inserts = array_dot($inserts);
462
        }
463
464
        foreach ($inserts as $column => $value) {
465
            if (is_null($field = $this->getFieldByColumn($column))) {
466
                unset($inserts[$column]);
467
                continue;
468
            }
469
470
            if (method_exists($field, 'prepare')) {
471
                $inserts[$column] = $field->prepare($value);
472
            }
473
        }
474
475
        $prepared = [];
476
477
        foreach ($inserts as $key => $value) {
478
            array_set($prepared, $key, $value);
479
        }
480
481
        return $prepared;
482
    }
483
484
    /**
485
     * Set saving callback.
486
     *
487
     * @param callable $callback
488
     *
489
     * @return void
490
     */
491
    public function saving(Closure $callback)
492
    {
493
        $this->saving = $callback;
494
    }
495
496
    /**
497
     * Set saved callback.
498
     *
499
     * @param callable $callback
500
     *
501
     * @return void
502
     */
503
    public function saved(Closure $callback)
504
    {
505
        $this->saved = $callback;
506
    }
507
508
    /**
509
     * @param array        $data
510
     * @param string|array $columns
511
     *
512
     * @return array|mixed
513
     */
514
    protected static function getDataByColumn($data, $columns)
515
    {
516
        if (is_string($columns)) {
517
            return array_get($data, $columns);
518
        }
519
520
        if (is_array($columns)) {
521
            $value = [];
522
            foreach ($columns as $name => $column) {
523
                if (!array_has($data, $column)) {
524
                    continue;
525
                }
526
                $value[$name] = array_get($data, $column);
527
            }
528
529
            return $value;
530
        }
531
    }
532
533
    /**
534
     * Find field object by column.
535
     *
536
     * @param $column
537
     *
538
     * @return mixed
539
     */
540
    protected function getFieldByColumn($column)
541
    {
542
        return $this->builder->fields()->first(
543
            function ($index, Field $field) use ($column) {
544
                if (is_array($field->column())) {
545
                    return in_array($column, $field->column());
546
                }
547
548
                return $field->column() == $column;
549
            }
550
        );
551
    }
552
553
    /**
554
     * Set original data for each field.
555
     *
556
     * @return void
557
     */
558
    protected function setFieldOriginalValue()
559
    {
560
        $values = $this->model->toArray();
561
562
        $this->builder->fields()->each(function (Field $field) use ($values) {
563
            $field->setOriginal($values);
564
        });
565
    }
566
567
    /**
568
     * Set all fields value in form.
569
     *
570
     * @param $id
571
     *
572
     * @return void
573
     */
574
    protected function setFieldValue($id)
575
    {
576
        $relations = $this->getRelations();
577
578
        $this->model = $this->model->with($relations)->findOrFail($id);
579
580
        $data = $this->model->toArray();
581
582
        $this->builder->fields()->each(function (Field $field) use ($data) {
583
            $field->fill($data);
584
        });
585
    }
586
587
    /**
588
     * Validate input data.
589
     *
590
     * @param $input
591
     *
592
     * @return bool
593
     */
594
    protected function validate($input)
595
    {
596
        $data = $rules = [];
597
598
        foreach ($this->builder->fields() as $field) {
0 ignored issues
show
Bug introduced by
The expression $this->builder->fields() of type object<Illuminate\Support\Collection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
599
            if (!method_exists($field, 'rules') || !$rule = $field->rules()) {
600
                continue;
601
            }
602
603
            $columns = $field->column();
604
605
            if (is_string($columns)) {
606
607
                if (!array_key_exists($columns, $input)) {
608
                    continue;
609
                }
610
611
                $data[$field->label()] = array_get($input, $columns);
612
                $rules[$field->label()] = $rule;
613
            }
614
615
            if (is_array($columns)) {
616
                foreach ($columns as $key => $column) {
617
                    if (!array_key_exists($column, $input)) {
618
                        continue;
619
                    }
620
                    $data[$field->label().$key] = array_get($input, $column);
621
                    $rules[$field->label().$key] = $rule;
622
                }
623
            }
624
        }
625
626
        $this->validator = Validator::make($data, $rules);
627
628
        return $this->validator->passes();
629
    }
630
631
    /**
632
     * Get all relations of model from callable.
633
     *
634
     * @return array
635
     */
636
    public function getRelations()
637
    {
638
        $relations = $columns = [];
639
640
        foreach ($this->builder->fields() as $field) {
0 ignored issues
show
Bug introduced by
The expression $this->builder->fields() of type object<Illuminate\Support\Collection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
641
            $columns[] = $field->column();
642
        }
643
644
        foreach (array_flatten($columns) as $column) {
645
            if (str_contains($column, '.')) {
646
                list($relation) = explode('.', $column);
647
648
                if (method_exists($this->model, $relation) &&
649
                    $this->model->$relation() instanceof Relation
650
                ) {
651
                    $relations[] = $relation;
652
                }
653
            } elseif (method_exists($this->model, $column)) {
654
                $relations[] = $column;
655
            }
656
        }
657
658
        return array_unique($relations);
659
    }
660
661
    /**
662
     * Get current resource route url.
663
     *
664
     * @return string
665
     */
666
    public function resource()
667
    {
668
        $route = app('router')->current();
669
        $prefix = $route->getPrefix();
670
671
        $resource = trim(preg_replace("#$prefix#", '', $route->getUri(), 1), '/').'/';
672
673
        return "/$prefix/".substr($resource, 0, strpos($resource, '/'));
674
    }
675
676
    /**
677
     * Render the form contents.
678
     *
679
     * @return string
680
     */
681
    public function render()
682
    {
683
        try {
684
            return $this->builder->render();
685
        } catch (\Exception $e) {
686
            return with(new Handle($e))->render();
687
        }
688
    }
689
690
    /**
691
     * Get or set input data.
692
     *
693
     * @param string $key
694
     * @param null   $value
695
     *
696
     * @return array|mixed
697
     */
698
    public function input($key, $value = null)
699
    {
700
        if (is_null($value)) {
701
            return array_get($this->inputs, $key);
702
        }
703
704
        return array_set($this->inputs, $key, $value);
705
    }
706
707
    /**
708
     * Getter.
709
     *
710
     * @param string $name
711
     *
712
     * @return array|mixed
713
     */
714
    public function __get($name)
715
    {
716
        return $this->input($name);
717
    }
718
719
    /**
720
     * Setter.
721
     *
722
     * @param string $name
723
     * @param $value
724
     */
725
    public function __set($name, $value)
726
    {
727
        $this->input($name, $value);
728
    }
729
730
    /**
731
     * Generate a Field object and add to form builder if Field exists.
732
     *
733
     * @param string $method
734
     * @param array  $arguments
735
     *
736
     * @return Field|void
737
     */
738 View Code Duplication
    public function __call($method, $arguments)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
739
    {
740
        if ($className = static::findFieldClass($method)) {
741
            $column = array_get($arguments, 0, ''); //[0];
742
743
            $element = new $className($column, array_slice($arguments, 1));
744
745
            $this->pushField($element);
746
747
            return $element;
748
        }
749
    }
750
751 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...
752
    {
753
        $className = __NAMESPACE__.'\\Form\\Field\\'.ucfirst($method);
754
755
        if (class_exists($className)) {
756
            return $className;
757
        }
758
759
        if ($method == 'switch') {
760
            return __NAMESPACE__.'\\Form\\Field\\SwitchField';
761
        }
762
763
        return false;
764
    }
765
766
    /**
767
     * Render the contents of the form when casting to string.
768
     *
769
     * @return string
770
     */
771
    public function __toString()
772
    {
773
        return $this->render();
774
    }
775
}
776