Version: current

      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

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -