SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/JsonForm.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Exception;
  17. use InvalidArgumentException;
  18.  
  19.  
  20. /**
  21.  * Processes forms using configuration stored in a JSON file which specifies database columns, their HTML input fields,
  22.  * and validation rules.
  23.  * A generic implementation which should work for most cases is found in {@see ManagedAdminController::_getEditForm}
  24.  * (generates the form) and {@see Controller::saveJsonData} (saves the POST submission)
  25.  */
  26. class JsonForm extends Form
  27. {
  28.  
  29. /**
  30.   * Loads multiedit data for use on a view
  31.   * @param array $conf The form config, pulled from JSON
  32.   * @param string $default_link The default linking column name
  33.   * @param int $record_id The base record ID
  34.   * @param array $conditions Conditions to get records in the multiedit table which relate to the base record;
  35.   * see {@see Pdb::buildClause}. A clause of 'link_column' => $record_id will be appended to any provided
  36.   * conditions.
  37.   * @return array The View will be modified
  38.   */
  39. public static function loadMultiEditData(array $conf, $default_link, $record_id, array $conditions)
  40. {
  41. $data = [];
  42. foreach ($conf as $tab => $tab_content) {
  43. if (!is_array($tab_content)) continue;
  44.  
  45. foreach ($tab_content as $item) {
  46. if (!isset($item['multiedit'])) continue;
  47.  
  48. $multed = $item['multiedit'];
  49. $id = 'multiedit_' . $multed['id'];
  50. $table = $multed['table'];
  51.  
  52. Pdb::validateIdentifier($table);
  53.  
  54. $values = [];
  55. $table_conditions = [];
  56. $link_column = !empty($multed['link']) ? $multed['link'] : $default_link;
  57.  
  58. if (isset($multed['where'])) $table_conditions = $multed['where'];
  59. $table_conditions[] = [$link_column, '=', $record_id];
  60.  
  61. $table_conditions = array_merge($table_conditions, $conditions);
  62. $q = "SELECT * FROM ~{$table} WHERE " . Pdb::buildClause($table_conditions, $values);
  63.  
  64. if ($multed['reorder']) {
  65. $q .= ' ORDER BY record_order';
  66. }
  67.  
  68. $data[$id] = Pdb::q($q, $values, 'arr');
  69. }
  70. }
  71. return $data;
  72. }
  73.  
  74.  
  75. /**
  76.   * Determine the auto-generated default values for an autofill-list
  77.   *
  78.   * Defaults:
  79.   * joiner_local_col Singular of local table name + '_id'
  80.   * joiner_foreign_col Singular of the foreign_table option + '_id'
  81.   * foreign_label_col 'name'
  82.   * reorder false
  83.   *
  84.   * @param array $auto Auto link opts, from a JSON form defintion
  85.   * @param string $local_table_name The 'local' table name (i.e. the table which is being edited)
  86.   * @return array
  87.   */
  88. public static function autofillOptionDefaults(array $auto, $local_table_name)
  89. {
  90. if (empty($auto['joiner_local_col'])) {
  91. $auto['joiner_local_col'] = Inflector::singular($local_table_name) . '_id';
  92. }
  93. if (empty($auto['joiner_local_col'])) {
  94. $auto['joiner_local_col'] = Inflector::singular($auto['foreign_table']) . '_id';
  95. }
  96. if (empty($auto['foreign_label_col'])) {
  97. $auto['foreign_label_col'] = 'name';
  98. }
  99. if (empty($auto['reorder'])) {
  100. $auto['reorder'] = false;
  101. }
  102. return $auto;
  103. }
  104.  
  105.  
  106. /**
  107.   * Loads autofill_list data for use on a view
  108.   * @param array $conf The form config, pulled from JSON
  109.   * @param string $local_table_name The 'local' table name (i.e. the table which is being edited)
  110.   * @param int $local_record_id The local record id
  111.   * @param array $conditions Conditions to get records in the joiner table which relate to the base record;
  112.   * see {@see Pdb::buildClause}. A clause of 'link_column' => $record_id will be appended to any provided
  113.   * conditions.
  114.   * @return array The View will be modified
  115.   */
  116. public static function loadAutofillListData(array $conf, $local_table_name, $local_record_id, array $conditions)
  117. {
  118. $data = [];
  119. foreach ($conf as $tab => $tab_content) {
  120. if (!is_array($tab_content)) continue;
  121.  
  122. foreach ($tab_content as $item) {
  123. if (!isset($item['autofill_list'])) continue;
  124.  
  125. $auto = $item['autofill_list'];
  126. $auto = self::autofillOptionDefaults($auto, $local_table_name);
  127.  
  128. // If specified use a foreign_label_sql parameter directly
  129. // If foreign_label_col is an array, CONCAT on space
  130. if (isset($auto['foreign_label_sql'])) {
  131. $label_sql = $auto['foreign_label_sql'];
  132. } elseif (is_array($auto['foreign_label_col'])) {
  133. foreach ($auto['foreign_label_col'] as $col) {
  134. Pdb::validateIdentifier($col);
  135. }
  136. $label_sql = 'CONCAT(item.' . implode(", ' ', item.", $auto['foreign_label_col']) . ')';
  137. } else {
  138. Pdb::validateIdentifier($auto['foreign_label_col']);
  139. $label_sql = 'item.' . $auto['foreign_label_col'];
  140. }
  141.  
  142. Pdb::validateIdentifier($auto['joiner_local_col']);
  143. Pdb::validateIdentifier($auto['joiner_foreign_col']);
  144. Pdb::validateIdentifier($auto['joiner_table']);
  145. Pdb::validateIdentifier($auto['foreign_table']);
  146.  
  147. // Need ID for saving, value for display, and orderkey for ordering
  148. $fields = [];
  149. $fields[] = 'item.id';
  150. $fields[] = "{$label_sql} AS value";
  151. if ($auto['reorder']) {
  152. $fields[] = 'joiner.record_order AS orderkey';
  153. }
  154. $fields = implode(', ', $fields);
  155.  
  156. if ($auto['reorder']) {
  157. $order = 'joiner.record_order';
  158. } else {
  159. $order = $label_sql;
  160. }
  161.  
  162. $q = "SELECT {$fields}
  163. FROM ~{$auto['joiner_table']} AS joiner
  164. INNER JOIN ~{$auto['foreign_table']} AS item ON item.id = joiner.{$auto['joiner_foreign_col']}
  165. WHERE joiner.{$auto['joiner_local_col']} = ?
  166. ORDER BY {$order}";
  167. $data[$auto['name']] = Pdb::query($q, [$local_record_id], 'arr');
  168. }
  169. }
  170. return $data;
  171. }
  172.  
  173.  
  174. /**
  175.   * Expands item definitions for a field pulled from JSON
  176.   * @param array &$field The field definition
  177.   * @param array $metadata Metadata for use in argument replacement
  178.   * @return void
  179.   */
  180. public static function expandItemDefns(array &$field, array $metadata = [])
  181. {
  182. if (!isset($field['attrs'])) $field['attrs'] = [];
  183. if (!isset($field['helptext'])) $field['helptext'] = '';
  184. if (!isset($field['required'])) $field['required'] = false;
  185. if (!isset($field['items'])) {
  186. $field['items'] = [];
  187. return;
  188. }
  189. $items = &$field['items'];
  190.  
  191. // Use a function to look up or generate items if specified
  192. if (isset($items['func']) and (count($items) == 1 or (count($items) == 2 and isset($items['args'])))) {
  193. if (strpos($items['func'], '::') !== false) {
  194. list($class, $func) = explode('::', $items['func']);
  195. $class = Sprout::nsClass($class, ['Sprout\Helpers']);
  196. $func = $class . '::' . $func;
  197. } else {
  198. $func = $items['func'];
  199. }
  200. $args = (isset($items['args']) ? $items['args'] : []);
  201. $args = self::argReplace($args, $metadata);
  202. $items = call_user_func_array($func, $args);
  203.  
  204. // Run a SQL query and return a Pdb map
  205. } else if (isset($items['query']) and (count($items) == 1 or (count($items) == 2 and isset($items['binds'])))) {
  206. $binds = isset($items['binds']) ? $items['binds'] : [];
  207. $items = Pdb::query($items['query'], $binds, 'map');
  208.  
  209. // Convert class vars
  210. } else if (isset($items['var']) and (count($items) == 1)) {
  211. list($class, $var) = explode('::', $items['var']);
  212. $class = Sprout::nsClass($class, ['Sprout\Helpers']);
  213. if (!class_exists($class)) {
  214. throw new Exception('Class lookup failed for var: ' . $items['var']);
  215. }
  216. $class_vars = get_class_vars($class);
  217.  
  218. // Chop leading $ to convert to array reference
  219. $var = substr($var, 1);
  220. $items = $class_vars[$var];
  221.  
  222. // Convert class constants
  223. } else if (isset($items['const']) and (count($items) == 1)) {
  224. list($class, $const) = explode('::', $items['const']);
  225. $class = Sprout::nsClass($class, ['Sprout\Helpers']);
  226. if (!class_exists($class)) {
  227. throw new Exception('Class lookup failed for var: ' . $items['var']);
  228. }
  229. $items = constant($class . '::' . $const);
  230.  
  231. // Convert constants
  232. } else {
  233. foreach ($items as $key => &$item) {
  234. if (!is_array($item)) continue;
  235. if (count($item) != 1) continue;
  236.  
  237. $item_fields = $item;
  238. if (isset($item_fields['const'])) {
  239. if (strpos($item_fields['const'], '::') === false) continue;
  240. list($class, $const) = explode('::', $item_fields['const']);
  241. $class = Sprout::nsClass($class, ['Sprout\Helpers']);
  242. if (defined($class. '::' . $const)) {
  243. $item = constant($class. '::' . $const);
  244. continue;
  245. }
  246. throw new Exception('Const lookup failed: ' . $item_fields['const']);
  247. }
  248. }
  249. }
  250. }
  251.  
  252.  
  253. /**
  254.   * Render a tab item, which may be a field, heading, html block, etc
  255.   *
  256.   * @param array $item The item definition
  257.   * @param string $for Either 'add', 'edit' or something custom; to check against the "for" parameter
  258.   * @param int $id Record ID; for pass-through to function calls
  259.   * @param int $data Data array; for pass-through to function calls
  260.   * @param int $errors Errors array; for pass-through to function calls
  261.   * @param string $name_prepend Prepended to the field name. Only applies for fields
  262.   * @return html
  263.   */
  264. public static function renderTabItem(array $item, $for, $id, array $data, array $errors, $name_prepend = '')
  265. {
  266. // Metadata which is passed into argReplace for display/validator argument replacement
  267. $metadata = [
  268. 'id' => $id,
  269. ];
  270.  
  271. if (isset($item['field'])) {
  272. // Field
  273. $field = $item['field'];
  274.  
  275. if (!isset($field['display']) or $field['display'] == null) return null;
  276. if (isset($field['for'])) {
  277. if (!in_array($for, $field['for'])) return null;
  278. }
  279.  
  280. if (!array_key_exists($field['name'], $data) and !empty($field['default'])) {
  281. Fb::setFieldValue($name_prepend . $field['name'], $field['default']);
  282.  
  283. // For fields like Fb::checkboxBoolList(string $name, array $attrs, array $settings),
  284. // $name_prepend doesn't actually get prepended. E.g. 'active' doesn't produce 'm_active' because
  285. // the <input> name is set using the $settings param, not $name like most other field types
  286. Fb::setFieldValue($field['name'], $field['default']);
  287. }
  288.  
  289. return JsonForm::renderField($field, $name_prepend, $metadata);
  290.  
  291.  
  292. } elseif (isset($item['heading'])) {
  293. // Heading
  294. return '<h3>' . Enc::html($item['heading']) . '</h3>';
  295.  
  296.  
  297. } elseif (isset($item['html'])) {
  298. // HTML text
  299. return $item['html'];
  300.  
  301.  
  302. } elseif (isset($item['group'])) {
  303. // Groups of similar items
  304. $group = $item['group'];
  305.  
  306. if (empty($group['wrap-class'])) $group['wrap-class'] = '';
  307. if (empty($group['item-class'])) $group['item-class'] = '';
  308.  
  309. $group['wrap-class'] = trim('field-group-wrap ' . $group['wrap-class']);
  310. $group['item-class'] = trim('field-group-item ' . $group['item-class']);
  311.  
  312. $out = '<div class="' . $group['wrap-class'] . '">';
  313. foreach ($group['items'] as $group_item) {
  314. $out .= '<div class="' . $group['item-class'] . '">';
  315. $out .= self::renderTabItem($group_item, $for, $id, $data, $errors, $name_prepend);
  316. $out .= '</div>';
  317. }
  318. $out .= '</div>';
  319. return $out;
  320.  
  321.  
  322. } elseif (isset($item['func'])) {
  323. // Call a custom function and return the result
  324. if (strpos($item['func'], '::') !== false) {
  325. list($class, $func) = explode('::', $item['func']);
  326. $class = Sprout::nsClass($class, ['Sprout\Helpers']);
  327. $func = $class . '::' . $func;
  328. } else {
  329. $func = $item['func'];
  330. }
  331.  
  332. $args = [$id, $data, $errors];
  333. if (isset($item['args'])) {
  334. $args = array_merge($args, $item['args']);
  335. }
  336. $args = self::argReplace($args, $metadata);
  337.  
  338. return call_user_func_array($func, $args);
  339.  
  340.  
  341. } elseif (isset($item['multiedit'])) {
  342. // Multiedit
  343. $multed = $item['multiedit'];
  344. if (!isset($data['multiedit_' . $multed['id']])) {
  345. $data['multiedit_' . $multed['id']] = [];
  346. }
  347. if (!isset($errors['multiedit_' . $multed['id']])) {
  348. $errors['multiedit_' . $multed['id']] = [];
  349. }
  350.  
  351. // Backup form data, then clobber it, to render using the multiedit's defaults
  352. $original_data = $data;
  353. $data = [];
  354. Fb::setData($data);
  355.  
  356. $out = '<script type="text/x-template" id="' . Enc::html('multiedit-' . $multed['id']) . '">';
  357. $out .= '<input type="hidden" name="m_id">';
  358.  
  359. foreach ($multed['items'] as $multi_item) {
  360. $out .= self::renderTabItem($multi_item, $for, $id, $data, $errors, 'm_');
  361. }
  362.  
  363. $out .= '</script>';
  364.  
  365. if (!empty($multed['post-add-js'])) {
  366. MultiEdit::setPostAddJavaScriptFunc($multed['post-add-js']);
  367. }
  368. if (!empty($multed['reorder'])) {
  369. MultiEdit::reorder();
  370. }
  371. MultiEdit::itemName($multed['single']);
  372.  
  373. // Restore original form data which was clobbered to render defaults
  374. Fb::setData($original_data);
  375. $data = $original_data;
  376.  
  377. MultiEdit::display(
  378. $multed['id'],
  379. $data['multiedit_' . $multed['id']],
  380. $errors['multiedit_' . $multed['id']]
  381. );
  382. $out .= ob_get_clean();
  383.  
  384. return $out;
  385.  
  386. } elseif (isset($item['autofill_list'])) {
  387. // The autofillList method receives the whole object in $options straight from the JSON
  388. $auto = $item['autofill_list'];
  389. return Form::autofillList($auto['name'], [], $auto);
  390.  
  391. } else {
  392. throw new InvalidArgumentException(
  393. "Unknown item type; expected key 'field', 'heading', 'html', 'func', 'multiedit', or 'autofill_list'"
  394. );
  395. }
  396. }
  397.  
  398.  
  399. /**
  400.   * Renders the input for a field definition pulled from a JSON file
  401.   * @param array $field The field definition
  402.   * @param string $name_prepend Prepended to the field name
  403.   * @param array $metadata Metadata for use in argument replacement
  404.   * @return string
  405.   */
  406. public static function renderField(array $field, $name_prepend = '', $metadata = [])
  407. {
  408. self::expandItemDefns($field, $metadata);
  409.  
  410. $func = $field['display'];
  411. if (strpos($func, '::') !== false) {
  412. list($class, $func) = explode('::', $func);
  413. $class = Sprout::nsClass($class, ['Sprout\Helpers']);
  414. $func = $class . '::' . $func;
  415. }
  416.  
  417. if (!is_callable($func)) {
  418. throw new InvalidArgumentException("Field display method '{$func}' does not exist");
  419. }
  420.  
  421. Form::nextFieldDetails($field['label'], $field['required'], $field['helptext']);
  422. return Form::fieldAuto($func, $name_prepend . $field['name'], $field['attrs'], $field['items']);
  423. }
  424.  
  425.  
  426. /**
  427.   * Set a parameter for fields to be a specific value, for one or more columns
  428.   *
  429.   * @param array $items Items array, may contain fields, groups, etc
  430.   * @param array $columns Columns to alter, as an array of strings (e.g. ['file','image'])
  431.   * @param string $key Key to set
  432.   * @param string $val Value to set the key to
  433.   * @return null Array $items is altered in-place
  434.   */
  435. public static function setParameterForColumns(array &$items, array $columns, $key, $val)
  436. {
  437. foreach ($items as &$item) {
  438. if (isset($item['field']) and in_array($item['field']['name'], $columns)) {
  439. $item['field'][$key] = $val;
  440. } else if (isset($item['group'])) {
  441. self::setParameterForColumns($item['group']['items'], $columns, $key, $val);
  442. }
  443. }
  444. }
  445.  
  446.  
  447. /**
  448.   * Extract field defns from a list (which may include groups)
  449.   *
  450.   * @param array $items Item defintions, e.g. from a tab
  451.   * @return array Field defintions only, in a flat list
  452.   **/
  453. public static function flattenGroups(array $items)
  454. {
  455. $field_defns = [];
  456.  
  457. foreach ($items as $item) {
  458. if (isset($item['field'])) {
  459. $field_defns[] = $item['field'];
  460. } else if (isset($item['group'])) {
  461. $field_defns = array_merge(
  462. $field_defns,
  463. self::flattenGroups($item['group']['items'])
  464. );
  465. }
  466. }
  467.  
  468. return $field_defns;
  469. }
  470.  
  471.  
  472. /**
  473.   * Collates POST data using specified config options
  474.   * @param array $conf Config, typically loaded by Controller::loadFormJson()
  475.   * @param string $mode Form mode, e.g. 'add', 'edit' or a custom value
  476.   * @param Validator $validator To validate the data; must be created externally so it can be used for other
  477.   * validation before and/or after collating the JsonForm data
  478.   * @param int $item_id The record being edited or 0 for record adding
  479.   * @return array [0] Data for insert/update, field => value [1] Errors generated, field => error
  480.   */
  481. public static function collateData($conf, $mode, Validator $validator, $item_id)
  482. {
  483. $item_id = (int) $item_id;
  484. $data = [];
  485. $errs = [];
  486. foreach ($conf as $tab => $tab_content) {
  487. if ($tab_content === 'categories') {
  488. $data['categories'] = [];
  489. if (@is_array($_POST['categories'])) {
  490. foreach ($_POST['categories'] as $cat_id) {
  491. $cat_id = (int) $cat_id;
  492. if ($cat_id > 0) $data['categories'][] = $cat_id;
  493. }
  494. }
  495. continue;
  496. }
  497. if (!is_array($tab_content)) continue;
  498.  
  499. // Metadata which is passed into argReplace for display/validator argument replacement
  500. $metadata = [
  501. 'id' => $item_id,
  502. ];
  503.  
  504. // Main fields
  505. $field_defns = self::flattenGroups($tab_content);
  506. foreach ($field_defns as $field_defn) {
  507. if (isset($field_defn['for']) and !in_array($mode, $field_defn['for'])) continue;
  508. $validator->setFieldLabel($field_defn['name'], @$field_defn['label']);
  509. if (strpos($field_defn['name'], ',') === false) {
  510. self::collateFieldData($field_defn, @$_POST[$field_defn['name']], $metadata, $validator, $data);
  511. } else {
  512. $errors = [];
  513. foreach (explode(',', $field_defn['name']) as $name) {
  514. // Prevent errors from going into main validation until they have been grouped
  515. $segment_validator = new Validator($_POST);
  516.  
  517. $temp_defn = $field_defn;
  518. $temp_defn['name'] = $name;
  519. self::collateFieldData($temp_defn, @$_POST[$name], $metadata, $segment_validator, $data);
  520. $field_errors = $segment_validator->getFieldErrors();
  521. if (isset($field_errors[$name])) {
  522. $errors = array_merge($errors, $field_errors[$name]);
  523. }
  524. }
  525. foreach ($errors as $err) {
  526. $validator->addFieldError($field_defn['name'], $err);
  527. }
  528. }
  529. }
  530. $errs = array_merge($errs, $validator->getFieldErrors());
  531.  
  532. // Multiedits
  533. $valid = [];
  534. foreach ($tab_content as $item) {
  535. if (!isset($item['multiedit'])) continue;
  536.  
  537. $multed = $item['multiedit'];
  538. $src = 'multiedit_' . $multed['id'];
  539.  
  540. // User has removed all multiedit records of this type
  541. if (!isset($_POST[$src])) continue;
  542.  
  543. $data[$src] = [];
  544. $valid[$src] = [];
  545. $defaults = [];
  546. $field_defns = self::flattenGroups($multed['items']);
  547. foreach ($field_defns as $field_defn) {
  548. $field = $field_defn['name'];
  549.  
  550. if (array_key_exists('default', $field_defn)) {
  551. $defaults[$field] = $field_defn['default'];
  552. }
  553.  
  554. foreach ($_POST[$src] as $item_num => $val) {
  555. if (!isset($val[$field])) $val[$field] = '';
  556. if (!isset($data[$src][$item_num])) $data[$src][$item_num] = [];
  557. if (!isset($errs[$src][$item_num])) $errs[$src][$item_num] = [];
  558. if (!isset($valid[$src][$item_num])) $valid[$src][$item_num] = new Validator([]);
  559.  
  560. if (!isset($data[$src][$item_num]['id']) and isset($val['id'])) {
  561. $data[$src][$item_num]['id'] = (int) $val['id'];
  562. }
  563.  
  564. $valid[$src][$item_num]->setFieldValue($field_defn['name'], $val[$field]);
  565. self::collateFieldData(
  566. $field_defn,
  567. $val[$field],
  568. $metadata,
  569. $valid[$src][$item_num],
  570. $data[$src][$item_num]
  571. );
  572. }
  573. }
  574.  
  575. $errs[$src] = [];
  576. foreach ($valid[$src] as $item_num => $v) {
  577. if ($v->hasErrors()) {
  578. $errs[$src][$item_num] = $v->getFieldErrors();
  579. }
  580. }
  581.  
  582. // Prune empty records, so user doesn't get an error about their required fields
  583. foreach ($data[$src] as $item_num => $record) {
  584. if (MultiEdit::recordEmpty($record, $defaults)) {
  585. unset($data[$src][$item_num]);
  586. unset($errs[$src][$item_num]);
  587. }
  588. }
  589. if (count($errs[$src]) == 0) unset($errs[$src]);
  590. }
  591. }
  592. return [$data, $errs];
  593. }
  594.  
  595.  
  596. /**
  597.   * Collates a single field's $_POST data for INSERT/UPDATE queries, and performs validation
  598.   *
  599.   * @param array $field_defn Field definition from JSON file
  600.   * @param string $input The POSTed input for the field, usually just from $_POST[field_name]
  601.   * @param Validator $valid The validator instance to do validation with
  602.   * @param array &$data Data for DB insert/update
  603.   */
  604. protected static function collateFieldData(array $field_defn, $input, array $metadata, Validator $valid, array &$data)
  605. {
  606. // Don't save anything for display-only fields
  607. if (isset($field_defn['save']) and !$field_defn['save']) return;
  608.  
  609. $field = $field_defn['name'];
  610.  
  611. if (is_array($input)) {
  612. $data[$field] = implode(',', $input);
  613. } else {
  614. $data[$field] = $input;
  615. }
  616.  
  617. if (Validator::isEmpty($input)) {
  618. if (array_key_exists('empty', $field_defn)) {
  619. $data[$field] = $field_defn['empty'];
  620. } else {
  621. $data[$field] = '';
  622. }
  623. $valid->setFieldValue($field, $data[$field]);
  624. }
  625.  
  626. if (!empty($field_defn['required'])) {
  627. $valid->required([$field]);
  628. }
  629.  
  630. if (isset($field_defn['validate'])) {
  631. foreach ($field_defn['validate'] as $call) {
  632. if (!isset($call['func'])) continue;
  633. if (empty($call['args'])) $call['args'] = [];
  634.  
  635. $call['args'] = self::argReplace($call['args'], $metadata);
  636.  
  637. switch (@count($call['args'])) {
  638. case 0:
  639. $valid->check($field, $call['func']);
  640. break;
  641. case 1:
  642. $valid->check($field, $call['func'], $call['args'][0]);
  643. break;
  644. case 2:
  645. $valid->check($field, $call['func'], $call['args'][0], $call['args'][1]);
  646. break;
  647. case 3:
  648. $valid->check($field, $call['func'], $call['args'][0], $call['args'][1], $call['args'][2]);
  649. break;
  650. default:
  651. $args = array_merge([$field, $call['func']], $call['args']);
  652. call_user_func_array(array($valid, 'check'), $args);
  653. break;
  654. }
  655. }
  656. }
  657. }
  658.  
  659.  
  660. /**
  661.   * Replace magic strings in "args" arrays with various metadata values
  662.   *
  663.   * Replacements:
  664.   * %% The current record id
  665.   *
  666.   * @param array $args Arguments in the JsonForm definition
  667.   * @param array $metadata Metadata array
  668.   * @return array Mogrified arguments
  669.   */
  670. private static function argReplace(array $args, array $metadata)
  671. {
  672. foreach ($args as &$arg) {
  673. if ($arg === '%%') {
  674. $arg = $metadata['id'];
  675. }
  676. }
  677.  
  678. return $args;
  679. }
  680.  
  681.  
  682. /**
  683.   * Modify a JSON form config to make a particular field optional
  684.   * @param array $conf The JSON form config
  685.   * @param string $field_name The name of the field
  686.   */
  687. public static function makeOptional(array &$conf, $field_name)
  688. {
  689. self::changeFieldRequired($conf, $field_name, false);
  690. }
  691.  
  692.  
  693. /**
  694.   * Modify a JSON form config to make a particular field required
  695.   * @param array $conf The JSON form config
  696.   * @param string $field_name The name of the field
  697.   */
  698. public static function makeRequired(array &$conf, $field_name)
  699. {
  700. self::changeFieldRequired($conf, $field_name, true);
  701. }
  702.  
  703.  
  704. /**
  705.   * Modify a JSON form config to change the 'required' status of a particular field
  706.   * This implements {@see JsonForm::makeOptional} and {@see JsonForm::makeRequired}
  707.   * @param array $conf The JSON form config
  708.   * @param string $field_name The name of the field
  709.   * @param bool $required True for required, false for optional
  710.   */
  711. protected static function changeFieldRequired(array &$conf, $field_name, $required)
  712. {
  713. $required = (bool) $required;
  714.  
  715. foreach ($conf as $tab_name => &$tab) {
  716. // Ignore e.g. categories
  717. if (!is_array($tab)) continue;
  718.  
  719. foreach ($tab as &$item) {
  720. if (isset($item['field'])) {
  721. if ($item['field']['name'] != $field_name) continue;
  722. $item['field']['required'] = $required;
  723. return;
  724. } else if (isset($item['group'])) {
  725. if (!@is_array($item['group']['items'])) continue;
  726. foreach ($item['group']['items'] as &$group_item) {
  727. if (!isset($group_item['field'])) continue;
  728. if ($group_item['field']['name'] != $field_name) continue;
  729. $group_item['field']['required'] = $required;
  730. return;
  731. }
  732. }
  733. }
  734. }
  735. }
  736.  
  737. }
  738.