Completed
Pull Request — master (#2836)
by
unknown
03:03 queued 16s
created

HasMany::prepare()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
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
    ];
58
59
    /**
60
     * Options for template.
61
     *
62
     * @var array
63
     */
64
    protected $options = [
65
        'allowCreate' => true,
66
        'allowDelete' => true,
67
    ];
68
69
    /**
70
     * Create a new HasMany field instance.
71
     *
72
     * @param $relationName
73
     * @param array $arguments
74
     */
75 View Code Duplication
    public function __construct($relationName, $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...
76
    {
77
        $this->relationName = $relationName;
78
79
        $this->column = $relationName;
80
81
        if (count($arguments) == 1) {
82
            $this->label = $this->formatLabel();
83
            $this->builder = $arguments[0];
84
        }
85
86
        if (count($arguments) == 2) {
87
            list($this->label, $this->builder) = $arguments;
88
        }
89
    }
90
91
    /**
92
     * Get validator for this field.
93
     *
94
     * @param array $input
95
     *
96
     * @return bool|Validator
97
     */
98
    public function getValidator(array $input)
99
    {
100
        if (!array_key_exists($this->column, $input)) {
101
            return false;
102
        }
103
104
        $input = array_only($input, $this->column);
105
106
        $form = $this->buildNestedForm($this->column, $this->builder);
0 ignored issues
show
Bug introduced by
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...
107
108
        $rules = $attributes = [];
109
110
        /* @var Field $field */
111
        foreach ($form->fields() as $field) {
112
            if (!$fieldRules = $field->getRules()) {
113
                continue;
114
            }
115
116
            $column = $field->column();
117
118
            if (is_array($column)) {
119
                foreach ($column as $key => $name) {
120
                    $rules[$name.$key] = $fieldRules;
121
                }
122
123
                $this->resetInputKey($input, $column);
124
            } else {
125
                $rules[$column] = $fieldRules;
126
            }
127
128
            $attributes = array_merge(
129
                $attributes,
130
                $this->formatValidationAttribute($input, $field->label(), $column)
131
            );
132
        }
133
134
        array_forget($rules, NestedForm::REMOVE_FLAG_NAME);
135
136
        if (empty($rules)) {
137
            return false;
138
        }
139
140
        $newRules = [];
141
        $newInput = [];
142
143
        foreach ($rules as $column => $rule) {
144
            foreach (array_keys($input[$this->column]) as $key) {
145
                $newRules["{$this->column}.$key.$column"] = $rule;
146
                if (isset($input[$this->column][$key][$column]) &&
147
                    is_array($input[$this->column][$key][$column])) {
148
                    foreach ($input[$this->column][$key][$column] as $vkey => $value) {
149
                        $newInput["{$this->column}.$key.{$column}$vkey"] = $value;
150
                    }
151
                }
152
            }
153
        }
154
155
        if (empty($newInput)) {
156
            $newInput = $input;
157
        }
158
159
        return Validator::make($newInput, $newRules, $this->validationMessages, $attributes);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Illuminate\Suppo...Messages, $attributes); (Illuminate\Contracts\Validation\Validator) is incompatible with the return type of the parent method Encore\Admin\Form\Field::getValidator of type boolean|Illuminate\Support\Facades\Validator.

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...
160
    }
161
162
    /**
163
     * Format validation attributes.
164
     *
165
     * @param array  $input
166
     * @param string $label
167
     * @param string $column
168
     *
169
     * @return array
170
     */
171 View Code Duplication
    protected function formatValidationAttribute($input, $label, $column)
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...
172
    {
173
        $new = $attributes = [];
174
175
        if (is_array($column)) {
176
            foreach ($column as $index => $col) {
177
                $new[$col.$index] = $col;
178
            }
179
        }
180
181
        foreach (array_keys(array_dot($input)) as $key) {
182
            if (is_string($column)) {
183
                if (Str::endsWith($key, ".$column")) {
184
                    $attributes[$key] = $label;
185
                }
186
            } else {
187
                foreach ($new as $k => $val) {
188
                    if (Str::endsWith($key, ".$k")) {
189
                        $attributes[$key] = $label."[$val]";
190
                    }
191
                }
192
            }
193
        }
194
195
        return $attributes;
196
    }
197
198
    /**
199
     * Reset input key for validation.
200
     *
201
     * @param array $input
202
     * @param array $column $column is the column name array set
203
     *
204
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
205
     */
206
    protected function resetInputKey(array &$input, array $column)
