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) { |
||
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
|
|||
657 | 'headers' => $headers, |
||
658 | 'forms' => $this->buildRelatedForms(), |
||
659 | 'template' => $template, |
||
660 | 'relationName' => $this->relationName, |
||
661 | 'options' => $this->options, |
||
662 | ]); |
||
663 | } |
||
664 | } |
||
665 |
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:
Available Fixes
Add an additional type-check:
Only allow a single type to be passed if the variable comes from a parameter: