Completed
Push — master ( 622a07...7c2344 )
by Daniel
108:51 queued 72:30
created

FieldList::flushFieldsCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\Dev\Deprecation;
6
use SilverStripe\ORM\ArrayList;
7
8
/**
9
 * A list designed to hold form field instances.
10
 *
11
 * @method FormField[] getIterator()
12
 */
13
class FieldList extends ArrayList
14
{
15
16
    /**
17
     * Cached flat representation of all fields in this set,
18
     * including fields nested in {@link CompositeFields}.
19
     *
20
     * @uses self::collateDataFields()
21
     * @var FormField[]
22
     */
23
    protected $sequentialSet;
24
25
    /**
26
     * @var FormField[]
27
     */
28
    protected $sequentialSaveableSet;
29
30
    /**
31
     * If this fieldlist is owned by a parent field (e.g. CompositeField)
32
     * this is the parent field.
33
     *
34
     * @var FieldList|FormField
35
     */
36
    protected $containerField;
37
38
    public function __construct($items = array())
39
    {
40
        if (!is_array($items) || func_num_args() > 1) {
41
            $items = func_get_args();
42
        }
43
44
        parent::__construct($items);
45
46
        foreach ($items as $item) {
47
            if ($item instanceof FormField) {
48
                $item->setContainerFieldList($this);
49
            }
50
        }
51
    }
52
53
    public function __clone()
54
    {
55
        // Clone all fields in this list
56
        foreach ($this->items as $key => $field) {
57
            $this->items[$key] = clone $field;
58
        }
59
    }
60
61
    /**
62
     * Return a sequential set of all fields that have data.  This excludes wrapper composite fields
63
     * as well as heading / help text fields.
64
     *
65
     * @return FormField[]
66
     */
67
    public function dataFields()
68
    {
69
        if (!$this->sequentialSet) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sequentialSet of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
70
            $this->collateDataFields($this->sequentialSet);
71
        }
72
        return $this->sequentialSet;
73
    }
74
75
    /**
76
     * @return FormField[]
77
     */
78
    public function saveableFields()
79
    {
80
        if (!$this->sequentialSaveableSet) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sequentialSaveableSet of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
81
            $this->collateDataFields($this->sequentialSaveableSet, true);
82
        }
83
        return $this->sequentialSaveableSet;
84
    }
85
86
    protected function flushFieldsCache()
87
    {
88
        $this->sequentialSet = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,object<Sil...tripe\Forms\FormField>> of property $sequentialSet.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
89
        $this->sequentialSaveableSet = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,object<Sil...tripe\Forms\FormField>> of property $sequentialSaveableSet.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
90
    }
91
92
    protected function collateDataFields(&$list, $saveableOnly = false)
93
    {
94
        if (!isset($list)) {
95
            $list = array();
96
        }
97
        /** @var FormField $field */
98
        foreach ($this as $field) {
99
            if ($field instanceof CompositeField) {
100
                $field->collateDataFields($list, $saveableOnly);
101
            }
102
103
            if ($saveableOnly) {
104
                $isIncluded =  $field->canSubmitValue();
105
            } else {
106
                $isIncluded =  $field->hasData();
107
            }
108
            if ($isIncluded) {
109
                $name = $field->getName();
110
                if (isset($list[$name])) {
111
                    if ($this->form) {
112
                        $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'";
113
                    } else {
114
                        $errSuffix = '';
115
                    }
116
                    user_error(
117
                        "collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.",
118
                        E_USER_ERROR
119
                    );
120
                }
121
                $list[$name] = $field;
122
            }
123
        }
124
    }
125
126
    /**
127
     * Add an extra field to a tab within this FieldList.
128
     * This is most commonly used when overloading getCMSFields()
129
     *
130
     * @param string $tabName The name of the tab or tabset.  Subtabs can be referred to as TabSet.Tab
131
     *                        or TabSet.Tab.Subtab. This function will create any missing tabs.
132
     * @param FormField $field The {@link FormField} object to add to the end of that tab.
133
     * @param string $insertBefore The name of the field to insert before.  Optional.
134
     */
