Completed
Push — master ( f9866e...fa91dc )
by Song
02:51
created

src/Form/Field/HasMany.php (2 issues)

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\Form\Field;
4
5
use Encore\Admin\Admin;
6
use Encore\Admin\Form;
7
use Encore\Admin\Form\Field;
8
use Encore\Admin\Form\NestedForm;
9
use Illuminate\Database\Eloquent\Relations\HasMany as Relation;
10
use Illuminate\Database\Eloquent\Relations\MorphMany;
11
use Illuminate\Support\Facades\Validator;
12
use Illuminate\Support\Str;
13
14
/**
15
 * Class HasMany.
16
 */
17
class HasMany extends Field
18
{
19
    /**
20
     * Relation name.
21
     *
22
     * @var string
23
     */
24
    protected $relationName = '';
25
26
    /**
27
     * Form builder.
28
     *
29
     * @var \Closure
30
     */
31
    protected $builder = null;
32
33
    /**
34
     * Form data.
35
     *
36
     * @var array
37
     */
38
    protected $value = [];
39
40
    /**
41
     * View Mode.
42
     *
43
     * Supports `default` and `tab` currently.
44
     *
45
     * @var string
46
     */
47
    protected $viewMode = 'default';
48
49
    /**
50
     * Available views for HasMany field.
51
     *
52
     * @var array
53
     */
54
    protected $views = [
55
        'default' => 'admin::form.hasmany',
56
        'tab'     => 'admin::form.hasmanytab',
57
        'table'   => 'admin::form.hasmanytable',
58
    ];
59
60
    /**
61
     * Options for template.
62
     *
63
     * @var array
64
     */
65
    protected $options = [
66
        'allowCreate' => true,
67
        'allowDelete' => true,
68
    ];
69
70
    /**
71
     * Create a new HasMany field instance.
72
     *
73
     * @param $relationName
74
     * @param array $arguments
75
     */
76 View Code Duplication
    public function __construct($relationName, $arguments = [])
77
    {
78
        $this->relationName = $relationName;
79
80
        $this->column = $relationName;
81
82
        if (count($arguments) == 1) {
83
            $this->label = $this->formatLabel();
84
            $this->builder = $arguments[0];
85
        }
86
87
        if (count($arguments) == 2) {
88
            list($this->label, $this->builder) = $arguments;
89
        }
90
    }
91
92
    /**
93
     * Get validator for this field.
94
     *
95
     * @param array $input
96
     *
97
     * @return bool|Validator
98
     */
99
    public function getValidator(array $input)
100
    {
101
        if (!array_key_exists($this->column, $input)) {
102
            return false;
103
        }
104
105
        $input = array_only($input, $this->column);
106
107
        $form = $this->buildNestedForm($this->column, $this->builder);
108
109
        $rules = $attributes = [];
110
111
        /* @var Field $field */
112
        foreach ($form->fields() as $field) {
113
            if (!$fieldRules = $field->getRules()) {
114
                continue;
115
            }
116
117
            $column = $field->column();
118
119
            if (is_array($column)) {
120
                foreach ($column as $key => $name) {
121
                    $rules[$name.$key] = $fieldRules;
122
                }
123
124
                $this->resetInputKey($input, $column);
125
            } else {
126
                $rules[$column] = $fieldRules;
127
            }
128
129
            $attributes = array_merge(
130
                $attributes,
131
                $this->formatValidationAttribute($input, $field->label(), $column)
132
            );
133
        }
134
135
        array_forget($rules, NestedForm::REMOVE_FLAG_NAME);
136
137
        if (empty($rules)) {
138
            return false;
139
        }
140
141
        $newRules = [];
142
        $newInput = [];
143
144
        foreach ($rules as $column => $rule) {
145
            foreach (array_keys($input[$this->column]) as $key) {
146
                $newRules["{$this->column}.$key.$column"] = $rule;
147
                if (isset($input[$this->column][$key][$column]) &&
148
                    is_array($input[$this->column][$key][$column])) {
149
                    foreach ($input[$this->column][$key][$column] as $vkey => $value) {
150
                        $newInput["{$this->column}.$key.{$column}$vkey"] = $value;
151
                    }
152
                }
153
            }
154
        }
155
156
        if (empty($newInput)) {
157
            $newInput = $input;
158
        }
159
160
        return Validator::make($newInput, $newRules, $this->validationMessages, $attributes);
161
    }
162
163
    /**
164
     * Format validation attributes.
165
     *
166
     * @param array  $input
167
     * @param string $label
168
     * @param string $column
169
     *
170
     * @return array
171
     */
172 View Code Duplication
    protected function formatValidationAttribute($input, $label, $column)
173
    {
174
        $new = $attributes = [];
175
176
        if (is_array($column)) {
177
            foreach ($column as $index => $col) {
178
                $new[$col.$index] = $col;
179
            }
180
        }
181
182
        foreach (array_keys(array_dot($input)) as $key) {
183
            if (is_string($column)) {
184
                if (Str::endsWith($key, ".$column")) {
185
                    $attributes[$key] = $label;
186
                }
187
            } else {
188
                foreach ($new as $k => $val) {
189
                    if (Str::endsWith($key, ".$k")) {
190
                        $attributes[$key] = $label."[$val]";
191
                    }
192
                }
193
            }
194
        }
195
196
        return $attributes;
197
    }
198
199
    /**
200
     * Reset input key for validation.
201
     *
202
     * @param array $input
203
     * @param array $column $column is the column name array set
204
     *
205
     * @return void.
206
     */
207
    protected function resetInputKey(array &$input, array $column)
208
    {
209
        /**
210
         * flip the column name array set.
211
         *
212
         * for example, for the DateRange, the column like as below
213
         *
214
         * ["start" => "created_at", "end" => "updated_at"]
215
         *
216
         * to:
217
         *
218
         * [ "created_at" => "start", "updated_at" => "end" ]
219
         */
220
        $column = array_flip($column);
221
222
        /**
223
         * $this->column is the inputs array's node name, default is the relation name.
224
         *
225
         * So... $input[$this->column] is the data of this column's inputs data
226
         *
227
         * in the HasMany relation, has many data/field set, $set is field set in the below
228
         */
229
        foreach ($input[$this->column] as $index => $set) {
230
231
            /*
232
             * foreach the field set to find the corresponding $column
233
             */
234 View Code Duplication
            foreach ($set as $name => $value) {
235
                /*
236
                 * if doesn't have column name, continue to the next loop
237
                 */
238
                if (!array_key_exists($name, $column)) {
239
                    continue;
240
                }
241
242
                /**
243
                 * example:  $newKey = created_atstart.
244
                 *
245
                 * Σ( ° △ °|||)︴
246
                 *
247
                 * I don't know why a form need range input? Only can imagine is for range search....
248
                 */
249
                $newKey = $name.$column[$name];
250
251
                /*
252
                 * set new key
253
                 */
254
                array_set($input, "{$this->column}.$index.$newKey", $value);
255
                /*
256
                 * forget the old key and value
257
                 */
258
                array_forget($input, "{$this->column}.$index.$name");
259
            }
260
        }
261
    }
262
263
    /**
264
     * Prepare input data for insert or update.
265
     *
266
     * @param array $input
267
     *
268
     * @return array
269
     */
270
    public function prepare($input)
271
    {
272
        $form = $this->buildNestedForm($this->column, $this->builder);
273
274
        return $form->setOriginal($this->original, $this->getKeyName())->prepare($input);
275
    }
276
277
    /**
278
     * Build a Nested form.
279
     *
280
     * @param string   $column
281
     * @param \Closure $builder
282
     * @param null     $key
283
     *
284
     * @return NestedForm
285
     */
286
    protected function buildNestedForm($column, \Closure $builder, $key = null)
287
    {
288
        $form = new Form\NestedForm($column, $key);
289
290
        $form->setForm($this->form);
291
292
        call_user_func($builder, $form);
293
294
        $form->hidden($this->getKeyName());
295
296
        $form->hidden(NestedForm::REMOVE_FLAG_NAME)->default(0)->addElementClass(NestedForm::REMOVE_FLAG_CLASS);
297
298
        return $form;
299
    }
300
301
    /**
302
     * Get the HasMany relation key name.
303
     *
304
     * @return string
305
     */
306
    protected function getKeyName()
307
    {
308
        if (is_null($this->form)) {
309
            return;
310
        }
311
312
        return $this->form->model()->{$this->relationName}()->getRelated()->getKeyName();
313
    }
314
315
    /**
316
     * Set view mode.
317
     *
318
     * @param string $mode currently support `tab` mode.
319
     *
320
     * @return $this
321
     *
322
     * @author Edwin Hui
323
     */
324
    public function mode($mode)
325
    {
326
        $this->viewMode = $mode;
327
328
        return $this;
329
    }
330
331
    /**
332
     * Use tab mode to showing hasmany field.
333
     *
334
     * @return HasMany
335
     */
336
    public function useTab()
337
    {
338
        return $this->mode('tab');
339
    }
340
    
341
    /**
342
     * Use table mode to showing hasmany field.
343
     *
344
     * @return HasMany
345
     */
346
    public function useTable()
347
    {
348
        return $this->mode('table');
349
    }
350
351
    /**
352
     * Build Nested form for related data.
353
     *
354
     * @throws \Exception
355
     *
356
     * @return array
357
     */
358
    protected function buildRelatedForms()
359
    {
360
        if (is_null($this->form)) {
361
            return [];
362
        }
363
364
        $model = $this->form->model();
365
366
        $relation = call_user_func([$model, $this->relationName]);
367
368
        if (!$relation instanceof Relation && !$relation instanceof MorphMany) {
369
            throw new \Exception('hasMany field must be a HasMany or MorphMany relation.');
370
        }
371
372
        $forms = [];
373
374
        /*
375
         * If redirect from `exception` or `validation error` page.
376
         *
377
         * Then get form data from session flash.
378
         *
379
         * Else get data from database.
380
         */
381
        if ($values = old($this->column)) {
382
            foreach ($values as $key => $data) {
383
                if ($data[NestedForm::REMOVE_FLAG_NAME] == 1) {
384
                    continue;
385
                }
386
387
                $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $key)
388
                    ->fill($data);
389
            }
390
        } else {
391
            foreach ($this->value as $data) {
392
                $key = array_get($data, $relation->getRelated()->getKeyName());
393
394
                $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $key)
395
                    ->fill($data);
396
            }
397
        }
