Completed
Push — authenticator-refactor ( 16f104...61b037 )
by Simon
06:52
created

TreeDropdownField::tree()   F

Complexity

Conditions 18
Paths 322

Size

Total Lines 93
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 53
nc 322
nop 1
dl 0
loc 93
rs 3.6714
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\Assets\Folder;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Control\HTTPResponse;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\ORM\DataList;
10
use SilverStripe\ORM\DataObject;
11
use SilverStripe\ORM\Hierarchy\Hierarchy;
12
use SilverStripe\ORM\Hierarchy\MarkedSet;
13
use SilverStripe\View\ViewableData;
14
use Exception;
15
use InvalidArgumentException;
16
17
/**
18
 * Dropdown-like field that allows you to select an item from a hierarchical
19
 * AJAX-expandable tree.
20
 *
21
 * Creates a field which opens a dropdown (actually a div via javascript
22
 * included for you) which contains a tree with the ability to select a singular
23
 * item for the value of the field. This field has the ability to store one-to-one
24
 * joins related to hierarchy or a hierarchy based filter.
25
 *
26
 * **Note:** your source object must use an implementation of hierarchy for this
27
 * field to generate the tree correctly, e.g. {@link Group}, {@link SiteTree} etc.
28
 *
29
 * All operations are carried out through javascript and provides no fallback
30
 * to non JS.
31
 *
32
 * <b>Usage</b>.
33
 *
34
 * <code>
35
 * static $has_one = array(
36
 *   'RightContent' => 'SiteTree'
37
 * );
38
 *
39
 * function getCMSFields() {
40
 * ...
41
 * $treedropdownfield = new TreeDropdownField("RightContentID", "Choose a page to show on the right:", "SiteTree");
42
 * ..
43
 * }
44
 * </code>
45
 *
46
 * This will generate a tree allowing the user to expand and contract subsections
47
 * to find the appropriate page to save to the field.
48
 *
49
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
50
 * since the required frontend dependencies are included through CMS bundling.
51
 *
52
 * @see TreeMultiselectField for the same implementation allowing multiple selections
53
 * @see DropdownField for a simple dropdown field.
54
 * @see CheckboxSetField for multiple selections through checkboxes.
55
 * @see OptionsetField for single selections via radiobuttons.
56
 */
57
class TreeDropdownField extends FormField
58
{
59
    protected $schemaDataType = self::SCHEMA_DATA_TYPE_SINGLESELECT;
60
61
    protected $schemaComponent = 'TreeDropdownField';
62
63
    private static $url_handlers = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
64
        '$Action!/$ID' => '$Action'
65
    );
66
67
    private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
68
        'tree'
69
    );
70
    
71
    /**
72
     * @var string
73
     */
74
    protected $emptyString = null;
75
    
76
    /**
77
     * @var bool
78
     */
79
    protected $hasEmptyDefault = false;
80
81
    /**
82
     * Class name for underlying object
83
     *
84
     * @var string
85
     */
86
    protected $sourceObject = null;
87
88
    /**
89
     * Name of key field on underlying object
90
     *
91
     * @var string
92
     */
93
    protected $keyField = null;
94
95
    /**
96
     * Name of lavel field on underlying object
97
     *
98
     * @var string
99
     */
100
    protected $labelField = null;
101
102
    /**
103
     * Callback for filtering records
104
     *
105
     * @var callable
106
     */
107
    protected $filterCallback = null;
108
109
    /**
110
     * Callback for marking record as disabled
111
     *
112
     * @var callable
113
     */
114
    protected $disableCallback = null;
115
116
    /**
117
     * Callback for searching records. This callback takes the following arguments:
118
     *  - sourceObject Object class to search
119
     *  - labelField Label field
120
     *  - search Search text
121
     *
122
     * @var callable
123
     */
124
    protected $searchCallback = null;
125
126
    /**
127
     * Filter for base record
128
     *
129
     * @var int
130
     */
131
    protected $baseID = 0;
132
133
    /**
134
     * Default child method in Hierarchy->getChildrenAsUL
135
     *
136
     * @var string
137
     */
138
    protected $childrenMethod = 'AllChildrenIncludingDeleted';
139
140
    /**
141
     * Default child counting method in Hierarchy->getChildrenAsUL
142
     *
143
     * @var string
144
     */
145
    protected $numChildrenMethod = 'numChildren';
146
147
    /**
148
     * Current string value for search text to filter on
149
     *
150
     * @var string
151
     */
152
    protected $search = null;