135
    public function addFieldToTab($tabName, $field, $insertBefore = null)
136
    {
137
        // This is a cache that must be flushed
138
        $this->flushFieldsCache();
139
140
        // Find the tab
141
        $tab = $this->findOrMakeTab($tabName);
142
143
        // Add the field to the end of this set
144
        if ($insertBefore) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $insertBefore of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
145
            $tab->insertBefore($insertBefore, $field);
146
        } else {
147
            $tab->push($field);
148
        }
149
    }
150
151
    /**
152
     * Add a number of extra fields to a tab within this FieldList.
153
     * This is most commonly used when overloading getCMSFields()
154
     *
155
     * @param string $tabName The name of the tab or tabset.  Subtabs can be referred to as TabSet.Tab
156
     *                        or TabSet.Tab.Subtab.
157
     * This function will create any missing tabs.
158
     * @param array $fields An array of {@link FormField} objects.
159
     * @param string $insertBefore Name of field to insert before
160
     */
161
    public function addFieldsToTab($tabName, $fields, $insertBefore = null)
162
    {
163
        $this->flushFieldsCache();
164
165
        // Find the tab
166
        $tab = $this->findOrMakeTab($tabName);
167
168
        // Add the fields to the end of this set
169
        foreach ($fields as $field) {
170
            // Check if a field by the same name exists in this tab
171
            if ($insertBefore) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $insertBefore of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
172
                $tab->insertBefore($insertBefore, $field);
173
            } elseif (($name = $field->getName()) && $tab->fieldByName($name)) {
174
                // It exists, so we need to replace the old one
175
                $this->replaceField($field->getName(), $field);
176
            } else {
177
                $tab->push($field);
178
            }
179
        }
180
    }
181
182
    /**
183
     * Remove the given field from the given tab in the field.
184
     *
185
     * @param string $tabName The name of the tab
186
     * @param string $fieldName The name of the field
187
     */
188
    public function removeFieldFromTab($tabName, $fieldName)
189
    {
190
        $this->flushFieldsCache();
191
192
        // Find the tab
193
        $tab = $this->findOrMakeTab($tabName);
194
        $tab->removeByName($fieldName);
195
    }
196
197
    /**
198
     * Removes a number of fields from a Tab/TabSet within this FieldList.
199
     *
200
     * @param string $tabName The name of the Tab or TabSet field
201
     * @param array $fields A list of fields, e.g. array('Name', 'Email')
202
     */
203
    public function removeFieldsFromTab($tabName, $fields)
204
    {
205
        $this->flushFieldsCache();
206
207
        // Find the tab
208
        $tab = $this->findOrMakeTab($tabName);
209
210
        // Add the fields to the end of this set
211
        foreach ($fields as $field) {
212
            $tab->removeByName($field);
213
        }
214
    }
215
216
    /**
217
     * Remove a field or fields from this FieldList by Name.
218
     * The field could also be inside a CompositeField.
219
     *
220
     * @param string|array $fieldName The name of, or an array with the field(s) or tab(s)
221
     * @param boolean $dataFieldOnly If this is true, then a field will only
222
     * be removed if it's a data field.  Dataless fields, such as tabs, will
223
     * be left as-is.
224
     */
225
    public function removeByName($fieldName, $dataFieldOnly = false)
226
    {
227
        if (!$fieldName) {
228
            user_error('FieldList::removeByName() was called with a blank field name.', E_USER_WARNING);
229
        }
230
231
        // Handle array syntax
232
        if (is_array($fieldName)) {
233
            foreach ($fieldName as $field) {
234
                $this->removeByName($field, $dataFieldOnly);
235
            }
236
            return;
237
        }
238
239
        $this->flushFieldsCache();
240
        foreach ($this as $i => $child) {
241
            $childName = $child->getName();
242
            if (!$childName) {
243
                $childName = $child->Title();
244
            }
245
246
            if (($childName == $fieldName) && (!$dataFieldOnly || $child->hasData())) {
247
                array_splice($this->items, $i, 1);
248
                break;
249
            } elseif ($child instanceof CompositeField) {
250
                $child->removeByName($fieldName, $dataFieldOnly);
251
            }
252
        }
253
    }