398
399
        return $forms;
400
    }
401
402
    /**
403
     * Setup script for this field in different view mode.
404
     *
405
     * @param string $script
406
     *
407
     * @return void
408
     */
409
    protected function setupScript($script)
410
    {
411
        $method = 'setupScriptFor'.ucfirst($this->viewMode).'View';
412
413
        call_user_func([$this, $method], $script);
414
    }
415
416
    /**
417
     * Setup default template script.
418
     *
419
     * @param string $templateScript
420
     *
421
     * @return void
422
     */
423 View Code Duplication
    protected function setupScriptForDefaultView($templateScript)
424
    {
425
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
426
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
427
428
        /**
429
         * When add a new sub form, replace all element key in new sub form.
430
         *
431
         * @example comments[new___key__][title]  => comments[new_{index}][title]
432
         *
433
         * {count} is increment number of current sub form count.
434
         */
435
        $script = <<<EOT
436
var index = 0;
437
$('#has-many-{$this->column}').on('click', '.add', function () {
438
439
    var tpl = $('template.{$this->column}-tpl');
440
441
    index++;
442
443
    var template = tpl.html().replace(/{$defaultKey}/g, index);
444
    $('.has-many-{$this->column}-forms').append(template);
445
    {$templateScript}
446
});
447
448
$('#has-many-{$this->column}').on('click', '.remove', function () {
449
    $(this).closest('.has-many-{$this->column}-form').hide();
450
    $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
451
});
452
453
EOT;
454
455
        Admin::script($script);
456
    }
