Completed
Push — master ( 82be35...1537c0 )
by Song
02:28
created

src/Form/Field/HasMany.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\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\Arr;
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
     * Distinct fields.
72
     *
73
     * @var array
74
     */
75
    protected $distinctFields = [];
76
77
    /**
78
     * Create a new HasMany field instance.
79
     *
80
     * @param $relationName
81
     * @param array $arguments
82
     */
83 View Code Duplication
    public function __construct($relationName, $arguments = [])
84
    {
85
        $this->relationName = $relationName;
86
87
        $this->column = $relationName;
88
89
        if (count($arguments) == 1) {
90
            $this->label = $this->formatLabel();
91
            $this->builder = $arguments[0];
92
        }
93
94
        if (count($arguments) == 2) {
95
            list($this->label, $this->builder) = $arguments;
96
        }
97
    }
98
99
    /**
100
     * Get validator for this field.
101
     *
102
     * @param array $input
103
     *
104
     * @return bool|\Illuminate\Contracts\Validation\Validator
105
     */
106
    public function getValidator(array $input)
107
    {
108
        if (!array_key_exists($this->column, $input)) {
109
            return false;
110
        }
111
112
        $input = Arr::only($input, $this->column);
113
114
        /** unset item that contains remove flag */
115
        foreach ($input[$this->column] as $key => $value) {
116
            if ($value[NestedForm::REMOVE_FLAG_NAME]) {
117
                unset($input[$this->column][$key]);
118
            }
119
        }
120
121
        $form = $this->buildNestedForm($this->column, $this->builder);
122
123
        $rules = $attributes = [];
124
125
        /* @var Field $field */
126
        foreach ($form->fields() as $field) {
127
            if (!$fieldRules = $field->getRules()) {
128
                continue;
129
            }
130
131
            $column = $field->column();
132
133
            if (is_array($column)) {
134
                foreach ($column as $key => $name) {
135
                    $rules[$name.$key] = $fieldRules;
136
                }
137
138
                $this->resetInputKey($input, $column);
139
            } else {
140
                $rules[$column] = $fieldRules;
141
            }
142
143
            $attributes = array_merge(
144
                $attributes,
145
                $this->formatValidationAttribute($input, $field->label(), $column)
146
            );
147
        }
148
149
        Arr::forget($rules, NestedForm::REMOVE_FLAG_NAME);
150
151
        if (empty($rules)) {
152
            return false;
153
        }
154
155
        $newRules = [];
156
        $newInput = [];
157
158
        foreach ($rules as $column => $rule) {
159
            foreach (array_keys($input[$this->column]) as $key) {
160
                $newRules["{$this->column}.$key.$column"] = $rule;
161
                if (isset($input[$this->column][$key][$column]) &&
162
                    is_array($input[$this->column][$key][$column])) {
163
                    foreach ($input[$this->column][$key][$column] as $vkey => $value) {
164
                        $newInput["{$this->column}.$key.{$column}$vkey"] = $value;
165
                    }
166
                }
167
            }
168
        }
169
170
        if (empty($newInput)) {
171
            $newInput = $input;
172
        }
173
174
        $this->appendDistinctRules($newRules);
175
176
        return \validator($newInput, $newRules, $this->getValidationMessages(), $attributes);
177
    }
178
179
    /**
180
     * Set distinct fields.
181
     *
182
     * @param array $fields
183
     *
184
     * @return $this
185
     */
186
    public function distinctFields(array $fields)
187
    {
188
        $this->distinctFields = $fields;
189
190
        return $this;
191
    }
192
193
    /**
194
     * Append distinct rules.
195
     *
196
     * @param array $rules
197
     */
198
    protected function appendDistinctRules(array &$rules)
199
    {
200
        foreach ($this->distinctFields as $field) {
201
            $rules["{$this->column}.*.$field"] = 'distinct';
202
        }
203
    }
204
205
    /**
206
     * Format validation attributes.
207
     *
208
     * @param array  $input
209
     * @param string $label
210
     * @param string $column
211
     *
212
     * @return array
213
     */
214 View Code Duplication
    protected function formatValidationAttribute($input, $label, $column)
215
    {
216
        $new = $attributes = [];
217
218
        if (is_array($column)) {
219
            foreach ($column as $index => $col) {
220
                $new[$col.$index] = $col;
221
            }
222
        }
223
224
        foreach (array_keys(Arr::dot($input)) as $key) {
225
            if (is_string($column)) {
226
                if (Str::endsWith($key, ".$column")) {
227
                    $attributes[$key] = $label;
228
                }
229
            } else {
230
                foreach ($new as $k => $val) {
231
                    if (Str::endsWith($key, ".$k")) {
232
                        $attributes[$key] = $label."[$val]";
233
                    }
234
                }
235
            }
236
        }
237
238
        return $attributes;
239
    }
