Completed
Pull Request — master (#2836)
by
unknown
02:28
created

HasMany::disableDelete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Encore\Admin\Form\Field;
3
4
use Encore\Admin\Admin;
5
use Encore\Admin\Form;
6
use Encore\Admin\Form\Field;
7
use Encore\Admin\Form\NestedForm;
8
use Illuminate\Database\Eloquent\Relations\HasMany as Relation;
9
use Illuminate\Database\Eloquent\Relations\MorphMany;
10
use Illuminate\Support\Facades\Validator;
11
use Illuminate\Support\Str;
12
13
/**
14
 * Class HasMany.
15
 */
16
class HasMany extends Field
17
{
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, $this->formatValidationAttribute($input, $field->label(), $column)
130
            );
131
        }
132
133
        array_forget($rules, NestedForm::REMOVE_FLAG_NAME);
134
135
        if (empty($rules)) {
136
            return false;
137
        }
138
139
        $newRules = [];
140
141
        foreach ($rules as $column => $rule) {
142
            foreach (array_keys($input[$this->column]) as $key) {
143
                $newRules["{$this->column}.$key.$column"] = $rule;
144
            }
145
        }
146
147
        return Validator::make($input, $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...
148
    }
149
150
    /**
151
     * Format validation attributes.
152
     *
153
     * @param array  $input
154
     * @param string $label
155
     * @param string $column
156
     *
157
     * @return array
158
     */
159 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...
160
    {
161
        $new = $attributes = [];
162
163
        if (is_array($column)) {
164
            foreach ($column as $index => $col) {
165
                $new[$col . $index] = $col;
166
            }
167
        }
168
169
        foreach (array_keys(array_dot($input)) as $key) {
170
            if (is_string($column)) {
171
                if (Str::endsWith($key, ".$column")) {
172
                    $attributes[$key] = $label;
173
                }
174
            } else {
175
                foreach ($new as $k => $val) {
176
                    if (Str::endsWith($key, ".$k")) {
177
                        $attributes[$key] = $label . "[$val]";
178
                    }
179
                }
180
            }
181
        }
182
183
        return $attributes;
184
    }
185
186
    /**
187
     * Reset input key for validation.
188
     *
189
     * @param array $input
190
     * @param array $column $column is the column name array set
191
     *
192
     * @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...
193
     */
194
    protected function resetInputKey(array &$input, array $column)
195
    {
196
        /**
197
         * flip the column name array set.
198
         *
199
         * for example, for the DateRange, the column like as below
200
         *
201
         * ["start" => "created_at", "end" => "updated_at"]
202
         *
203
         * to:
204
         *
205
         * [ "created_at" => "start", "updated_at" => "end" ]
206
         */
207
        $column = array_flip($column);
208
209
        /**
210
         * $this->column is the inputs array's node name, default is the relation name.
211
         *
212
         * So... $input[$this->column] is the data of this column's inputs data
213
         *
214
         * in the HasMany relation, has many data/field set, $set is field set in the below
215
         */
216
        foreach ($input[$this->column] as $index => $set) {
217
218
            /*
219
             * foreach the field set to find the corresponding $column
220
             */
221 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...
222
                /*
223
                 * if doesn't have column name, continue to the next loop
224
                 */
225
                if (!array_key_exists($name, $column)) {
226
                    continue;
227
                }
228
229
                /**
230
                 * example:  $newKey = created_atstart.
231
                 *
232
                 * Σ( ° △ °|||)︴
233
                 *
234
                 * I don't know why a form need range input? Only can imagine is for range search....
235
                 */
236
                $newKey = $name . $column[$name];
237
238
                /*
239
                 * set new key
240
                 */
241
                array_set($input, "{$this->column}.$index.$newKey", $value);
242
                /*
243
                 * forget the old key and value
244
                 */
245
                array_forget($input, "{$this->column}.$index.$name");
246
            }
247
        }
248
    }
249
250
    /**
251
     * Prepare input data for insert or update.
252
     *
253
     * @param array $input
254
     *
255
     * @return array
256
     */
257
    public function prepare($input)
258
    {
259
        $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...
260
261
        return $form->setOriginal($this->original, $this->getKeyName())->prepare($input);
262
    }
263
264
    /**
265
     * Build a Nested form.
266
     *
267
     * @param string   $column
268
     * @param \Closure $builder
269
     * @param null     $key
270
     *
271
     * @return NestedForm
272
     */
273
    protected function buildNestedForm($column, \Closure $builder, $key = null)
274
    {
275
        $form = new Form\NestedForm($column, $key);
276
277
        $form->setForm($this->form);
278
279
        call_user_func($builder, $form);
280
281
        $form->hidden($this->getKeyName());
282
283
        $form->hidden(NestedForm::REMOVE_FLAG_NAME)->default(0)->addElementClass(NestedForm::REMOVE_FLAG_CLASS);
284
285
        return $form;
286
    }
287
288
    /**
289
     * Get the HasMany relation key name.
290
     *
291
     * @return string
292
     */
293
    protected function getKeyName()
294
    {
295
        if (is_null($this->form)) {
296
            return;
297
        }
298
299
        return $this->form->model()->{$this->relationName}()->getRelated()->getKeyName();
300
    }
301
302
    /**
303
     * Set view mode.
304
     *
305
     * @param string $mode currently support `tab` mode.
306
     *
307
     * @return $this
308
     *
309
     * @author Edwin Hui
310
     */
311
    public function mode($mode)
312
    {
313
        $this->viewMode = $mode;
314
315
        return $this;
316
    }
317
318
    /**
319
     * Use tab mode to showing hasmany field.
320
     *
321
     * @return HasMany
322
     */
323
    public function useTab()
324
    {
325
        return $this->mode('tab');
326
    }
327
328
    /**
329
     * Build Nested form for related data.
330
     *
331
     * @throws \Exception
332
     *
333
     * @return array
334
     */
335
    protected function buildRelatedForms()
336
    {
337
        if (is_null($this->form)) {
338
            return [];
339
        }
340
341
        $model = $this->form->model();
342
343
        $relation = call_user_func([$model, $this->relationName]);
344
345
        if (!$relation instanceof Relation && !$relation instanceof MorphMany) {
346
            throw new \Exception('hasMany field must be a HasMany or MorphMany relation.');
347
        }
348
349
        $forms = [];
350
351
        /*
352
         * If redirect from `exception` or `validation error` page.
353
         *
354
         * Then get form data from session flash.
355
         *
356
         * Else get data from database.
357
         */
358
        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...
359
            foreach ($values as $key => $data) {
360
                if ($data[NestedForm::REMOVE_FLAG_NAME] == 1) {
361
                    continue;
362
                }
363
364
                $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...
365
                    ->fill($data);
366
            }
367
        } else {
368
            foreach ($this->value as $data) {
369
                $key = array_get($data, $relation->getRelated()->getKeyName());
370
371
                $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...
372
                    ->fill($data);
373
            }
374
        }
375
376
        return $forms;
377
    }
378
379
    /**
380
     * Setup script for this field in different view mode.
381
     *
382
     * @param string $script
383
     *
384
     * @return void
385
     */
386
    protected function setupScript($script)
387
    {
388
        $method = 'setupScriptFor' . ucfirst($this->viewMode) . 'View';
389
390
        call_user_func([$this, $method], $script);
391
    }
392
393
    /**
394
     * Setup default template script.
395
     *
396
     * @param string $templateScript
397
     *
398
     * @return void
399
     */
400 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...
401
    {
402
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
403
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
404
405
        /**
406
         * When add a new sub form, replace all element key in new sub form.
407
         *
408
         * @example comments[new___key__][title]  => comments[new_{index}][title]
409
         *
410
         * {count} is increment number of current sub form count.
411
         */
412
        $script = <<<EOT
413
var index = 0;
414
$('#has-many-{$this->column}').on('click', '.add', function () {
415
416
    var tpl = $('template.{$this->column}-tpl');
417
418
    index++;
419
420
    var template = tpl.html().replace(/{$defaultKey}/g, index);
421
    $('.has-many-{$this->column}-forms').append(template);
422
    {$templateScript}
423
});
424
425
$('#has-many-{$this->column}').on('click', '.remove', function () {
426
    $(this).closest('.has-many-{$this->column}-form').hide();
427
    $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
428
});
429
430
EOT;
431
432
        Admin::script($script);
433
    }
434
435
    /**
436
     * Setup tab template script.
437
     *
438
     * @param string $templateScript
439
     *
440
     * @return void
441
     */
442 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...
443
    {
444
        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
445
        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
446
447
        $script = <<<EOT
448
449
$('#has-many-{$this->column} > .nav').off('click', 'i.close-tab').on('click', 'i.close-tab', function(){
450
    var \$navTab = $(this).siblings('a');
451
    var \$pane = $(\$navTab.attr('href'));
452
    if( \$pane.hasClass('new') ){
453
        \$pane.remove();
454
    }else{
455
        \$pane.removeClass('active').find('.$removeClass').val(1);
456
    }
457
    if(\$navTab.closest('li').hasClass('active')){
458
        \$navTab.closest('li').remove();
459
        $('#has-many-{$this->column} > .nav > li:nth-child(1) > a').tab('show');
460
    }else{
461
        \$navTab.closest('li').remove();
462
    }
463
});
464
465
var index = 0;
466
$('#has-many-{$this->column} > .header').off('click', '.add').on('click', '.add', function(){
467
    index++;
468
    var navTabHtml = $('#has-many-{$this->column} > template.nav-tab-tpl').html().replace(/{$defaultKey}/g, index);
469
    var paneHtml = $('#has-many-{$this->column} > template.pane-tpl').html().replace(/{$defaultKey}/g, index);
470
    $('#has-many-{$this->column} > .nav').append(navTabHtml);
471
    $('#has-many-{$this->column} > .tab-content').append(paneHtml);
472
    $('#has-many-{$this->column} > .nav > li:last-child a').tab('show');
473
    {$templateScript}
474
});
475
476
if ($('.has-error').length) {
477
    $('.has-error').parent('.tab-pane').each(function () {
478
        var tabId = '#'+$(this).attr('id');
479
        $('li a[href="'+tabId+'"] i').removeClass('hide');
480
    });
481
    
482
    var first = $('.has-error:first').parent().attr('id');
483
    $('li a[href="#'+first+'"]').tab('show');
484
}
485
EOT;
486
487
        Admin::script($script);
488
    }
489
490
    public function disableCreate()
491
    {
492
        $this->options['allowCreate'] = false;
493
        return $this;
494
    }
495
496
    public function disableDelete()
497
    {
498
        $this->options['allowDelete'] = false;
499
        return $this;
500
    }
501
502
    /**
503
     * Render the `HasMany` field.
504
     *
505
     * @throws \Exception
506
     *
507
     * @return \Illuminate\View\View
508
     */
509
    public function render()
510
    {
511
        // specify a view to render.
512
        $this->view = $this->views[$this->viewMode];
513
514
        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...
515
            ->getTemplateHtmlAndScript();
516
517
        $this->setupScript($script);
518
519
        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...
520
                'forms' => $this->buildRelatedForms(),
521
                'template' => $template,
522
                'relationName' => $this->relationName,
523
                'options' => $this->options,
524
        ]);
525
    }
526
}
527