254
255
    /**
256
     * Replace a single field with another.  Ignores dataless fields such as Tabs and TabSets
257
     *
258
     * @param string $fieldName The name of the field to replace
259
     * @param FormField $newField The field object to replace with
260
     * @return boolean TRUE field was successfully replaced
261
     *                   FALSE field wasn't found, nothing changed
262
     */
263
    public function replaceField($fieldName, $newField)
264
    {
265
        $this->flushFieldsCache();
266
        foreach ($this as $i => $field) {
267
            if ($field->getName() == $fieldName && $field->hasData()) {
268
                $this->items[$i] = $newField;
269
                return true;
270
            } elseif ($field instanceof CompositeField) {
271
                if ($field->replaceField($fieldName, $newField)) {
272
                    return true;
273
                }
274
            }
275
        }
276
        return false;
277
    }
278
279
    /**
280
     * Rename the title of a particular field name in this set.
281
     *
282
     * @param string $fieldName Name of field to rename title of
283
     * @param string $newFieldTitle New title of field
284
     * @return boolean
285
     */
286
    public function renameField($fieldName, $newFieldTitle)
287
    {
288
        $field = $this->dataFieldByName($fieldName);
289
        if (!$field) {
290
            return false;
291
        }
292
293
        $field->setTitle($newFieldTitle);
294
295
        return $field->Title() == $newFieldTitle;
296
    }
297
298
    /**
299
     * @return boolean
300
     */
301
    public function hasTabSet()
302
    {
303
        foreach ($this->items as $i => $field) {
304
            if (is_object($field) && $field instanceof TabSet) {
305
                return true;
306
            }
307
        }
308
309
        return false;
310
    }
311
312
    /**
313
     * Returns the specified tab object, creating it if necessary.
314
     *
315
     * @todo Support recursive creation of TabSets
316
     *
317
     * @param string $tabName The tab to return, in the form "Tab.Subtab.Subsubtab".
318
     *   Caution: Does not recursively create TabSet instances, you need to make sure everything
319
     *   up until the last tab in the chain exists.
320
     * @param string $title Natural language title of the tab. If {@link $tabName} is passed in dot notation,
321
     *   the title parameter will only apply to the innermost referenced tab.
322
     *   The title is only changed if the tab doesn't exist already.
323
     * @return Tab The found or newly created Tab instance
324
     */
325
    public function findOrMakeTab($tabName, $title = null)
326
    {
327
        $parts = explode('.', $tabName);
328
        $last_idx = count($parts) - 1;
329
        // We could have made this recursive, but I've chosen to keep all the logic code within FieldList rather than
330
        // add it to TabSet and Tab too.
331
        $currentPointer = $this;
332
        foreach ($parts as $k => $part) {
333
            $parentPointer = $currentPointer;
334
            /** @var FormField $currentPointer */
335
            $currentPointer = $currentPointer->fieldByName($part);
336
            // Create any missing tabs
337
            if (!$currentPointer) {
338
                if ($parentPointer instanceof TabSet) {
339
                    // use $title on the innermost tab only
340
                    if ($k == $last_idx) {
341
                        $currentPointer = isset($title) ? new Tab($part, $title) : new Tab($part);
342
                    } else {
343
                        $currentPointer = new TabSet($part);
344
                    }
345
                    $parentPointer->push($currentPointer);
346
                } else {
347
                    $withName = $parentPointer instanceof FormField
348
                        ? " named '{$parentPointer->getName()}'"
349
                        : null;
350
                    user_error("FieldList::addFieldToTab() Tried to add a tab to object"
351
                        . " '{$parentPointer->class}'{$withName} - '$part' didn't exist.", E_USER_ERROR);
352
                }
353
            }
354
        }
355
356
        return $currentPointer;
357
    }
