Completed
Push — fix-2494 ( 3153ee...40d9bb )
by Sam
13:43 queued 06:38
created

TreeDropdownField::getSchemaStateDefaults()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 4
nop 0
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
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
                $object = $this->objectForKey($value);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $object is correct as $this->objectForKey($value) (which targets SilverStripe\Forms\TreeD...wnField::objectForKey()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
433
                if (!$object) {
434
                    continue;
435
                }
436
                $markingSet->markToExpose($object);
437
            }
438
        }
439
440
        // Set title formatter
441
        $customised = function (DataObject $child) use ($isSubTree) {
442
            return [
443
                'name' => $this->getName(),
444
                'id' => $child->obj($this->keyField),
445
                'title' => $child->getTitle(),
446
                'treetitle' => $child->obj($this->labelField),
447
                'disabled' => $this->nodeIsDisabled($child),
448
                'isSubTree' => $isSubTree
449
            ];
450
        };
451
452
        // Determine output format
453
        if ($request->requestVar('format') === 'json') {
454
            // Format JSON output
455
            $json = $markingSet
456
                ->getChildrenAsArray($customised);
457
            return HTTPResponse::create()
458
                ->addHeader('Content-Type', 'application/json')
459
                ->setBody(json_encode($json));
460
        } else {
461
            // Return basic html
462
            $html = $markingSet->renderChildren(
463
                [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...
464
                $customised
465
            );
466
            return HTTPResponse::create()
467
                ->addHeader('Content-Type', 'text/html')
468
                ->setBody($html);
469
        }
470
    }
471
472
    /**
473
     * Marking public function for the tree, which combines different filters sensibly.
474
     * If a filter function has been set, that will be called. And if search text is set,
475
     * filter on that too. Return true if all applicable conditions are true, false otherwise.
476
     *
477
     * @param DataObject $node
478
     * @return bool
479
     */
480
    public function filterMarking($node)
481
    {
482
        if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) {
483
            return false;
484
        }
485
486
        if ($this->search) {
487
            return isset($this->searchIds[$node->ID]) && $this->searchIds[$node->ID] ? true : false;
488
        }
489
490
        return true;
491
    }
492
493
    /**
494
     * Marking a specific node in the tree as disabled
495
     * @param $node
496
     * @return boolean
497
     */
498
    public function nodeIsDisabled($node)
499
    {
500
        return ($this->disableCallback && call_user_func($this->disableCallback, $node));
501
    }
502
503
    /**
504
     * @param string $field
505
     * @return $this
506
     */
507
    public function setLabelField($field)
508
    {
509
        $this->labelField = $field;
510
        return $this;
511
    }
512
513
    /**
514
     * @return String
515
     */
516
    public function getLabelField()
517
    {
518
        return $this->labelField;
519
    }
520
521
    /**
522
     * @param string $field
523
     * @return $this
524
     */
525
    public function setKeyField($field)
526
    {
527
        $this->keyField = $field;
528
        return $this;
529
    }
530
531
    /**
532
     * @return String
533
     */
534
    public function getKeyField()
535
    {
536
        return $this->keyField;
537
    }
538
539
    /**
540
     * @param string $class
541
     * @return $this
542
     */
543
    public function setSourceObject($class)
544
    {
545
        $this->sourceObject = $class;
546
        return $this;
547
    }
548
549
    /**
550
     * @return String
551
     */
552
    public function getSourceObject()
553
    {
554
        return $this->sourceObject;
555
    }
556
557
    /**
558
     * Populate $this->searchIds with the IDs of the pages matching the searched parameter and their parents.
559
     * Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter, but modified
560
     * with pluggable search function.
561
     */
562
    protected function populateIDs()