153
154
    /**
155
     * List of ids in current search result (keys are ids, values are true)
156
     *
157
     * @var array
158
     */
159
    protected $searchIds = [];
160
161
    /**
162
     * Determine if search should be shown
163
     *
164
     * @var bool
165
     */
166
    protected $showSearch = false;
167
168
    /**
169
     * List of ids which have their search expanded (keys are ids, values are true)
170
     *
171
     * @var array
172
     */
173
    protected $searchExpanded = [];
174
175
    /**
176
     * CAVEAT: for search to work properly $labelField must be a database field,
177
     * or you need to setSearchFunction.
178
     *
179
     * @param string $name the field name
180
     * @param string $title the field label
181
     * @param string $sourceObject A DataObject class name with the {@link Hierarchy} extension.
182
     * @param string $keyField to field on the source class to save as the
183
     *      field value (default ID).
184
     * @param string $labelField the field name to show as the human-readable
185
     *      value on the tree (default Title).
186
     * @param bool $showSearch enable the ability to search the tree by
187
     *      entering the text in the input field.
188
     */
189
    public function __construct(
190
        $name,
191
        $title = null,
192
        $sourceObject = null,
193
        $keyField = 'ID',
194
        $labelField = 'TreeTitle',
195
        $showSearch = true
196
    ) {
197
        if (!is_a($sourceObject, DataObject::class, true)) {
198
            throw new InvalidArgumentException("SourceObject must be a DataObject subclass");
199
        }
200
        if (!DataObject::has_extension($sourceObject, Hierarchy::class)) {
201
            throw new InvalidArgumentException("SourceObject must have Hierarchy extension");
202
        }
203
        $this->sourceObject = $sourceObject;
204
        $this->keyField     = $keyField;
205
        $this->labelField   = $labelField;
206
        $this->showSearch   = $showSearch;
207
208
        // Extra settings for Folders
209
        if (strcasecmp($sourceObject, Folder::class) === 0) {
210
            $this->childrenMethod = 'ChildFolders';
211
            $this->numChildrenMethod = 'numChildFolders';
212
        }
213
214
        $this->addExtraClass('single');
215
216
        parent::__construct($name, $title);
217
    }
218
219
    /**
220
     * Set the ID of the root node of the tree. This defaults to 0 - i.e.
221
     * displays the whole tree.
222
     *
223
     * @param int $ID
224
     * @return $this
225
     */
226
    public function setTreeBaseID($ID)
227
    {
228
        $this->baseID = (int) $ID;
229
        return $this;
230
    }
231
232
    /**
233
     * Set a callback used to filter the values of the tree before
234
     * displaying to the user.
235
     *
236
     * @param callback $callback
237
     * @return $this
238
     */
239
    public function setFilterFunction($callback)
240
    {
241
        if (!is_callable($callback, true)) {
242
            throw new InvalidArgumentException('TreeDropdownField->setFilterCallback(): not passed a valid callback');
243
        }
244
245
        $this->filterCallback = $callback;
246
        return $this;
247
    }
248
249
    /**
250
     * Set a callback used to disable checkboxes for some items in the tree
251
     *
252
     * @param callback $callback
253
     * @return $this
254
     */
255
    public function setDisableFunction($callback)
256
    {
257
        if (!is_callable($callback, true)) {
258
            throw new InvalidArgumentException('TreeDropdownField->setDisableFunction(): not passed a valid callback');
259
        }
260
261
        $this->disableCallback = $callback;
262
        return $this;
263
    }
264
265
    /**
266
     * Set a callback used to search the hierarchy globally, even before
267
     * applying the filter.
268
     *
269
     * @param callback $callback
270
     * @return $this
271
     */
272
    public function setSearchFunction($callback)
273
    {
274
        if (!is_callable($callback, true)) {
275
            throw new InvalidArgumentException('TreeDropdownField->setSearchFunction(): not passed a valid callback');
276
        }
277
278
        $this->searchCallback = $callback;
279
        return $this;
280
    }
281
282
    /**
283
     * Check if search is shown
284
     *
285
     * @return bool
286
     */
287
    public function getShowSearch()
288
    {
289
        return $this->showSearch;
290
    }
291
292
    /**
293
     * @param bool $bool
294
     * @return $this
295
     */
296
    public function setShowSearch($bool)
297
    {
298
        $this->showSearch = $bool;
299
        return $this;
300
    }