358
359
    /**
360
     * Returns a named field.
361
     * You can use dot syntax to get fields from child composite fields
362
     *
363
     * @todo Implement similarly to dataFieldByName() to support nested sets - or merge with dataFields()
364
     *
365
     * @param string $name
366
     * @return FormField
367
     */
368
    public function fieldByName($name)
369
    {
370
        if (strpos($name, '.') !== false) {
371
            list($name, $remainder) = explode('.', $name, 2);
372
        } else {
373
            $remainder = null;
374
        }
375
376
        foreach ($this as $child) {
377
            if (trim($name) == trim($child->getName()) || $name == $child->id) {
378
                if ($remainder) {
379
                    if ($child instanceof CompositeField) {
380
                        return $child->fieldByName($remainder);
381
                    } else {
382
                        user_error(
383
                            "Trying to get field '$remainder' from non-composite field $child->class.$name",
384
                            E_USER_WARNING
385
                        );
386
                        return null;
387
                    }
388
                } else {
389
                    return $child;
390
                }
391
            }
392
        }
393
        return null;
394
    }
395
396
    /**
397
     * Returns a named field in a sequential set.
398
     * Use this if you're using nested FormFields.
399
     *
400
     * @param string $name The name of the field to return
401
     * @return FormField instance
402
     */
403
    public function dataFieldByName($name)
404
    {
405
        if ($dataFields = $this->dataFields()) {
406
            foreach ($dataFields as $child) {
407
                if (trim($name) == trim($child->getName()) || $name == $child->id) {
408
                    return $child;
409
                }
410
            }
411
        }
412
        return null;
413
    }
414
415
    /**
416
     * Inserts a field before a particular field in a FieldList.
417
     *
418
     * @param string $name Name of the field to insert before
419
     * @param FormField $item The form field to insert
420
     * @return FormField|false
421
     */
422
    public function insertBefore($name, $item)
423
    {
424
        // Backwards compatibility for order of arguments
425
        if ($name instanceof FormField) {
426
            Deprecation::notice('5.0', 'Incorrect order of arguments for insertBefore');
427
            list($item, $name) = array($name, $item);
428
        }
429
        $this->onBeforeInsert($item);
430
        $item->setContainerFieldList($this);
431
432
        $i = 0;
433
        foreach ($this as $child) {
434
            if ($name == $child->getName() || $name == $child->id) {
435
                array_splice($this->items, $i, 0, array($item));
436
                return $item;
437
            } elseif ($child instanceof CompositeField) {
438
                $ret = $child->insertBefore($name, $item);
0 ignored issues
show
Bug introduced by
It seems like $name defined by array($name, $item) on line 427 can also be of type object<SilverStripe\Forms\FormField>; however, SilverStripe\Forms\CompositeField::insertBefore() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
439
                if ($ret) {
440
                    return $ret;
441
                }
442
            }
443
            $i++;
444
        }
445
446
        return false;
447
    }
448
449
    /**
450
     * Inserts a field after a particular field in a FieldList.
451
     *
452
     * @param string $name Name of the field to insert after
453
     * @param FormField $item The form field to insert
454
     * @return FormField|false
455
     */
456
    public function insertAfter($name, $item)
457
    {
458
        // Backwards compatibility for order of arguments
459
        if ($name instanceof FormField) {
460
            Deprecation::notice('5.0', 'Incorrect order of arguments for insertAfter');
461
            list($item, $name) = array($name, $item);
462
        }
463
        $this->onBeforeInsert($item);
464
        $item->setContainerFieldList($this);
465
466
        $i = 0;
467
        foreach ($this as $child) {
468
            if ($name == $child->getName() || $name == $child->id) {
469
                array_splice($this->items, $i+1, 0, array($item));
470
                return $item;
471
            } elseif ($child instanceof CompositeField) {
472
                $ret = $child->insertAfter($name, $item);
0 ignored issues
show
Bug introduced by
It seems like $name defined by array($name, $item) on line 461 can also be of type object<SilverStripe\Forms\FormField>; however, SilverStripe\Forms\CompositeField::insertAfter() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
473
                if ($ret) {
474
                    return $ret;
475
                }
476
            }
477
            $i++;
478
        }
479
480
        return false;
481
    }