457
458
    /**
459
     * Setup tab template script.
460
     *
461
     * @param string $templateScript
462
     *
463
     * @return void
464
     */
465 View Code Duplication
    protected function setupScriptForTabView($templateScript)
466
    {
467
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
468
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
469
470
        $script = <<<EOT
471
472
$('#has-many-{$this->column} > .nav').off('click', 'i.close-tab').on('click', 'i.close-tab', function(){
473
    var \$navTab = $(this).siblings('a');
474
    var \$pane = $(\$navTab.attr('href'));
475
    if( \$pane.hasClass('new') ){
476
        \$pane.remove();
477
    }else{
478
        \$pane.removeClass('active').find('.$removeClass').val(1);
479
    }
480
    if(\$navTab.closest('li').hasClass('active')){
481
        \$navTab.closest('li').remove();
482
        $('#has-many-{$this->column} > .nav > li:nth-child(1) > a').tab('show');
483
    }else{
484
        \$navTab.closest('li').remove();
485
    }
486
});
487
488
var index = 0;
489
$('#has-many-{$this->column} > .header').off('click', '.add').on('click', '.add', function(){
490
    index++;
491
    var navTabHtml = $('#has-many-{$this->column} > template.nav-tab-tpl').html().replace(/{$defaultKey}/g, index);
492
    var paneHtml = $('#has-many-{$this->column} > template.pane-tpl').html().replace(/{$defaultKey}/g, index);
493
    $('#has-many-{$this->column} > .nav').append(navTabHtml);
494
    $('#has-many-{$this->column} > .tab-content').append(paneHtml);
495
    $('#has-many-{$this->column} > .nav > li:last-child a').tab('show');
496
    {$templateScript}
497
});
498
499
if ($('.has-error').length) {
500
    $('.has-error').parent('.tab-pane').each(function () {
501
        var tabId = '#'+$(this).attr('id');
502
        $('li a[href="'+tabId+'"] i').removeClass('hide');
503
    });
504
    
505
    var first = $('.has-error:first').parent().attr('id');
506
    $('li a[href="#'+first+'"]').tab('show');
507
}
508
EOT;
509
510
        Admin::script($script);
511
    }
512
    
513
    /**
514
     * Setup default template script.
515
     *
516
     * @param string $templateScript
517
     *
518
     * @return void
519
     */
520 View Code Duplication
    protected function setupScriptForTableView($templateScript)
521
    {
522
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
523
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
524
        
525
        /**
526
         * When add a new sub form, replace all element key in new sub form.
527
         *
528
         * @example comments[new___key__][title]  => comments[new_{index}][title]
529
         *
530
         * {count} is increment number of current sub form count.
531
         */
532
        $script = <<<EOT
533
var index = 0;
534
$('#has-many-{$this->column}').on('click', '.add', function () {
535
536
    var tpl = $('template.{$this->column}-tpl');
537
538
    index++;
539
540
    var template = tpl.html().replace(/{$defaultKey}/g, index);
541
    $('.has-many-{$this->column}-forms').append(template);
542
    {$templateScript}
543
});
544
545
$('#has-many-{$this->column}').on('click', '.remove', function () {
546
    $(this).closest('.has-many-{$this->column}-form').hide();
547
    $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
548
});
549
550
EOT;
551
        
552
        Admin::script($script);
553
    }
554
555
    /**
556
     * Disable create button.
557
     *
558
     * @return $this
559
     */
560
    public function disableCreate()
561
    {
562
        $this->options['allowCreate'] = false;
563
564
        return $this;
565
    }
566
567
    /**
568
     * Disable delete button.
569
     *
570
     * @return $this
571
     */
572
    public function disableDelete()
573
    {
574
        $this->options['allowDelete'] = false;
575
576
        return $this;
577
    }
578
579
    /**
580
     * Render the `HasMany` field.
581
     *
582
     * @throws \Exception
583
     *
584
     * @return \Illuminate\View\View
585
     */
586
    public function render()
587
    {
588
        if ($this->viewMode == 'table') {
589
            return $this->renderTable();
590
        }
591
592
        // specify a view to render.
593
        $this->view = $this->views[$this->viewMode];
594
595
        list($template, $script) = $this->buildNestedForm($this->column, $this->builder)
596
            ->getTemplateHtmlAndScript();
597
598
        $this->setupScript($script);
599
600
        return parent::render()->with([
601
            'forms'        => $this->buildRelatedForms(),
602
            'template'     => $template,
603
            'relationName' => $this->relationName,
604
            'options'      => $this->options,
605
        ]);
606
    }
607
608
    /**
609
     * Render the `HasMany` field for table style
610
     *
611
     * @return mixed
612
     * @throws \Exception
613
     */
614
    protected function renderTable()
615
    {
616
        $headers = [];
617
        $fields = [];
618
        $hidden = [];
619
        $scripts = [];
620
621
        /* @var Field $field */
622
        foreach ($this->buildNestedForm($this->column, $this->builder)->fields() as $field) {
0 ignored issues
show
It seems like $this->column can also be of type array; however, Encore\Admin\Form\Field\HasMany::buildNestedForm() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
623
624
            if (is_a($field, Hidden::class)) {
625
                $hidden[] = $field->render();
626
            } else {
627
                /* Hide label and set field width 100% */
628
                $field->setLabelClass(['hidden']);
629
                $field->setWidth(12, 0);
630
                $fields[] = $field->render();
631
                $headers[] = $field->label();
632
            }
633
634
            /*
635
             * Get and remove the last script of Admin::$script stack.
636
             */
637
            if ($field->getScript()) {
638
                $scripts[] = array_pop(Admin::$script);
639
            }
640
        }
641
642
        /* Build row elements */
643
        $template = array_reduce($fields, function ($all, $field) {
644
            $all .= "<td>{$field}</td>";
645
            return $all;
646
        }, '');
647
648
        /* Build cell with hidden elements */
649
        $template .= '<td class="hidden">' . implode('', $hidden) . '</td>';
650
651
        $this->setupScript(implode("\r\n", $scripts));
652
653
        // specify a view to render.
654
        $this->view = $this->views[$this->viewMode];
655
656
        return parent::render()->with([
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (render() instead of renderTable()). Are you sure this is correct? If so, you might want to change this to $this->render().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
657
            'headers'      => $headers,
658
            'forms'        => $this->buildRelatedForms(),
659
            'template'     => $template,
660
            'relationName' => $this->relationName,
661
            'options'      => $this->options,
662
        ]);
663
    }
664
}
665