301
302
    /**
303
     * @param string $method The parameter to ChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
304
     * {@link Hierarchy}. The method specified determines the structure of the returned list. Use "ChildFolders"
305
     * in place of the default to get a drop-down listing with only folders, i.e. not including the child elements in
306
     * the currently selected folder. setNumChildrenMethod() should be used as well for proper functioning.
307
     *
308
     * See {@link Hierarchy} for a complete list of possible methods.
309
     * @return $this
310
     */
311
    public function setChildrenMethod($method)
312
    {
313
        $this->childrenMethod = $method;
314
        return $this;
315
    }
316
317
    /**
318
     * @param string $method The parameter to numChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
319
     * {@link Hierarchy}. Should be used in conjunction with setChildrenMethod().
320
     *
321
     * @return $this
322
     */
323
    public function setNumChildrenMethod($method)
324
    {
325
        $this->numChildrenMethod = $method;
326
        return $this;
327
    }
328
329
    /**
330
     * @param array $properties
331
     * @return string
332
     */
333
    public function Field($properties = array())
334
    {
335
        $record = $this->Value() ? $this->objectForKey($this->Value()) : null;
336
        if ($record instanceof ViewableData) {
337
            $title = $record->obj($this->labelField)->forTemplate();
338
        } elseif ($record) {
339
            $title = Convert::raw2xml($record->{$this->labelField});
340
        } else {
341
            $title = $this->getEmptyString();
342
        }
343
344
        // TODO Implement for TreeMultiSelectField
345
        $metadata = array(
346
            'id' => $record ? $record->ID : null,
347
            'ClassName' => $record ? $record->ClassName : $this->sourceObject
348
        );
349
350
        $properties = array_merge(
351
            $properties,
352
            array(
353
                'Title' => $title,
354
                'EmptyTitle' => $this->getEmptyString(),
355
                'Metadata' => ($metadata) ? Convert::raw2json($metadata) : null,
356
            )
357
        );
358
359
        return parent::Field($properties);
360
    }
361
362
    public function extraClass()
363
    {
364
        return implode(' ', array(parent::extraClass(), ($this->showSearch ? "searchable" : null)));
365
    }
366
367
    /**
368
     * Get the whole tree of a part of the tree via an AJAX request.
369
     *
370
     * @param HTTPRequest $request
371
     * @return HTTPResponse
372
     * @throws Exception
373
     */
374
    public function tree(HTTPRequest $request)
375
    {
376
        // Regular source specification
377
        $isSubTree = false;
378
379
        $this->search = $request->requestVar('search');
380
        $id = (is_numeric($request->latestParam('ID')))
381
            ? (int)$request->latestParam('ID')
382
            : (int)$request->requestVar('ID');
383
384
        /** @var DataObject|Hierarchy $obj */
385
        $obj = null;
386
        if ($id && !$request->requestVar('forceFullTree')) {
387
            $obj = DataObject::get_by_id($this->sourceObject, $id);
388
            $isSubTree = true;
389
            if (!$obj) {
390
                throw new Exception(
391
                    "TreeDropdownField->tree(): the object #$id of type $this->sourceObject could not be found"
392
                );
393
            }
394
        } else {
395
            if ($this->baseID) {
396
                $obj = DataObject::get_by_id($this->sourceObject, $this->baseID);
397
            }
398
399
            if (!$this->baseID || !$obj) {
400
                $obj = DataObject::singleton($this->sourceObject);
401
            }
402
        }
403
404
        // pre-process the tree - search needs to operate globally, not locally as marking filter does
405
        if ($this->search) {
406
            $this->populateIDs();
407
        }
408
409
        // Create marking set
410
        $markingSet = MarkedSet::create($obj, $this->childrenMethod, $this->numChildrenMethod, 30);
411
412
        // Set filter on searched nodes
413
        if ($this->filterCallback || $this->search) {
414
            // Rely on filtering to limit tree
415
            $markingSet->setMarkingFilterFunction(function ($node) {
416
                return $this->filterMarking($node);
417
            });
418
            $markingSet->setLimitingEnabled(false);
419
        }
420
421
        // Begin marking
422
        $markingSet->markPartialTree();
423
424
        // Allow to pass values to be selected within the ajax request
425
        $value = $request->requestVar('forceValue') ?: $this->value;
426
        if ($value && ($values = preg_split('/,\s*/', $value))) {
427
            foreach ($values as $value) {
428
                if (!$value || $value == 'unchanged') {
429
                    continue;
430
                }
431
432
                $markingSet->markToExpose($this->objectForKey($value));
0 ignored issues
show
Bug introduced by
It seems like $this->objectForKey($value) can be null; however, markToExpose() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
433
            }
434
        }
435
436
        // Set title formatter
437
        $customised = function (DataObject $child) use ($isSubTree) {
438
            return [
439
                'name' => $this->getName(),
440
                'id' => $child->obj($this->keyField),
441
                'title' => $child->getTitle(),
442
                'treetitle' => $child->obj($this->labelField),
443
                'disabled' => $this->nodeIsDisabled($child),
444
                'isSubTree' => $isSubTree
445
            ];
446
        };
447
448
        // Determine output format
449
        if ($request->requestVar('format') === 'json') {
450
            // Format JSON output
451
            $json = $markingSet
452
                ->getChildrenAsArray($customised);
453
            return HTTPResponse::create()
454
                ->addHeader('Content-Type', 'application/json')
455
                ->setBody(json_encode($json));
456
        } else {
457
            // Return basic html
458
            $html = $markingSet->renderChildren(
459
                [self::class . '_HTML', 'type' => 'Includes'],
0 ignored issues
show
Documentation introduced by
array(self::class . '_HT..., 'type' => 'Includes') is of type array<integer|string,str...ring","type":"string"}>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
460
                $customised
461
            );
462
            return HTTPResponse::create()
463
                ->addHeader('Content-Type', 'text/html')
464
                ->setBody($html);
465
        }
466
    }
467
468
    /**
469
     * Marking public function for the tree, which combines different filters sensibly.
470
     * If a filter function has been set, that will be called. And if search text is set,
471
     * filter on that too. Return true if all applicable conditions are true, false otherwise.
472
     *
473
     * @param DataObject $node
474
     * @return bool
475
     */
476
    public function filterMarking($node)
477
    {
478
        if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) {
479
            return false;
480
        }
481
482
        if ($this->search) {
483
            return isset($this->searchIds[$node->ID]) && $this->searchIds[$node->ID] ? true : false;
484
        }
485
486
        return true;
487
    }