207
    {
208
        /**
209
         * flip the column name array set.
210
         *
211
         * for example, for the DateRange, the column like as below
212
         *
213
         * ["start" => "created_at", "end" => "updated_at"]
214
         *
215
         * to:
216
         *
217
         * [ "created_at" => "start", "updated_at" => "end" ]
218
         */
219
        $column = array_flip($column);
220
221
        /**
222
         * $this->column is the inputs array's node name, default is the relation name.
223
         *
224
         * So... $input[$this->column] is the data of this column's inputs data
225
         *
226
         * in the HasMany relation, has many data/field set, $set is field set in the below
227
         */
228
        foreach ($input[$this->column] as $index => $set) {
229
230
            /*
231
             * foreach the field set to find the corresponding $column
232
             */
233 View Code Duplication
            foreach ($set as $name => $value) {
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...
234
                /*
235
                 * if doesn't have column name, continue to the next loop
236
                 */
237
                if (!array_key_exists($name, $column)) {
238
                    continue;
239
                }
240
241
                /**
242
                 * example:  $newKey = created_atstart.
243
                 *
244
                 * Σ( ° △ °|||)︴
245
                 *
246
                 * I don't know why a form need range input? Only can imagine is for range search....
247
                 */
248
                $newKey = $name.$column[$name];
249
250
                /*
251
                 * set new key
252
                 */
253
                array_set($input, "{$this->column}.$index.$newKey", $value);
254
                /*
255
                 * forget the old key and value
256
                 */
257
                array_forget($input, "{$this->column}.$index.$name");
258
            }
259
        }
260
    }
261
262
    /**
263
     * Prepare input data for insert or update.
264
     *
265
     * @param array $input
266
     *
267
     * @return array
268
     */
269
    public function prepare($input)
270
    {
271
        $form = $this->buildNestedForm($this->column, $this->builder);
0 ignored issues
show
Bug introduced by
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...
272
273
        return $form->setOriginal($this->original, $this->getKeyName())->prepare($input);
274
    }
275
276
    /**
277
     * Build a Nested form.
278
     *
279
     * @param string   $column
280
     * @param \Closure $builder
281
     * @param null     $key
282
     *
283
     * @return NestedForm
284
     */
285
    protected function buildNestedForm($column, \Closure $builder, $key = null)
286
    {
287
        $form = new Form\NestedForm($column, $key);
288
289
        $form->setForm($this->form);
290
291
        call_user_func($builder, $form);
292
293
        $form->hidden($this->getKeyName());
294
295
        $form->hidden(NestedForm::REMOVE_FLAG_NAME)->default(0)->addElementClass(NestedForm::REMOVE_FLAG_CLASS);
296
297
        return $form;
298
    }
299
300
    /**
301
     * Get the HasMany relation key name.
302
     *
303
     * @return string
304
     */
305
    protected function getKeyName()
306
    {
307
        if (is_null($this->form)) {
308
            return;
309
        }
310
311
        return $this->form->model()->{$this->relationName}()->getRelated()->getKeyName();
312
    }
313
314
    /**
315
     * Set view mode.
316
     *
317
     * @param string $mode currently support `tab` mode.
318
     *
319
     * @return $this
320
     *
321
     * @author Edwin Hui
322
     */
323
    public function mode($mode)
324
    {
325
        $this->viewMode = $mode;
326
327
        return $this;
328
    }
329
330
    /**
331
     * Use tab mode to showing hasmany field.
332
     *
333
     * @return HasMany
334
     */
335
    public function useTab()
336
    {
337
        return $this->mode('tab');
338
    }
339
340
    /**
341
     * Build Nested form for related data.
342
     *
343
     * @throws \Exception
344
     *
345
     * @return array
346
     */
347
    protected function buildRelatedForms()
348
    {
349
        if (is_null($this->form)) {
350
            return [];
351
        }
352
353
        $model = $this->form->model();
354
355
        $relation = call_user_func([$model, $this->relationName]);
356
357
        if (!$relation instanceof Relation && !$relation instanceof MorphMany) {
358
            throw new \Exception('hasMany field must be a HasMany or MorphMany relation.');
359
        }
360
361
        $forms = [];
362
363
        /*
364
         * If redirect from `exception` or `validation error` page.
365
         *
366
         * Then get form data from session flash.
367
         *
368
         * Else get data from database.
369
         */
370
        if ($values = old($this->column)) {
0 ignored issues
show
Bug introduced by
It seems like $this->column can also be of type array; however, old() does only seem to accept string|null, 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...
371
            foreach ($values as $key => $data) {
372
                if ($data[NestedForm::REMOVE_FLAG_NAME] == 1) {
373
                    continue;
374
                }
375
376
                $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $key)
0 ignored issues
show
Bug introduced by
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...
377
                    ->fill($data);
378
            }
379
        } else {
380
            foreach ($this->value as $data) {
381
                $key = array_get($data, $relation->getRelated()->getKeyName());
382
383
                $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $key)
0 ignored issues
show
Bug introduced by
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...
384
                    ->fill($data);
385
            }
386
        }
387
388
        return $forms;
389
    }
390
391
    /**
392
     * Setup script for this field in different view mode.
393
     *
394
     * @param string $script
395
     *
396
     * @return void
397
     */
