1 <?php
2 /**
3 * DataObjectSet designed for form fields.
4 * It extends the DataObjectSet with the ability to get a sequential set of fields.
5 * @package forms
6 * @subpackage fields-structural
7 */
8 class FieldSet extends DataObjectSet {
9
10 /**
11 * Cached flat representation of all fields in this set,
12 * including fields nested in {@link CompositeFields}.
13 *
14 * @uses self::collateDataFields()
15 * @var array
16 */
17 protected $sequentialSet;
18
19 /**
20 * @var array
21 */
22 protected $sequentialSaveableSet;
23
24 /**
25 * @todo Documentation
26 */
27 protected $containerField;
28
29 public function __construct($items = null) {
30 // if the first parameter is not an array, or we have more than one parameter, collate all parameters to an array
31 // otherwise use the passed array
32 $itemsArr = (!is_array($items) || count(func_get_args()) > 1) ? func_get_args() : $items;
33 parent::__construct($itemsArr);
34
35 if(isset($this->items) && count($this->items)) {
36 foreach($this->items as $item) {
37 if(isset($item) && is_a($item, 'FormField')) {
38 $item->setContainerFieldSet($this);
39 }
40 }
41 }
42
43 }
44
45 /**
46 * Return a sequential set of all fields that have data. This excludes wrapper composite fields
47 * as well as heading / help text fields.
48 */
49 public function dataFields() {
50 if(!$this->sequentialSet) $this->collateDataFields($this->sequentialSet);
51 return $this->sequentialSet;
52 }
53
54 public function saveableFields() {
55 if(!$this->sequentialSaveableSet) $this->collateDataFields($this->sequentialSaveableSet, true);
56 return $this->sequentialSaveableSet;
57 }
58
59 protected function flushFieldsCache() {
60 $this->sequentialSet = null;
61 $this->sequentialSaveableSet = null;
62 }
63
64 protected function collateDataFields(&$list, $saveableOnly = false) {
65 foreach($this as $field) {
66 if($field->isComposite()) $field->collateDataFields($list, $saveableOnly);
67
68 if($saveableOnly) {
69 $isIncluded = ($field->hasData() && !$field->isReadonly() && !$field->isDisabled());
70 } else {
71 $isIncluded = ($field->hasData());
72 }
73 if($isIncluded) {
74 $name = $field->Name();
75 if(isset($list[$name])) {
76 $errSuffix = "";
77 if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'";
78 else $errSuffix = '';
79 user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR);
80 }
81 $list[$name] = $field;
82 }
83 }
84 }
85
86 /**
87 * Add an extra field to a tab within this fieldset.
88 * This is most commonly used when overloading getCMSFields()
89 *
90 * @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab.
91 * This function will create any missing tabs.
92 * @param FormField $field The {@link FormField} object to add to the end of that tab.
93 * @param string $insertBefore The name of the field to insert before. Optional.
94 */
95 public function addFieldToTab($tabName, $field, $insertBefore = null) {
96 // This is a cache that must be flushed
97 $this->flushFieldsCache();
98
99 // Find the tab
100 $tab = $this->findOrMakeTab($tabName);
101
102 // Add the field to the end of this set
103 if($insertBefore) $tab->insertBefore($field, $insertBefore);
104 else $tab->push($field);
105 }
106
107 /**
108 * Add a number of extra fields to a tab within this fieldset.
109 * This is most commonly used when overloading getCMSFields()
110 *
111 * @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab.
112 * This function will create any missing tabs.
113 * @param array $fields An array of {@link FormField} objects.
114 */
115 public function addFieldsToTab($tabName, $fields) {
116 $this->flushFieldsCache();
117
118 // Find the tab
119 $tab = $this->findOrMakeTab($tabName);
120
121 // Add the fields to the end of this set
122 foreach($fields as $field) {
123 // Check if a field by the same name exists in this tab
124 if($tab->fieldByName($field->Name())) {
125 // It exists, so we need to replace the old one
126 $this->replaceField($field->Name(), $field);
127 } else {
128 $tab->push($field);
129 }
130 }
131 }
132
133 /**
134 * Remove the given field from the given tab in the field.
135 *
136 * @param string $tabName The name of the tab
137 * @param string $fieldName The name of the field
138 */
139 public function removeFieldFromTab($tabName, $fieldName) {
140 $this->flushFieldsCache();
141
142 // Find the tab
143 $tab = $this->findOrMakeTab($tabName);
144 $tab->removeByName($fieldName);
145 }
146
147 /**
148 * Removes a number of fields from a Tab/TabSet within this FieldSet.
149 *
150 * @param string $tabName The name of the Tab or TabSet field
151 * @param array $fields A list of fields, e.g. array('Name', 'Email')
152 */
153 public function removeFieldsFromTab($tabName, $fields) {
154 $this->flushFieldsCache();
155
156 // Find the tab
157 $tab = $this->findOrMakeTab($tabName);
158
159 // Add the fields to the end of this set
160 foreach($fields as $field) $tab->removeByName($field);
161 }
162
163 /**
164 * Remove a field from this FieldSet by Name.
165 * The field could also be inside a CompositeField.
166 *
167 * @param string $fieldName The name of the field or tab
168 * @param boolean $dataFieldOnly If this is true, then a field will only
169 * be removed if it's a data field. Dataless fields, such as tabs, will
170 * be left as-is.
171 */
172 public function removeByName($fieldName, $dataFieldOnly = false) {
173 if(!$fieldName) {
174 user_error('FieldSet::removeByName() was called with a blank field name.', E_USER_WARNING);
175 }
176 $this->flushFieldsCache();
177
178 foreach($this->items as $i => $child) {
179 if(is_object($child)){
180 if(($child->Name() == $fieldName || $child->Title() == $fieldName) && (!$dataFieldOnly || $child->hasData())) {
181 array_splice( $this->items, $i, 1 );
182 break;
183 } else if($child->isComposite()) {
184 $child->removeByName($fieldName, $dataFieldOnly);
185 }
186 }
187 }
188 }
189
190 /**
191 * Replace a single field with another. Ignores dataless fields such as Tabs and TabSets
192 *
193 * @param string $fieldName The name of the field to replace
194 * @param FormField $newField The field object to replace with
195 * @return boolean TRUE field was successfully replaced
196 * FALSE field wasn't found, nothing changed
197 */
198 public function replaceField($fieldName, $newField) {
199 $this->flushFieldsCache();
200 foreach($this->items as $i => $field) {
201 if(is_object($field)) {
202 if($field->Name() == $fieldName && $field->hasData()) {
203 $this->items[$i] = $newField;
204 return true;
205
206 } else if($field->isComposite()) {
207 if($field->replaceField($fieldName, $newField)) return true;
208 }
209 }
210 }
211 return false;
212 }
213
214 /**
215 * Rename the title of a particular field name in this set.
216 *
217 * @param string $fieldName Name of field to rename title of
218 * @param string $newFieldTitle New title of field
219 * @return boolean
220 */
221 function renameField($fieldName, $newFieldTitle) {
222 $field = $this->dataFieldByName($fieldName);
223 if(!$field) return false;
224
225 $field->setTitle($newFieldTitle);
226
227 return $field->Title() == $newFieldTitle;
228 }
229
230 /**
231 * @return boolean
232 */
233 public function hasTabSet() {
234 foreach($this->items as $i => $field) {
235 if(is_object($field) && $field instanceof TabSet) {
236 return true;
237 }
238 }
239
240 return false;
241 }
242
243 /**
244 * Returns the specified tab object, creating it if necessary.
245 *
246 * @todo Support recursive creation of TabSets
247 *
248 * @param string $tabName The tab to return, in the form "Tab.Subtab.Subsubtab".
249 * Caution: Does not recursively create TabSet instances, you need to make sure everything
250 * up until the last tab in the chain exists.
251 * @param string $title Natural language title of the tab. If {@link $tabName} is passed in dot notation,
252 * the title parameter will only apply to the innermost referenced tab.
253 * The title is only changed if the tab doesn't exist already.
254 * @return Tab The found or newly created Tab instance
255 */
256 public function findOrMakeTab($tabName, $title = null) {
257 $parts = explode('.',$tabName);
258
259 // We could have made this recursive, but I've chosen to keep all the logic code within FieldSet rather than add it to TabSet and Tab too.
260 $currentPointer = $this;
261 foreach($parts as $k => $part) {
262 $parentPointer = $currentPointer;
263 $currentPointer = $currentPointer->fieldByName($part);
264 // Create any missing tabs
265 if(!$currentPointer) {
266 if(is_a($parentPointer, 'TabSet')) {
267 // use $title on the innermost tab only
268 if($k == count($parts)-1) {
269 $currentPointer = ($title) ? new Tab($part, $title) : new Tab($part);
270 } else {
271 $currentPointer = new TabSet($part);
272 }
273 $parentPointer->push($currentPointer);
274 } else {
275 $withName = ($parentPointer->hasMethod('Name')) ? " named '{$parentPointer->Name()}'" : null;
276 user_error("FieldSet::addFieldToTab() Tried to add a tab to object '{$parentPointer->class}'{$withName} - '$part' didn't exist.", E_USER_ERROR);
277 }
278 }
279 }
280
281 return $currentPointer;
282 }
283
284 /**
285 * Returns a named field.
286 * You can use dot syntax to get fields from child composite fields
287 *
288 * @todo Implement similiarly to dataFieldByName() to support nested sets - or merge with dataFields()
289 */
290 public function fieldByName($name) {
291 if(strpos($name,'.') !== false) list($name, $remainder) = explode('.',$name,2);
292 else $remainder = null;
293
294 foreach($this->items as $child) {
295 if(trim($name) == trim($child->Name()) || $name == $child->id) {
296 if($remainder) {
297 if($child->isComposite()) {
298 return $child->fieldByName($remainder);
299 } else {
300 user_error("Trying to get field '$remainder' from non-composite field $child->class.$name", E_USER_WARNING);
301 return null;
302 }
303 } else {
304 return $child;
305 }
306 }
307 }
308 }
309
310 /**
311 * Returns a named field in a sequential set.
312 * Use this if you're using nested FormFields.
313 *
314 * @param string $name The name of the field to return
315 * @return FormField instance
316 */
317 public function dataFieldByName($name) {
318 if($dataFields = $this->dataFields()) {
319 foreach($dataFields as $child) {
320 if(trim($name) == trim($child->Name()) || $name == $child->id) return $child;
321 }
322 }
323 }
324
325 /**
326 * Inserts a field before a particular field in a FieldSet.
327 *
328 * @param FormField $item The form field to insert
329 * @param string $name Name of the field to insert before
330 */
331 public function insertBefore($item, $name) {
332 $this->onBeforeInsert($item);
333 $item->setContainerFieldSet($this);
334
335 $i = 0;
336 foreach($this->items as $child) {
337 if($name == $child->Name() || $name == $child->id) {
338 array_splice($this->items, $i, 0, array($item));
339 return $item;
340 } elseif($child->isComposite()) {
341 $ret = $child->insertBefore($item, $name);
342 if($ret) return $ret;
343 }
344 $i++;
345 }
346
347 return false;
348 }
349
350 /**
351 * Inserts a field after a particular field in a FieldSet.
352 *
353 * @param FormField $item The form field to insert
354 * @param string $name Name of the field to insert after
355 */
356 public function insertAfter($item, $name) {
357 $this->onBeforeInsert($item);
358 $item->setContainerFieldSet($this);
359
360 $i = 0;
361 foreach($this->items as $child) {
362 if($name == $child->Name() || $name == $child->id) {
363 array_splice($this->items, $i+1, 0, array($item));
364 return $item;
365 } elseif($child->isComposite()) {
366 $ret = $child->insertAfter($item, $name);
367 if($ret) return $ret;
368 }
369 $i++;
370 }
371
372 return false;
373 }
374
375 /**
376 * Push a single field into this FieldSet instance.
377 *
378 * @param FormField $item The FormField to add
379 * @param string $key An option array key (field name)
380 */
381 public function push($item, $key = null) {
382 $this->onBeforeInsert($item);
383 $item->setContainerFieldSet($this);
384 return parent::push($item, $key = null);
385 }
386
387 /**
388 * Handler method called before the FieldSet is going to be manipulated.
389 */
390 protected function onBeforeInsert($item) {
391 $this->flushFieldsCache();
392 if($item->Name()) $this->rootFieldSet()->removeByName($item->Name(), true);
393 }
394
395
396 /**
397 * Set the Form instance for this FieldSet.
398 *
399 * @param Form $form The form to set this FieldSet to
400 */
401 public function setForm($form) {
402 foreach($this as $field) $field->setForm($form);
403 }
404
405 /**
406 * Load the given data into this form.
407 *
408 * @param data An map of data to load into the FieldSet
409 */
410 public function setValues($data) {
411 foreach($this->dataFields() as $field) {
412 $fieldName = $field->Name();
413 if(isset($data[$fieldName])) $field->setValue($data[$fieldName]);
414 }
415 }
416
417 /**
418 * Return all <input type="hidden"> fields
419 * in a form - including fields nested in {@link CompositeFields}.
420 * Useful when doing custom field layouts.
421 *
422 * @return FieldSet
423 */
424 function HiddenFields() {
425 $hiddenFields = new HiddenFieldSet();
426 $dataFields = $this->dataFields();
427
428 if($dataFields) foreach($dataFields as $field) {
429 if($field instanceof HiddenField) $hiddenFields->push($field);
430 }
431
432 return $hiddenFields;
433 }
434
435 /**
436 * Transform this FieldSet with a given tranform method,
437 * e.g. $this->transform(new ReadonlyTransformation())
438 *
439 * @return FieldSet
440 */
441 function transform($trans) {
442 $this->flushFieldsCache();
443 $newFields = new FieldSet();
444 foreach($this as $field) {
445 $newFields->push($field->transform($trans));
446 }
447 return $newFields;
448 }
449
450 /**
451 * Returns the root field set that this belongs to
452 */
453 function rootFieldSet() {
454 if($this->containerField) return $this->containerField->rootFieldSet();
455 else return $this;
456 }
457
458 function setContainerField($field) {
459 $this->containerField = $field;
460 }
461
462 /**
463 * Transforms this FieldSet instance to readonly.
464 *
465 * @return FieldSet
466 */
467 function makeReadonly() {
468 return $this->transform(new ReadonlyTransformation());
469 }
470
471 /**
472 * Transform the named field into a readonly feld.
473 *
474 * @param string|FormField
475 */
476 function makeFieldReadonly($field) {
477 $fieldName = ($field instanceof FormField) ? $field->Name() : $field;
478 $srcField = $this->dataFieldByName($fieldName);
479 $this->replaceField($fieldName, $srcField->performReadonlyTransformation());
480 }
481
482 /**
483 * Change the order of fields in this FieldSet by specifying an ordered list of field names.
484 * This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and
485 * shuffle the fields around to the order that you want.
486 *
487 * Please note that any tabs or other dataless fields will be clobbered by this operation.
488 *
489 * @param array $fieldNames Field names can be given as an array, or just as a list of arguments.
490 */
491 function changeFieldOrder($fieldNames) {
492 // Field names can be given as an array, or just as a list of arguments.
493 if(!is_array($fieldNames)) $fieldNames = func_get_args();
494
495 // Build a map of fields indexed by their name. This will make the 2nd step much easier.
496 $fieldMap = array();
497 foreach($this->dataFields() as $field) $fieldMap[$field->Name()] = $field;
498
499 // Iterate through the ordered list of names, building a new array to be put into $this->items.
500 // While we're doing this, empty out $fieldMap so that we can keep track of leftovers.
501 // Unrecognised field names are okay; just ignore them
502 $fields = array();
503 foreach($fieldNames as $fieldName) {
504 if(isset($fieldMap[$fieldName])) {
505 $fields[] = $fieldMap[$fieldName];
506 unset($fieldMap[$fieldName]);
507 }
508 }
509
510 // Add the leftover fields to the end of the list.
511 $fields = $fields + array_values($fieldMap);
512
513 // Update our internal $this->items parameter.
514 $this->items = $fields;
515
516 $this->flushFieldsCache();
517 }
518
519 /**
520 * Find the numerical position of a field within
521 * the children collection. Doesn't work recursively.
522 *
523 * @param string|FormField
524 * @return Position in children collection (first position starts with 0). Returns FALSE if the field can't be found.
525 */
526 function fieldPosition($field) {
527 if(is_object($field)) $field = $field->Name();
528
529 $i = 0;
530 foreach($this->dataFields() as $child) {
531 if($child->Name() == $field) return $i;
532 $i++;
533 }
534
535 return false;
536 }
537
538 }
539
540 /**
541 * A fieldset designed to store a list of hidden fields. When inserted into a template, only the
542 * input tags will be included
543 *
544 * @package forms
545 * @subpackage fields-structural
546 */
547 class HiddenFieldSet extends FieldSet {
548 function forTemplate() {
549 $output = "";
550 foreach($this as $field) {
551 $output .= $field->Field();
552 }
553 return $output;
554 }
555 }
556
557 ?>
558