488
489
    /**
490
     * Marking a specific node in the tree as disabled
491
     * @param $node
492
     * @return boolean
493
     */
494
    public function nodeIsDisabled($node)
495
    {
496
        return ($this->disableCallback && call_user_func($this->disableCallback, $node));
497
    }
498
499
    /**
500
     * @param string $field
501
     * @return $this
502
     */
503
    public function setLabelField($field)
504
    {
505
        $this->labelField = $field;
506
        return $this;
507
    }
508
509
    /**
510
     * @return String
511
     */
512
    public function getLabelField()
513
    {
514
        return $this->labelField;
515
    }
516
517
    /**
518
     * @param string $field
519
     * @return $this
520
     */
521
    public function setKeyField($field)
522
    {
523
        $this->keyField = $field;
524
        return $this;
525
    }
526
527
    /**
528
     * @return String
529
     */
530
    public function getKeyField()
531
    {
532
        return $this->keyField;
533
    }
534
535
    /**
536
     * @param string $class
537
     * @return $this
538
     */
539
    public function setSourceObject($class)
540
    {
541
        $this->sourceObject = $class;
542
        return $this;
543
    }
544
545
    /**
546
     * @return String
547
     */
548
    public function getSourceObject()
549
    {
550
        return $this->sourceObject;
551
    }
552
553
    /**
554
     * Populate $this->searchIds with the IDs of the pages matching the searched parameter and their parents.
555
     * Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter, but modified
556
     * with pluggable search function.
557
     */
558
    protected function populateIDs()
559
    {
560
        // get all the leaves to be displayed
561
        if ($this->searchCallback) {
562
            $res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
563
        } else {
564
            $sourceObject = $this->sourceObject;
565
            $filters = array();
566
            if (singleton($sourceObject)->hasDatabaseField($this->labelField)) {
567
                $filters["{$this->labelField}:PartialMatch"]  = $this->search;
568
            } else {
569
                if (singleton($sourceObject)->hasDatabaseField('Title')) {
570
                    $filters["Title:PartialMatch"] = $this->search;
571
                }
572
                if (singleton($sourceObject)->hasDatabaseField('Name')) {
573
                    $filters["Name:PartialMatch"] = $this->search;
574
                }
575
            }
576
577
            if (empty($filters)) {
578
                throw new InvalidArgumentException(sprintf(
579
                    'Cannot query by %s.%s, not a valid database column',
580
                    $sourceObject,
581
                    $this->labelField
582
                ));
583
            }
584
            $res = DataObject::get($this->sourceObject)->filterAny($filters);
585
        }
586
587
        if ($res) {
588
            // iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
589
            foreach ($res as $row) {
590
                if ($row->ParentID) {
591
                    $parents[$row->ParentID] = true;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parents was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parents = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
592
                }
593
                $this->searchIds[$row->ID] = true;
594
            }
595
596
            $sourceObject = $this->sourceObject;
597
598
            while (!empty($parents)) {
599
                $items = DataObject::get($sourceObject)
600
                    ->filter("ID", array_keys($parents));
601
                $parents = array();
602
603
                foreach ($items as $item) {
604
                    if ($item->ParentID) {
605
                        $parents[$item->ParentID] = true;
606
                    }
607
                    $this->searchIds[$item->ID] = true;
608
                    $this->searchExpanded[$item->ID] = true;
609
                }
610
            }
611
        }
612
    }