240
241
    /**
242
     * Reset input key for validation.
243
     *
244
     * @param array $input
245
     * @param array $column $column is the column name array set
246
     *
247
     * @return void.
248
     */
249
    protected function resetInputKey(array &$input, array $column)
250
    {
251
        /**
252
         * flip the column name array set.
253
         *
254
         * for example, for the DateRange, the column like as below
255
         *
256
         * ["start" => "created_at", "end" => "updated_at"]
257
         *
258
         * to:
259
         *
260
         * [ "created_at" => "start", "updated_at" => "end" ]
261
         */
262
        $column = array_flip($column);
263
264
        /**
265
         * $this->column is the inputs array's node name, default is the relation name.
266
         *
267
         * So... $input[$this->column] is the data of this column's inputs data
268
         *
269
         * in the HasMany relation, has many data/field set, $set is field set in the below
270
         */
271
        foreach ($input[$this->column] as $index => $set) {
272
273
            /*
274
             * foreach the field set to find the corresponding $column
275
             */
276 View Code Duplication
            foreach ($set as $name => $value) {
277
                /*
278
                 * if doesn't have column name, continue to the next loop
279
                 */
280
                if (!array_key_exists($name, $column)) {
281
                    continue;
282
                }
283
284
                /**
285
                 * example:  $newKey = created_atstart.
286
                 *
287
                 * Σ( ° △ °|||)︴
288
                 *
289
                 * I don't know why a form need range input? Only can imagine is for range search....
290
                 */
291
                $newKey = $name.$column[$name];
292
293
                /*
294
                 * set new key
295
                 */
296
                Arr::set($input, "{$this->column}.$index.$newKey", $value);
297
                /*
298
                 * forget the old key and value
299
                 */
300
                Arr::forget($input, "{$this->column}.$index.$name");
301
            }
302
        }
303
    }
304
305
    /**
306
     * Prepare input data for insert or update.
307
     *
308
     * @param array $input
309
     *
310
     * @return array
311
     */
312
    public function prepare($input)
313
    {
314
        $form = $this->buildNestedForm($this->column, $this->builder);
315
316
        return $form->setOriginal($this->original, $this->getKeyName())->prepare($input);
317
    }
318
319
    /**
320
     * Build a Nested form.
321
     *
322
     * @param string   $column
323
     * @param \Closure $builder
324
     * @param null     $model
325
     *
326
     * @return NestedForm
327
     */
328 View Code Duplication
    protected function buildNestedForm($column, \Closure $builder, $model = null)
329
    {
330
        $form = new Form\NestedForm($column, $model);
331
332
        $form->setForm($this->form);
333
334
        call_user_func($builder, $form);
335
336
        $form->hidden($this->getKeyName());
337
338
        $form->hidden(NestedForm::REMOVE_FLAG_NAME)->default(0)->addElementClass(NestedForm::REMOVE_FLAG_CLASS);
339
340
        return $form;
341
    }
342
343
    /**
344
     * Get the HasMany relation key name.
345
     *
346
     * @return string
347
     */
348
    protected function getKeyName()
349
    {
350
        if (is_null($this->form)) {
351
            return;
352
        }
353
354
        return $this->form->model()->{$this->relationName}()->getRelated()->getKeyName();
355
    }
356
357
    /**
358
     * Set view mode.
359
     *
360
     * @param string $mode currently support `tab` mode.
361
     *
362
     * @return $this
363
     *
364
     * @author Edwin Hui
365
     */
366
    public function mode($mode)
367
    {
368
        $this->viewMode = $mode;
369
370
        return $this;
371
    }
372
373
    /**
374
     * Use tab mode to showing hasmany field.
375
     *
376
     * @return HasMany
377
     */
378
    public function useTab()
379
    {
380
        return $this->mode('tab');
381
    }
382
383
    /**
384
     * Use table mode to showing hasmany field.
385
     *
386
     * @return HasMany
387
     */
388
    public function useTable()
389
    {
390
        return $this->mode('table');
391
    }
392
393
    /**
394
     * Build Nested form for related data.
395
     *
396
     * @throws \Exception
397
     *
398
     * @return array
399
     */
400
    protected function buildRelatedForms()