563
    {
564
        // get all the leaves to be displayed
565
        if ($this->searchCallback) {
566
            $res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
567
        } else {
568
            $sourceObject = $this->sourceObject;
569
            $filters = array();
570
            if (singleton($sourceObject)->hasDatabaseField($this->labelField)) {
571
                $filters["{$this->labelField}:PartialMatch"]  = $this->search;
572
            } else {
573
                if (singleton($sourceObject)->hasDatabaseField('Title')) {
574
                    $filters["Title:PartialMatch"] = $this->search;
575
                }
576
                if (singleton($sourceObject)->hasDatabaseField('Name')) {
577
                    $filters["Name:PartialMatch"] = $this->search;
578
                }
579
            }
580
581
            if (empty($filters)) {
582
                throw new InvalidArgumentException(sprintf(
583
                    'Cannot query by %s.%s, not a valid database column',
584
                    $sourceObject,
585
                    $this->labelField
586
                ));
587
            }
588
            $res = DataObject::get($this->sourceObject)->filterAny($filters);
589
        }
590
591
        if ($res) {
592
            // iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
593
            foreach ($res as $row) {
594
                if ($row->ParentID) {
595
                    $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...
596
                }
597
                $this->searchIds[$row->ID] = true;
598
            }
599
600
            $sourceObject = $this->sourceObject;
601
602
            while (!empty($parents)) {
603
                $items = DataObject::get($sourceObject)
604
                    ->filter("ID", array_keys($parents));
605
                $parents = array();
606
607
                foreach ($items as $item) {
608
                    if ($item->ParentID) {
609
                        $parents[$item->ParentID] = true;
610
                    }
611
                    $this->searchIds[$item->ID] = true;
612
                    $this->searchExpanded[$item->ID] = true;
613
                }
614
            }
615
        }
616
    }
617
618
    /**
619
     * Get the object where the $keyField is equal to a certain value
620
     *
621
     * @param string|int $key
622
     * @return DataObject
623
     */
624
    protected function objectForKey($key)
625
    {
626
        return DataObject::get($this->sourceObject)
627
            ->filter($this->keyField, $key)
628
            ->first();
629
    }
630
631
    /**
632
     * Changes this field to the readonly field.
633
     */
634 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...
635
    {
636
        /** @var TreeDropdownField_Readonly $copy */
637
        $copy = $this->castedCopy(TreeDropdownField_Readonly::class);
638
        $copy->setKeyField($this->keyField);
639
        $copy->setLabelField($this->labelField);
640
        $copy->setSourceObject($this->sourceObject);
641
        return $copy;
642
    }
643
644
    /**
645
     * @param string|FormField $classOrCopy
646
     * @return FormField
647
     */
648
    public function castedCopy($classOrCopy)
649
    {
650
        $field = $classOrCopy;
651
652
        if (!is_object($field)) {
653
            $field = new $classOrCopy($this->name, $this->title, $this->sourceObject);
654
        }
655
656
        return parent::castedCopy($field);
657
    }
658
659
    public function getSchemaStateDefaults()
660
    {
661
        $data = parent::getSchemaStateDefaults();
662
        // Check label for field
663
        $record = $this->Value() ? $this->objectForKey($this->Value()) : null;
664
        $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...
665
666
        // Ensure cache is keyed by last modified date of the underlying list
667
        $data['data']['cacheKey'] = DataList::create($this->sourceObject)->max('LastEdited');
668
        if ($record) {
669
            $data['data']['valueObject'] = [
670
                'id' => $record->getField($this->keyField),
671
                'title' => $record->getTitle(),
672
                'treetitle' => $record->obj($this->labelField)->getSchemaValue(),
673
            ];
674
        }
675
676
        return $data;
677
    }
678
679
    public function getSchemaDataDefaults()
680
    {
681
        $data = parent::getSchemaDataDefaults();
682
        $data['data']['urlTree'] = $this->Link('tree');
683
        $data['data']['emptyString'] = $this->getEmptyString();
684
        $data['data']['hasEmptyDefault'] = $this->getHasEmptyDefault();
685
686
        return $data;
687
    }
688
689
    /**
690
     * @param boolean $bool
691
     * @return self Self reference
692
     */
693
    public function setHasEmptyDefault($bool)
694
    {
695
        $this->hasEmptyDefault = $bool;
696
        return $this;
697
    }
698
699
    /**
700
     * @return bool
701
     */
702
    public function getHasEmptyDefault()
703
    {
704
        return $this->hasEmptyDefault;
705
    }
706
707
    /**
708
     * Set the default selection label, e.g. "select...".
709
     * Defaults to an empty string. Automatically sets
710
     * {@link $hasEmptyDefault} to true.
711
     *
712
     * @param string $string
713
     * @return $this
714
     */
715
    public function setEmptyString($string)
716
    {
717
        $this->setHasEmptyDefault(true);
718
        $this->emptyString = $string;
719
        return $this;
720
    }
721
722
    /**
723
     * @return string
724
     */
725
    public function getEmptyString()
726
    {
727
        if ($this->emptyString !== null) {
728
            return $this->emptyString;
729
        }
730
731
        $item = DataObject::singleton($this->sourceObject);
732
        $emptyString = _t(
733
            'SilverStripe\\Forms\\DropdownField.CHOOSE_MODEL',
734
            '(Choose {name})',
735
            ['name' => $item->i18n_singular_name()]
736
        );
737
        return $emptyString;
738
    }
739
}
740