613
614
    /**
615
     * Get the object where the $keyField is equal to a certain value
616
     *
617
     * @param string|int $key
618
     * @return DataObject
619
     */
620
    protected function objectForKey($key)
621
    {
622
        return DataObject::get($this->sourceObject)
623
            ->filter($this->keyField, $key)
624
            ->first();
625
    }
626
627
    /**
628
     * Changes this field to the readonly field.
629
     */
630 View Code Duplication
    public function performReadonlyTransformation()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
631
    {
632
        /** @var TreeDropdownField_Readonly $copy */
633
        $copy = $this->castedCopy(TreeDropdownField_Readonly::class);
634
        $copy->setKeyField($this->keyField);
635
        $copy->setLabelField($this->labelField);
636
        $copy->setSourceObject($this->sourceObject);
637
        return $copy;
638
    }
639
    
640
    /**
641
     * @param string|FormField $classOrCopy
642
     * @return FormField
643
     */
644
    public function castedCopy($classOrCopy)
645
    {
646
        $field = $classOrCopy;
647
        
648
        if (!is_object($field)) {
649
            $field = new $classOrCopy($this->name, $this->title, $this->sourceObject);
650
        }
651
    
652
        return parent::castedCopy($field);
653
    }
654
655
    public function getSchemaStateDefaults()
656
    {
657
        $data = parent::getSchemaStateDefaults();
658
        // Check label for field
659
        $record = $this->Value() ? $this->objectForKey($this->Value()) : null;
660
        $selectedlabel = null;
0 ignored issues
show
Unused Code introduced by
$selectedlabel is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
661
662
        // Ensure cache is keyed by last modified date of the underlying list
663
        $data['data']['cacheKey'] = DataList::create($this->sourceObject)->max('LastEdited');
664
        if ($record) {
665
            $data['data']['valueObject'] = [
666
                'id' => $record->getField($this->keyField),
667
                'title' => $record->getTitle(),
668
                'treetitle' => $record->obj($this->labelField)->getSchemaValue(),
669
            ];
670
        }
671
672
        return $data;
673
    }
674
675
    public function getSchemaDataDefaults()
676
    {
677
        $data = parent::getSchemaDataDefaults();
678
        $data['data']['urlTree'] = $this->Link('tree');
679
        $data['data']['emptyString'] = $this->getEmptyString();
680
        $data['data']['hasEmptyDefault'] = $this->getHasEmptyDefault();
681
        
682
        return $data;
683
    }
684
    
685
    /**
686
     * @param boolean $bool
687
     * @return self Self reference
688
     */
689
    public function setHasEmptyDefault($bool)
690
    {
691
        $this->hasEmptyDefault = $bool;
692
        return $this;
693
    }
694
    
695
    /**
696
     * @return bool
697
     */
698
    public function getHasEmptyDefault()
699
    {
700
        return $this->hasEmptyDefault;
701
    }
702
    
703
    /**
704
     * Set the default selection label, e.g. "select...".
705
     * Defaults to an empty string. Automatically sets
706
     * {@link $hasEmptyDefault} to true.
707
     *
708
     * @param string $string
709
     * @return $this
710
     */
711
    public function setEmptyString($string)
712
    {
713
        $this->setHasEmptyDefault(true);
714
        $this->emptyString = $string;
715
        return $this;
716
    }
717
    
718
    /**
719
     * @return string
720
     */
721
    public function getEmptyString()
722
    {
723
        if ($this->emptyString !== null) {
724
            return $this->emptyString;
725
        }
726
        
727
        $item = DataObject::singleton($this->sourceObject);
728
        $emptyString = _t(
729
            'SilverStripe\\Forms\\DropdownField.CHOOSE_MODEL',
730
            '(Choose {name})',
731
            ['name' => $item->i18n_singular_name()]
732
        );
733
        return $emptyString;
734
    }
735
}
736