401
    {
402
        if (is_null($this->form)) {
403
            return [];
404
        }
405
406
        $model = $this->form->model();
407
408
        $relation = call_user_func([$model, $this->relationName]);
409
410
        if (!$relation instanceof Relation && !$relation instanceof MorphMany) {
411
            throw new \Exception('hasMany field must be a HasMany or MorphMany relation.');
412
        }
413
414
        $forms = [];
415
416
        /*
417
         * If redirect from `exception` or `validation error` page.
418
         *
419
         * Then get form data from session flash.
420
         *
421
         * Else get data from database.
422
         */
423
        if ($values = old($this->column)) {
424
            foreach ($values as $key => $data) {
425
                if ($data[NestedForm::REMOVE_FLAG_NAME] == 1) {
426
                    continue;
427
                }
428
429
                $model = $relation->getRelated()->replicate()->forceFill($data);
430
431
                $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $model)
432
                    ->fill($data);
433
            }
434
        } else {
435
            if (empty($this->value)) {
436
                return [];
437
            }
438
439
            foreach ($this->value as $data) {
440
                $key = Arr::get($data, $relation->getRelated()->getKeyName());
441
442
                $model = $relation->getRelated()->replicate()->forceFill($data);
443
444
                $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $model)
445
                    ->fill($data);
446
            }
447
        }
448
449
        return $forms;
450
    }
451
452
    /**
453
     * Setup script for this field in different view mode.
454
     *
455
     * @param string $script
456
     *
457
     * @return void
458
     */
459
    protected function setupScript($script)
460
    {
461
        $method = 'setupScriptFor'.ucfirst($this->viewMode).'View';
462
463
        call_user_func([$this, $method], $script);
464
    }
465
466
    /**
467
     * Setup default template script.
468
     *
469
     * @param string $templateScript
470
     *
471
     * @return void
472
     */
473
    protected function setupScriptForDefaultView($templateScript)
474
    {
475
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
476
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
477
478
        /**
479
         * When add a new sub form, replace all element key in new sub form.
480
         *
481
         * @example comments[new___key__][title]  => comments[new_{index}][title]
482
         *
483
         * {count} is increment number of current sub form count.
484
         */
485
        $script = <<<EOT
486
var index = 0;
487
$('#has-many-{$this->column}').off('click', '.add').on('click', '.add', function () {
488
489
    var tpl = $('template.{$this->column}-tpl');
490
491
    index++;
492
493
    var template = tpl.html().replace(/{$defaultKey}/g, index);
494
    $('.has-many-{$this->column}-forms').append(template);
495
    {$templateScript}
496
    return false;
497
});
498
499
$('#has-many-{$this->column}').off('click', '.remove').on('click', '.remove', function () {
500
    $(this).closest('.has-many-{$this->column}-form').find('input').removeAttr('required');
501
    $(this).closest('.has-many-{$this->column}-form').hide();
502
    $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
503
    return false;
504
});
505
506
EOT;
507
508
        Admin::script($script);
509
    }
510
511
    /**
512
     * Setup tab template script.
513
     *
514
     * @param string $templateScript
515
     *
516
     * @return void
517
     */
518 View Code Duplication
    protected function setupScriptForTabView($templateScript)
519
    {
520
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
521
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
522
523
        $script = <<<EOT
524
525
$('#has-many-{$this->column} > .nav').off('click', 'i.close-tab').on('click', 'i.close-tab', function(){
526
    var \$navTab = $(this).siblings('a');
527
    var \$pane = $(\$navTab.attr('href'));
528
    if( \$pane.hasClass('new') ){
529
        \$pane.remove();
530
    }else{
531
        \$pane.removeClass('active').find('.$removeClass').val(1);
532
    }
533
    if(\$navTab.closest('li').hasClass('active')){
534
        \$navTab.closest('li').remove();
535
        $('#has-many-{$this->column} > .nav > li:nth-child(1) > a').tab('show');
536
    }else{
537
        \$navTab.closest('li').remove();
538
    }
539
});
540
541
var index = 0;
542
$('#has-many-{$this->column} > .header').off('click', '.add').on('click', '.add', function(){
543
    index++;
544
    var navTabHtml = $('#has-many-{$this->column} > template.nav-tab-tpl').html().replace(/{$defaultKey}/g, index);
545
    var paneHtml = $('#has-many-{$this->column} > template.pane-tpl').html().replace(/{$defaultKey}/g, index);
546
    $('#has-many-{$this->column} > .nav').append(navTabHtml);
547
    $('#has-many-{$this->column} > .tab-content').append(paneHtml);
548
    $('#has-many-{$this->column} > .nav > li:last-child a').tab('show');
549
    {$templateScript}
550
});
551
552
if ($('.has-error').length) {
553
    $('.has-error').parent('.tab-pane').each(function () {
554
        var tabId = '#'+$(this).attr('id');
555
        $('li a[href="'+tabId+'"] i').removeClass('hide');
556
    });
557
558
    var first = $('.has-error:first').parent().attr('id');
559
    $('li a[href="#'+first+'"]').tab('show');
560
}
561
EOT;
562
563
        Admin::script($script);
564
    }