398
    protected function setupScript($script)
399
    {
400
        $method = 'setupScriptFor'.ucfirst($this->viewMode).'View';
401
402
        call_user_func([$this, $method], $script);
403
    }
404
405
    /**
406
     * Setup default template script.
407
     *
408
     * @param string $templateScript
409
     *
410
     * @return void
411
     */
412 View Code Duplication
    protected function setupScriptForDefaultView($templateScript)
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...
413
    {
414
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
415
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
416
417
        /**
418
         * When add a new sub form, replace all element key in new sub form.
419
         *
420
         * @example comments[new___key__][title]  => comments[new_{index}][title]
421
         *
422
         * {count} is increment number of current sub form count.
423
         */
424
        $script = <<<EOT
425
var index = 0;
426
$('#has-many-{$this->column}').on('click', '.add', function () {
427
428
    var tpl = $('template.{$this->column}-tpl');
429
430
    index++;
431
432
    var template = tpl.html().replace(/{$defaultKey}/g, index);
433
    $('.has-many-{$this->column}-forms').append(template);
434
    {$templateScript}
435
});
436
437
$('#has-many-{$this->column}').on('click', '.remove', function () {
438
    $(this).closest('.has-many-{$this->column}-form').hide();
439
    $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
440
});
441
442
EOT;
443
444
        Admin::script($script);
445
    }
446
447
    /**
448
     * Setup tab template script.
449
     *
450
     * @param string $templateScript
451
     *
452
     * @return void
453
     */
454 View Code Duplication
    protected function setupScriptForTabView($templateScript)
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...
455
    {
456
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
457
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
458
459
        $script = <<<EOT
460
461
$('#has-many-{$this->column} > .nav').off('click', 'i.close-tab').on('click', 'i.close-tab', function(){
462
    var \$navTab = $(this).siblings('a');
463
    var \$pane = $(\$navTab.attr('href'));
464
    if( \$pane.hasClass('new') ){
465
        \$pane.remove();
466
    }else{
467
        \$pane.removeClass('active').find('.$removeClass').val(1);
468
    }
469
    if(\$navTab.closest('li').hasClass('active')){
470
        \$navTab.closest('li').remove();
471
        $('#has-many-{$this->column} > .nav > li:nth-child(1) > a').tab('show');
472
    }else{
473
        \$navTab.closest('li').remove();
474
    }
475
});
476
477
var index = 0;
478
$('#has-many-{$this->column} > .header').off('click', '.add').on('click', '.add', function(){
479
    index++;
480
    var navTabHtml = $('#has-many-{$this->column} > template.nav-tab-tpl').html().replace(/{$defaultKey}/g, index);
481
    var paneHtml = $('#has-many-{$this->column} > template.pane-tpl').html().replace(/{$defaultKey}/g, index);
482
    $('#has-many-{$this->column} > .nav').append(navTabHtml);
483
    $('#has-many-{$this->column} > .tab-content').append(paneHtml);
484
    $('#has-many-{$this->column} > .nav > li:last-child a').tab('show');
485
    {$templateScript}
486
});
487
488
if ($('.has-error').length) {
489
    $('.has-error').parent('.tab-pane').each(function () {
490
        var tabId = '#'+$(this).attr('id');
491
        $('li a[href="'+tabId+'"] i').removeClass('hide');
492
    });
493
    
494
    var first = $('.has-error:first').parent().attr('id');
495
    $('li a[href="#'+first+'"]').tab('show');
496
}
497
EOT;
498
499
        Admin::script($script);
500
    }
501
502
    /**
503
     * Disable create button.
504
     *
505
     * @return $this
506
     */
507
    public function disableCreate()
508
    {
509
        $this->options['allowCreate'] = false;
510
511
        return $this;
512
    }
513
514
    /**
515
     * Disable delete button.
516
     *
517
     * @return $this
518
     */
519
    public function disableDelete()
520
    {
521
        $this->options['allowDelete'] = false;
522
523
        return $this;
524
    }
525
526
    /**
527
     * Render the `HasMany` field.
528
     *
529
     * @throws \Exception
530
     *
531
     * @return \Illuminate\View\View
532
     */
533
    public function render()
534
    {
535
        // specify a view to render.
536
        $this->view = $this->views[$this->viewMode];
537
538
        list($template, $script) = $this->buildNestedForm($this->column, $this->builder)
0 ignored issues
show
Bug introduced by
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...
539
            ->getTemplateHtmlAndScript();
540
541
        $this->setupScript($script);
542
543
        return parent::render()->with([
0 ignored issues
show
Bug introduced by
The method with does only exist in Illuminate\View\View, but not in Illuminate\Contracts\View\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
544
            'forms'        => $this->buildRelatedForms(),
545
            'template'     => $template,
546
            'relationName' => $this->relationName,
547
            'options'      => $this->options,
548
        ]);
549
    }
550
}
551