482
483
    /**
484
     * Push a single field onto the end of this FieldList instance.
485
     *
486
     * @param FormField $item The FormField to add
487
     */
488
    public function push($item)
489
    {
490
        $this->onBeforeInsert($item);
491
        $item->setContainerFieldList($this);
492
493
        return parent::push($item);
494
    }
495
496
    /**
497
     * Push a single field onto the beginning of this FieldList instance.
498
     *
499
     * @param FormField $item The FormField to add
500
     */
501
    public function unshift($item)
502
    {
503
        $this->onBeforeInsert($item);
504
        $item->setContainerFieldList($this);
505
506
        return parent::unshift($item);
507
    }
508
509
    /**
510
     * Handler method called before the FieldList is going to be manipulated.
511
     *
512
     * @param FormField $item
513
     */
514
    protected function onBeforeInsert($item)
515
    {
516
        $this->flushFieldsCache();
517
518
        if ($item->getName()) {
519
            $this->rootFieldList()->removeByName($item->getName(), true);
0 ignored issues
show
Bug introduced by
The method removeByName does only exist in SilverStripe\Forms\FieldList, but not in SilverStripe\Forms\FormField.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
520
        }
521
    }
522
523
524
    /**
525
     * Set the Form instance for this FieldList.
526
     *
527
     * @param Form $form The form to set this FieldList to
528
     * @return $this
529
     */
530
    public function setForm($form)
531
    {
532
        foreach ($this as $field) {
533
            $field->setForm($form);
534
        }
535
536
        return $this;
537
    }
538
539
    /**
540
     * Load the given data into this form.
541
     *
542
     * @param array $data An map of data to load into the FieldList
543
     * @return $this
544
     */
545
    public function setValues($data)
546
    {
547
        foreach ($this->dataFields() as $field) {
548
            $fieldName = $field->getName();
549
            if (isset($data[$fieldName])) {
550
                $field->setValue($data[$fieldName]);
551
            }
552
        }
553
        return $this;
554
    }
555
556
    /**
557
     * Return all <input type="hidden"> fields
558
     * in a form - including fields nested in {@link CompositeFields}.
559
     * Useful when doing custom field layouts.
560
     *
561
     * @return FieldList
562
     */
563
    public function HiddenFields()
564
    {
565
        $hiddenFields = new FieldList();
566
        $dataFields = $this->dataFields();
567
568
        if ($dataFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataFields of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
569
            foreach ($dataFields as $field) {
570
                if ($field instanceof HiddenField) {
571
                    $hiddenFields->push($field);
572
                }
573
            }
574
        }
575
576
        return $hiddenFields;
577
    }
578
579
    /**
580
     * Return all fields except for the hidden fields.
581
     * Useful when making your own simplified form layouts.
582
     */
583
    public function VisibleFields()
584
    {
585
        $visibleFields = new FieldList();
586
587
        foreach ($this as $field) {
588
            if (!($field instanceof HiddenField)) {
589
                $visibleFields->push($field);
590
            }
591
        }
592
593
        return $visibleFields;
594
    }
595
596
    /**
597
     * Transform this FieldList with a given tranform method,
598
     * e.g. $this->transform(new ReadonlyTransformation())
599
     *
600
     * @param FormTransformation $trans
601
     * @return FieldList
602
     */
603
    public function transform($trans)
604
    {
605
        $this->flushFieldsCache();
606
        $newFields = new FieldList();
607
        foreach ($this as $field) {
608
            $newFields->push($field->transform($trans));
609
        }
610
        return $newFields;
611
    }
