HasMany.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <?php
  2. namespace Dcat\Admin\Form\Field;
  3. use Dcat\Admin\Form;
  4. use Dcat\Admin\Form\Field;
  5. use Dcat\Admin\Form\NestedForm;
  6. use Dcat\Admin\Support\Helper;
  7. use Illuminate\Support\Arr;
  8. use Illuminate\Support\Facades\Validator;
  9. use Illuminate\Support\Str;
  10. /**
  11. * Class HasMany.
  12. */
  13. class HasMany extends Field
  14. {
  15. use Form\ResolveField;
  16. /**
  17. * Relation name.
  18. *
  19. * @var string
  20. */
  21. protected $relationName;
  22. /**
  23. * @var string
  24. */
  25. protected $parentRelationName;
  26. /**
  27. * @var string|int
  28. */
  29. protected $parentKey;
  30. /**
  31. * Relation key name.
  32. *
  33. * @var string
  34. */
  35. protected $relationKeyName = 'id';
  36. /**
  37. * Form builder.
  38. *
  39. * @var \Closure
  40. */
  41. protected $builder = null;
  42. /**
  43. * Form data.
  44. *
  45. * @var array
  46. */
  47. protected $value = [];
  48. /**
  49. * View Mode.
  50. *
  51. * Supports `default` and `tab` currently.
  52. *
  53. * @var string
  54. */
  55. protected $viewMode = 'default';
  56. /**
  57. * Available views for HasMany field.
  58. *
  59. * @var array
  60. */
  61. protected $views = [
  62. 'default' => 'admin::form.hasmany',
  63. 'tab' => 'admin::form.hasmanytab',
  64. 'table' => 'admin::form.hasmanytable',
  65. ];
  66. /**
  67. * Options for template.
  68. *
  69. * @var array
  70. */
  71. protected $options = [
  72. 'allowCreate' => true,
  73. 'allowDelete' => true,
  74. ];
  75. protected $columnClass;
  76. /**
  77. * Create a new HasMany field instance.
  78. *
  79. * @param $relationName
  80. * @param array $arguments
  81. */
  82. public function __construct($relationName, $arguments = [])
  83. {
  84. $this->relationName = $relationName;
  85. $this->column = $relationName;
  86. $this->columnClass = $this->formatClass($relationName);
  87. if (count($arguments) == 1) {
  88. $this->label = $this->formatLabel();
  89. $this->builder = $arguments[0];
  90. }
  91. if (count($arguments) == 2) {
  92. [$this->label, $this->builder] = $arguments;
  93. }
  94. }
  95. protected function formatClass(string $column)
  96. {
  97. return str_replace('.', '-', $column);
  98. }
  99. /**
  100. * Get validator for this field.
  101. *
  102. * @param array $input
  103. * @return bool|Validator
  104. */
  105. public function getValidator(array $input)
  106. {
  107. if (! Arr::has($input, $this->column)) {
  108. return false;
  109. }
  110. $form = $this->buildNestedForm();
  111. $rules = $attributes = $messages = [];
  112. /* @var Field $field */
  113. foreach ($form->fields() as $field) {
  114. if (! $fieldRules = $field->getRules()) {
  115. continue;
  116. }
  117. File::deleteRules($field, $fieldRules);
  118. $column = $field->column();
  119. $this->normalizeValidatorInput($field, $input);
  120. if (is_array($column)) {
  121. foreach ($column as $key => $name) {
  122. $rules[$name.$key] = $fieldRules;
  123. }
  124. $this->resetInputKey($input, $column);
  125. } else {
  126. $rules[$column] = $fieldRules;
  127. }
  128. $attributes = array_merge(
  129. $attributes,
  130. $this->formatValidationAttribute($input, $field->label(), $column)
  131. );
  132. $messages = array_merge(
  133. $messages,
  134. $this->formatValidationMessages($input, $field->getValidationMessages())
  135. );
  136. }
  137. Arr::forget($rules, NestedForm::REMOVE_FLAG_NAME);
  138. if (empty($rules)) {
  139. return false;
  140. }
  141. $newRules = [];
  142. $newInput = [];
  143. foreach ($rules as $column => $rule) {
  144. foreach (array_keys(Arr::get($input, $this->column)) as $key) {
  145. if (Arr::get($input, "{$this->column}.{$key}.".NestedForm::REMOVE_FLAG_NAME)) {
  146. continue;
  147. }
  148. $subKey = "{$this->column}.{$key}.{$column}";
  149. $newRules[$subKey] = $rule;
  150. $newInput[$subKey] = $ruleValue = Arr::get($input, "{$this->column}.$key.$column");
  151. if (is_array($ruleValue)) {
  152. foreach ($ruleValue as $vkey => $value) {
  153. $newInput["{$subKey}.{$vkey}"] = $value;
  154. }
  155. }
  156. }
  157. }
  158. $newInput += $input;
  159. if ($hasManyRules = $this->getRules()) {
  160. if (! Arr::has($input, $this->column)) {
  161. return false;
  162. }
  163. $newInput += $this->sanitizeInput($input, $this->column);
  164. $newRules[$this->column] = $hasManyRules;
  165. $attributes[$this->column] = $this->label;
  166. }
  167. return Validator::make($newInput, $newRules, array_merge($this->getValidationMessages(), $messages), $attributes);
  168. }
  169. protected function normalizeValidatorInput(Field $field, array &$input)
  170. {
  171. if (
  172. $field instanceof MultipleSelect
  173. || $field instanceof Checkbox
  174. || $field instanceof Tags
  175. ) {
  176. foreach (array_keys(Arr::get($input, $this->column)) as $key) {
  177. $value = $input[$this->column][$key][$field->column()];
  178. $input[$this->column][$key][$field->column()] = array_filter($value, function ($v) {
  179. return $v !== null;
  180. });
  181. }
  182. }
  183. }
  184. /**
  185. * Format validation messages.
  186. *
  187. * @param array $input
  188. * @param array $messages
  189. * @return array
  190. */
  191. protected function formatValidationMessages(array $input, array $messages)
  192. {
  193. $result = [];
  194. foreach (Arr::get($input, $this->column) as $key => &$value) {
  195. $newKey = $this->column.'.'.$key;
  196. foreach ($messages as $k => $message) {
  197. $result[$newKey.'.'.$k] = $message;
  198. }
  199. }
  200. return $result;
  201. }
  202. /**
  203. * Format validation attributes.
  204. *
  205. * @param array $input
  206. * @param string $label
  207. * @param string $column
  208. * @return array
  209. */
  210. protected function formatValidationAttribute($input, $label, $column)
  211. {
  212. $new = $attributes = [];
  213. if (is_array($column)) {
  214. foreach ($column as $index => $col) {
  215. $new[$col.$index] = $col;
  216. }
  217. }
  218. foreach (array_keys(Arr::dot($input)) as $key) {
  219. if (is_string($column)) {
  220. if (Str::endsWith($key, ".$column")) {
  221. $attributes[$key] = $label;
  222. }
  223. } else {
  224. foreach ($new as $k => $val) {
  225. if (Str::endsWith($key, ".$k")) {
  226. $attributes[$key] = $label."[$val]";
  227. }
  228. }
  229. }
  230. }
  231. return $attributes;
  232. }
  233. /**
  234. * Reset input key for validation.
  235. *
  236. * @param array $input
  237. * @param array $column $column is the column name array set
  238. * @return void.
  239. */
  240. protected function resetInputKey(array &$input, array $column)
  241. {
  242. /**
  243. * flip the column name array set.
  244. *
  245. * for example, for the DateRange, the column like as below
  246. *
  247. * ["start" => "created_at", "end" => "updated_at"]
  248. *
  249. * to:
  250. *
  251. * [ "created_at" => "start", "updated_at" => "end" ]
  252. */
  253. $column = array_flip($column);
  254. /**
  255. * $this->column is the inputs array's node name, default is the relation name.
  256. *
  257. * So... $input[$this->column] is the data of this column's inputs data
  258. *
  259. * in the HasMany relation, has many data/field set, $set is field set in the below
  260. */
  261. foreach (Arr::get($input, $this->column) as $index => $set) {
  262. /*
  263. * foreach the field set to find the corresponding $column
  264. */
  265. foreach ($set as $name => $value) {
  266. /*
  267. * if doesn't have column name, continue to the next loop
  268. */
  269. if (! array_key_exists($name, $column)) {
  270. continue;
  271. }
  272. /**
  273. * example: $newKey = created_atstart.
  274. *
  275. * Σ( ° △ °|||)︴
  276. *
  277. * I don't know why a form need range input? Only can imagine is for range search....
  278. */
  279. $newKey = $name.$column[$name];
  280. /*
  281. * set new key
  282. */
  283. Arr::set($input, "{$this->column}.$index.$newKey", $value);
  284. /*
  285. * forget the old key and value
  286. */
  287. Arr::forget($input, "{$this->column}.$index.$name");
  288. }
  289. }
  290. }
  291. /**
  292. * Prepare input data for insert or update.
  293. *
  294. * @param array $input
  295. * @return array
  296. */
  297. protected function prepareInputValue($input)
  298. {
  299. $form = $this->buildNestedForm();
  300. return array_values(
  301. $form->setOriginal($this->original, $this->getKeyName())->prepare($input)
  302. );
  303. }
  304. public function setParentRelationName($name, $parentKey)
  305. {
  306. $this->parentRelationName = $name;
  307. $this->parentKey = $parentKey;
  308. return $this;
  309. }
  310. public function getNestedFormColumnName()
  311. {
  312. if ($this->parentRelationName) {
  313. $key = $this->parentKey ?? (NestedForm::DEFAULT_KEY_PREFIX.NestedForm::DEFAULT_PARENT_KEY_NAME);
  314. return $this->parentRelationName.'.'.$key.'.'.$this->column;
  315. }
  316. return $this->column;
  317. }
  318. protected function getNestedFormDefaultKeyName()
  319. {
  320. if ($this->parentRelationName) {
  321. // hasmany嵌套table时,需要重新设置行的默认key
  322. return $this->parentRelationName.'_NKEY_';
  323. }
  324. }
  325. protected function setNestedFormDefaultKey(Form\NestedForm $form)
  326. {
  327. if ($this->parentRelationName) {
  328. // hasmany嵌套table时需要特殊处理
  329. $form->setDefaultKey(Form\NestedForm::DEFAULT_KEY_PREFIX.$this->getNestedFormDefaultKeyName());
  330. }
  331. }
  332. /**
  333. * Build a Nested form.
  334. *
  335. * @param null $key
  336. * @return NestedForm
  337. */
  338. public function buildNestedForm($key = null)
  339. {
  340. $form = new Form\NestedForm($this->getNestedFormColumnName(), $key);
  341. $this->setNestedFormDefaultKey($form);
  342. $form->setForm($this->form);
  343. $form->setResolvingFieldCallbacks($this->resolvingFieldCallbacks);
  344. call_user_func($this->builder, $form);
  345. $form->hidden($this->getKeyName());
  346. $form->hidden(NestedForm::REMOVE_FLAG_NAME)->default(0)->addElementClass(NestedForm::REMOVE_FLAG_CLASS);
  347. return $form;
  348. }
  349. /**
  350. * Get the HasMany relation key name.
  351. *
  352. * @return string
  353. */
  354. public function getKeyName()
  355. {
  356. if (is_null($this->form)) {
  357. return;
  358. }
  359. return $this->relationKeyName;
  360. }
  361. /**
  362. * @param string $relationKeyName
  363. */
  364. public function setRelationKeyName(?string $relationKeyName)
  365. {
  366. $this->relationKeyName = $relationKeyName;
  367. return $this;
  368. }
  369. /**
  370. * Set view mode.
  371. *
  372. * @param string $mode currently support `tab` mode.
  373. * @return $this
  374. *
  375. * @author Edwin Hui
  376. */
  377. public function mode($mode)
  378. {
  379. $this->viewMode = $mode;
  380. return $this;
  381. }
  382. /**
  383. * Use tab mode to showing hasmany field.
  384. *
  385. * @return HasMany
  386. */
  387. public function useTab()
  388. {
  389. return $this->mode('tab');
  390. }
  391. /**
  392. * Use table mode to showing hasmany field.
  393. *
  394. * @return HasMany
  395. */
  396. public function useTable()
  397. {
  398. return $this->mode('table');
  399. }
  400. /**
  401. * Build Nested form for related data.
  402. *
  403. * @return array
  404. *
  405. * @throws \Exception
  406. */
  407. protected function buildRelatedForms()
  408. {
  409. if (is_null($this->form)) {
  410. return [];
  411. }
  412. $forms = [];
  413. /*
  414. * If redirect from `exception` or `validation error` page.
  415. *
  416. * Then get form data from session flash.
  417. *
  418. * Else get data from database.
  419. */
  420. foreach (Helper::array($this->value()) as $idx => $data) {
  421. $key = Arr::get($data, $this->getKeyName(), $idx);
  422. $forms[$key] = $this->buildNestedForm($key)
  423. ->fill($data);
  424. }
  425. return $forms;
  426. }
  427. /**
  428. * Disable create button.
  429. *
  430. * @return $this
  431. */
  432. public function disableCreate()
  433. {
  434. $this->options['allowCreate'] = false;
  435. return $this;
  436. }
  437. /**
  438. * Disable delete button.
  439. *
  440. * @return $this
  441. */
  442. public function disableDelete()
  443. {
  444. $this->options['allowDelete'] = false;
  445. return $this;
  446. }
  447. public function value($value = null)
  448. {
  449. if ($value === null) {
  450. return Helper::array(parent::value($value));
  451. }
  452. return parent::value($value);
  453. }
  454. /**
  455. * Render the `HasMany` field.
  456. *
  457. * @return \Illuminate\View\View|string
  458. *
  459. * @throws \Exception
  460. */
  461. public function render()
  462. {
  463. if (! $this->shouldRender()) {
  464. return '';
  465. }
  466. if ($this->viewMode == 'table') {
  467. return $this->renderTable();
  468. }
  469. // specify a view to render.
  470. $this->view = $this->view ?: $this->views[$this->viewMode];
  471. $this->addVariables([
  472. 'forms' => $this->buildRelatedForms(),
  473. 'template' => $this->buildNestedForm()->render(),
  474. 'relationName' => $this->relationName,
  475. 'options' => $this->options,
  476. 'count' => count($this->value()),
  477. 'columnClass' => $this->columnClass,
  478. ]);
  479. return parent::render();
  480. }
  481. /**
  482. * Render the `HasMany` field for table style.
  483. *
  484. * @return mixed
  485. *
  486. * @throws \Exception
  487. */
  488. protected function renderTable()
  489. {
  490. $headers = [];
  491. $fields = [];
  492. $hidden = [];
  493. /* @var Field $field */
  494. foreach ($this->buildNestedForm()->fields() as $field) {
  495. if (is_a($field, Hidden::class)) {
  496. $hidden[] = $field->render();
  497. } else {
  498. /* Hide label and set field width 100% */
  499. $field->setLabelClass(['hidden']);
  500. $field->width(12, 0);
  501. $fields[] = $field->render();
  502. $headers[] = $field->label();
  503. }
  504. }
  505. /* Build row elements */
  506. $template = array_reduce($fields, function ($all, $field) {
  507. $all .= "<td>{$field}</td>";
  508. return $all;
  509. }, '');
  510. /* Build cell with hidden elements */
  511. $template .= '<td class="hidden">'.implode('', $hidden).'</td>';
  512. // specify a view to render.
  513. $this->view = $this->view ?: $this->views[$this->viewMode];
  514. $this->addVariables([
  515. 'headers' => $headers,
  516. 'forms' => $this->buildRelatedForms(),
  517. 'template' => $template,
  518. 'relationName' => $this->relationName,
  519. 'options' => $this->options,
  520. 'count' => count($this->value()),
  521. 'columnClass' => $this->columnClass,
  522. 'parentKey' => $this->getNestedFormDefaultKeyName(),
  523. ]);
  524. return parent::render();
  525. }
  526. }