565
566
    /**
567
     * Setup default template script.
568
     *
569
     * @param string $templateScript
570
     *
571
     * @return void
572
     */
573 View Code Duplication
    protected function setupScriptForTableView($templateScript)
574
    {
575
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
576
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
577
578
        /**
579
         * When add a new sub form, replace all element key in new sub form.
580
         *
581
         * @example comments[new___key__][title]  => comments[new_{index}][title]
582
         *
583
         * {count} is increment number of current sub form count.
584
         */
585
        $script = <<<EOT
586
var index = 0;
587
$('#has-many-{$this->column}').on('click', '.add', function () {
588
589
    var tpl = $('template.{$this->column}-tpl');
590
591
    index++;
592
593
    var template = tpl.html().replace(/{$defaultKey}/g, index);
594
    $('.has-many-{$this->column}-forms').append(template);
595
    {$templateScript}
596
    return false;
597
});
598
599
$('#has-many-{$this->column}').on('click', '.remove', function () {
600
    var first_input_name = $(this).closest('.has-many-{$this->column}-form').find('input[name]:first').attr('name');
601
    if (first_input_name.match('{$this->column}\\\[new_')) {
602
        $(this).closest('.has-many-{$this->column}-form').remove();
603
    } else {
604
        $(this).closest('.has-many-{$this->column}-form').hide();
605
        $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
606
        $(this).closest('.has-many-{$this->column}-form').find('input').removeAttr('required');
607
    }
608
    return false;
609
});
610
611
EOT;
612
613
        Admin::script($script);
614
    }
615
616
    /**
617
     * Disable create button.
618
     *
619
     * @return $this
620
     */
621
    public function disableCreate()
622
    {
623
        $this->options['allowCreate'] = false;
624
625
        return $this;
626
    }
627
628
    /**
629
     * Disable delete button.
630
     *
631
     * @return $this
632
     */
633
    public function disableDelete()
634
    {
635
        $this->options['allowDelete'] = false;
636
637
        return $this;
638
    }
639
640
    /**
641
     * Render the `HasMany` field.
642
     *
643
     * @throws \Exception
644
     *
645
     * @return \Illuminate\View\View
646
     */
647
    public function render()
648
    {
649
        if (!$this->shouldRender()) {
650
            return '';
651
        }
652
653
        if ($this->viewMode == 'table') {
654
            return $this->renderTable();
655
        }
656
657
        // specify a view to render.
658
        $this->view = $this->views[$this->viewMode];
659
660
        list($template, $script) = $this->buildNestedForm($this->column, $this->builder)
661
            ->getTemplateHtmlAndScript();
662
663
        $this->setupScript($script);
664
665
        return parent::render()->with([
666
            'forms'        => $this->buildRelatedForms(),
667
            'template'     => $template,
668
            'relationName' => $this->relationName,
669
            'options'      => $this->options,
670
        ]);
671
    }
672
673
    /**
674
     * Render the `HasMany` field for table style.
675
     *
676
     * @throws \Exception
677
     *
678
     * @return mixed
679
     */
680
    protected function renderTable()
681
    {
682
        $headers = [];
683
        $fields = [];
684
        $hidden = [];
685
        $scripts = [];
686
687
        /* @var Field $field */
688
        foreach ($this->buildNestedForm($this->column, $this->builder)->fields() as $field) {
689
            if (is_a($field, Hidden::class)) {
690
                $hidden[] = $field->render();
691
            } else {
692
                /* Hide label and set field width 100% */
693
                $field->setLabelClass(['hidden']);
694
                $field->setWidth(12, 0);
695
                $fields[] = $field->render();
696
                $headers[] = $field->label();
697
            }
698
699
            /*
700
             * Get and remove the last script of Admin::$script stack.
701
             */
702
            if ($field->getScript()) {
703
                $scripts[] = array_pop(Admin::$script);
704
            }
705
        }
706
707
        /* Build row elements */
708
        $template = array_reduce($fields, function ($all, $field) {
709
            $all .= "<td>{$field}</td>";
710
711
            return $all;
712
        }, '');
713
714
        /* Build cell with hidden elements */
715
        $template .= '<td class="hidden">'.implode('', $hidden).'</td>';
716
717
        $this->setupScript(implode("\r\n", $scripts));
718
719
        // specify a view to render.
720
        $this->view = $this->views[$this->viewMode];
721
722
        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...
723
            'headers'      => $headers,
724
            'forms'        => $this->buildRelatedForms(),
725
            'template'     => $template,
726
            'relationName' => $this->relationName,
727
            'options'      => $this->options,
728
        ]);
729
    }
730
}
731