612
613
    /**
614
     * Returns the root field set that this belongs to
615
     *
616
     * @return FieldList|FormField
617
     */
618
    public function rootFieldList()
619
    {
620
        if ($this->containerField) {
621
            return $this->containerField->rootFieldList();
622
        }
623
624
        return $this;
625
    }
626
627
    /**
628
     * @param $field
629
     * @return $this
630
     */
631
    public function setContainerField($field)
632
    {
633
        $this->containerField = $field;
634
        return $this;
635
    }
636
637
    /**
638
     * Transforms this FieldList instance to readonly.
639
     *
640
     * @return FieldList
641
     */
642
    public function makeReadonly()
643
    {
644
        return $this->transform(new ReadonlyTransformation());
645
    }
646
647
    /**
648
     * Transform the named field into a readonly feld.
649
     *
650
     * @param string|FormField
651
     */
652
    public function makeFieldReadonly($field)
653
    {
654
        $fieldName = ($field instanceof FormField) ? $field->getName() : $field;
655
        $srcField = $this->dataFieldByName($fieldName);
656
        if ($srcField) {
657
            $this->replaceField($fieldName, $srcField->performReadonlyTransformation());
658
        } else {
659
            user_error("Trying to make field '$fieldName' readonly, but it does not exist in the list", E_USER_WARNING);
660
        }
661
    }
662
663
    /**
664
     * Change the order of fields in this FieldList by specifying an ordered list of field names.
665
     * This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and
666
     * shuffle the fields around to the order that you want.
667
     *
668
     * Please note that any tabs or other dataless fields will be clobbered by this operation.
669
     *
670
     * @param array $fieldNames Field names can be given as an array, or just as a list of arguments.
671
     */
672
    public function changeFieldOrder($fieldNames)
673
    {
674
        // Field names can be given as an array, or just as a list of arguments.
675
        if (!is_array($fieldNames)) {
676
            $fieldNames = func_get_args();
677
        }
678
679
        // Build a map of fields indexed by their name.  This will make the 2nd step much easier.
680
        $fieldMap = array();
681
        foreach ($this->dataFields() as $field) {
682
            $fieldMap[$field->getName()] = $field;
683
        }
684
685
        // Iterate through the ordered list	of names, building a new array to be put into $this->items.
686
        // While we're doing this, empty out $fieldMap so that we can keep track of leftovers.
687
        // Unrecognised field names are okay; just ignore them
688
        $fields = array();
689
        foreach ($fieldNames as $fieldName) {
690
            if (isset($fieldMap[$fieldName])) {
691
                $fields[] = $fieldMap[$fieldName];
692
                unset($fieldMap[$fieldName]);
693
            }
694
        }
695
696
        // Add the leftover fields to the end of the list.
697
        $fields = array_values($fields + $fieldMap);
698
699
        // Update our internal $this->items parameter.
700
        $this->items = $fields;
701
702
        $this->flushFieldsCache();
703
    }
704
705
    /**
706
     * Find the numerical position of a field within
707
     * the children collection. Doesn't work recursively.
708
     *
709
     * @param string|FormField
710
     * @return int Position in children collection (first position starts with 0).
711
     * Returns FALSE if the field can't be found.
712
     */
713
    public function fieldPosition($field)
714
    {
715
        if ($field instanceof FormField) {
716
            $field = $field->getName();
717
        }
718
719
        $i = 0;
720
        foreach ($this->dataFields() as $child) {
721
            if ($child->getName() == $field) {
722
                return $i;
723
            }
724
            $i++;
725
        }
726
727
        return false;
728
    }
729
730
    /**
731
     * Default template rendering of a FieldList will concatenate all FieldHolder values.
732
     */
733
    public function forTemplate()
734
    {
735
        $output = "";
736
        foreach ($this as $field) {
737
            $output .= $field->FieldHolder();
738
        }
739
        return $output;
740
    }
741
}
742