PageRenderTime 32ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/src/Forms/TreeMultiselectField.php

http://github.com/silverstripe/sapphire
PHP | 300 lines | 187 code | 26 blank | 87 comment | 25 complexity | fd3c001d2bdd0a90b5ae09779b692544 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. <?php
  2. namespace SilverStripe\Forms;
  3. use SilverStripe\Core\Convert;
  4. use SilverStripe\Control\Controller;
  5. use SilverStripe\ORM\ArrayList;
  6. use SilverStripe\ORM\DataList;
  7. use SilverStripe\ORM\DataObject;
  8. use SilverStripe\ORM\DataObjectInterface;
  9. use SilverStripe\ORM\FieldType\DBHTMLText;
  10. use SilverStripe\Security\Group;
  11. use SilverStripe\View\ViewableData;
  12. use stdClass;
  13. /**
  14. * This formfield represents many-many joins using a tree selector shown in a dropdown styled element
  15. * which can be added to any form usually in the CMS.
  16. *
  17. * This form class allows you to represent Many-Many Joins in a handy single field. The field has javascript which
  18. * generates a AJAX tree of the site structure allowing you to save selected options to a component set on a given
  19. * {@link DataObject}.
  20. *
  21. * <b>Saving</b>
  22. *
  23. * This field saves a {@link ComponentSet} object which is present on the {@link DataObject} passed by the form,
  24. * returned by calling a function with the same name as the field. The Join is updated by running setByIDList on the
  25. * {@link ComponentSet}
  26. *
  27. * <b>Customizing Save Behaviour</b>
  28. *
  29. * Before the data is saved, you can modify the ID list sent to the {@link ComponentSet} by specifying a function on
  30. * the {@link DataObject} called "onChange[fieldname](&items)". This will be passed by reference the IDlist (an array
  31. * of ID's) from the Treefield to be saved to the component set.
  32. *
  33. * Returning false on this method will prevent treemultiselect from saving to the {@link ComponentSet} of the given
  34. * {@link DataObject}
  35. *
  36. * <code>
  37. * // Called when we try and set the Parents() component set
  38. * // by Tree Multiselect Field in the administration.
  39. * function onChangeParents(&$items) {
  40. * // This ensures this DataObject can never be a parent of itself
  41. * if($items){
  42. * foreach($items as $k => $id){
  43. * if($id == $this->ID){
  44. * unset($items[$k]);
  45. * }
  46. * }
  47. * }
  48. * return true;
  49. * }
  50. * </code>
  51. *
  52. * @see TreeDropdownField for the sample implementation, but only allowing single selects
  53. */
  54. class TreeMultiselectField extends TreeDropdownField
  55. {
  56. public function __construct(
  57. $name,
  58. $title = null,
  59. $sourceObject = Group::class,
  60. $keyField = "ID",
  61. $labelField = "Title"
  62. ) {
  63. parent::__construct($name, $title, $sourceObject, $keyField, $labelField);
  64. $this->removeExtraClass('single');
  65. $this->addExtraClass('multiple');
  66. $this->value = 'unchanged';
  67. }
  68. public function getSchemaDataDefaults()
  69. {
  70. $data = parent::getSchemaDataDefaults();
  71. $data['data'] = array_merge($data['data'], [
  72. 'hasEmptyDefault' => false,
  73. 'multiple' => true,
  74. ]);
  75. return $data;
  76. }
  77. public function getSchemaStateDefaults()
  78. {
  79. $data = parent::getSchemaStateDefaults();
  80. unset($data['data']['valueObject']);
  81. $items = $this->getItems();
  82. $values = [];
  83. foreach ($items as $item) {
  84. if ($item instanceof DataObject) {
  85. $values[] = [
  86. 'id' => $item->obj($this->getKeyField())->getValue(),
  87. 'title' => $item->obj($this->getTitleField())->getValue(),
  88. 'parentid' => $item->ParentID,
  89. 'treetitle' => $item->obj($this->getLabelField())->getSchemaValue(),
  90. ];
  91. } else {
  92. $values[] = $item;
  93. }
  94. }
  95. $data['data']['valueObjects'] = $values;
  96. // cannot rely on $this->value as this could be a many-many relationship
  97. $value = array_column($values, 'id');
  98. if ($value) {
  99. sort($value);
  100. $data['value'] = $value;
  101. } else {
  102. $data['value'] = 'unchanged';
  103. }
  104. return $data;
  105. }
  106. /**
  107. * Return this field's linked items
  108. * @return ArrayList|DataList $items
  109. */
  110. public function getItems()
  111. {
  112. $items = new ArrayList();
  113. // If the value has been set, use that
  114. if ($this->value != 'unchanged') {
  115. $sourceObject = $this->getSourceObject();
  116. if (is_array($sourceObject)) {
  117. $values = is_array($this->value) ? $this->value : preg_split('/ *, */', trim($this->value));
  118. foreach ($values as $value) {
  119. $item = new stdClass;
  120. $item->ID = $value;
  121. $item->Title = $sourceObject[$value];
  122. $items->push($item);
  123. }
  124. return $items;
  125. }
  126. // Otherwise, look data up from the linked relation
  127. if (is_string($this->value)) {
  128. $ids = explode(',', $this->value);
  129. foreach ($ids as $id) {
  130. if (!is_numeric($id)) {
  131. continue;
  132. }
  133. $item = DataObject::get_by_id($sourceObject, $id);
  134. if ($item) {
  135. $items->push($item);
  136. }
  137. }
  138. return $items;
  139. }
  140. }
  141. if ($this->form) {
  142. $fieldName = $this->name;
  143. $record = $this->form->getRecord();
  144. if (is_object($record) && $record->hasMethod($fieldName)) {
  145. return $record->$fieldName();
  146. }
  147. }
  148. return $items;
  149. }
  150. /**
  151. * We overwrite the field attribute to add our hidden fields, as this
  152. * formfield can contain multiple values.
  153. *
  154. * @param array $properties
  155. * @return DBHTMLText
  156. */
  157. public function Field($properties = [])
  158. {
  159. $value = '';
  160. $titleArray = [];
  161. $idArray = [];
  162. $items = $this->getItems();
  163. $emptyTitle = _t('SilverStripe\\Forms\\DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
  164. if ($items && count($items)) {
  165. foreach ($items as $item) {
  166. $idArray[] = $item->ID;
  167. $titleArray[] = ($item instanceof ViewableData)
  168. ? $item->obj($this->getLabelField())->forTemplate()
  169. : Convert::raw2xml($item->{$this->getLabelField()});
  170. }
  171. $title = implode(", ", $titleArray);
  172. sort($idArray);
  173. $value = implode(",", $idArray);
  174. } else {
  175. $title = $emptyTitle;
  176. }
  177. $dataUrlTree = '';
  178. if ($this->form) {
  179. $dataUrlTree = $this->Link('tree');
  180. if (!empty($idArray)) {
  181. $dataUrlTree = Controller::join_links($dataUrlTree, '?forceValue=' . implode(',', $idArray));
  182. }
  183. }
  184. $properties = array_merge(
  185. $properties,
  186. [
  187. 'Title' => $title,
  188. 'EmptyTitle' => $emptyTitle,
  189. 'Link' => $dataUrlTree,
  190. 'Value' => $value
  191. ]
  192. );
  193. return FormField::Field($properties);
  194. }
  195. /**
  196. * Save the results into the form
  197. * Calls function $record->onChange($items) before saving to the assummed
  198. * Component set.
  199. *
  200. * @param DataObjectInterface $record
  201. */
  202. public function saveInto(DataObjectInterface $record)
  203. {
  204. $items = [];
  205. $fieldName = $this->name;
  206. $saveDest = $record->$fieldName();
  207. if (!$saveDest) {
  208. $recordClass = get_class($record);
  209. user_error(
  210. "TreeMultiselectField::saveInto() Field '$fieldName' not found on"
  211. . " {$recordClass}.{$record->ID}",
  212. E_USER_ERROR
  213. );
  214. }
  215. // Detect whether this field has actually been updated
  216. if ($this->value !== 'unchanged') {
  217. if (is_array($this->value)) {
  218. $items = $this->value;
  219. } elseif ($this->value) {
  220. $items = preg_split("/ *, */", trim($this->value));
  221. }
  222. }
  223. // Allows you to modify the items on your object before save
  224. $funcName = "onChange$fieldName";
  225. if ($record->hasMethod($funcName)) {
  226. $result = $record->$funcName($items);
  227. if (!$result) {
  228. return;
  229. }
  230. }
  231. $saveDest->setByIDList($items);
  232. }
  233. /**
  234. * Changes this field to the readonly field.
  235. */
  236. public function performReadonlyTransformation()
  237. {
  238. /** @var TreeMultiselectField_Readonly $copy */
  239. $copy = $this->castedCopy(TreeMultiselectField_Readonly::class);
  240. $copy->setKeyField($this->getKeyField());
  241. $copy->setLabelField($this->getLabelField());
  242. $copy->setSourceObject($this->getSourceObject());
  243. $copy->setTitleField($this->getTitleField());
  244. return $copy;
  245. }
  246. /**
  247. * {@inheritdoc}
  248. *
  249. * @internal To be removed in 5.0
  250. */
  251. protected function objectForKey($key)
  252. {
  253. /**
  254. * Fixes https://github.com/silverstripe/silverstripe-framework/issues/8332
  255. *
  256. * Due to historic reasons, the default (empty) value for this field is 'unchanged', even though
  257. * the field is usually integer on the database side.
  258. * MySQL handles that gracefully and returns an empty result in that case,
  259. * whereas some other databases (e.g. PostgreSQL) do not support comparison
  260. * of numeric types with string values, issuing a database error.
  261. *
  262. * This fix is not ideal, but supposed to keep backward compatibility for SS4.
  263. *
  264. * In 5.0 this method to be removed and NULL should be used instead of 'unchanged' (or an empty array. to be decided).
  265. * In 5.0 this class to be refactored so that $this->value is always an array of values (or null)
  266. */
  267. if ($this->getKeyField() === 'ID' && $key === 'unchanged') {
  268. $key = null;
  269. } elseif (is_string($key)) {
  270. $key = preg_split('/\s*,\s*/', trim($key));
  271. }
  272. return parent::objectForKey($key);
  273. }
  274. }