TreeDropdownField::tree()   F
last analyzed

Complexity

Conditions 23
Paths 1928

Size

Total Lines 125
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 70
nc 1928
nop 1
dl 0
loc 125
rs 0
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 Exception;
6
use InvalidArgumentException;
7
use SilverStripe\Assets\Folder;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\FieldType\DBDatetime;
13
use SilverStripe\ORM\Hierarchy\Hierarchy;
14
use SilverStripe\ORM\Hierarchy\MarkedSet;
15
16
/**
17
 * Dropdown-like field that allows you to select an item from a hierarchical
18
 * AJAX-expandable tree.
19
 *
20
 * Creates a field which opens a dropdown (actually a div via javascript
21
 * included for you) which contains a tree with the ability to select a singular
22
 * item for the value of the field. This field has the ability to store one-to-one
23
 * joins related to hierarchy or a hierarchy based filter.
24
 *
25
 * **Note:** your source object must use an implementation of hierarchy for this
26
 * field to generate the tree correctly, e.g. {@link Group}, {@link SiteTree} etc.
27
 *
28
 * All operations are carried out through javascript and provides no fallback
29
 * to non JS.
30
 *
31
 * <b>Usage</b>.
32
 *
33
 * <code>
34
 * static $has_one = array(
35
 *   'RightContent' => 'SiteTree'
36
 * );
37
 *
38
 * function getCMSFields() {
39
 * ...
40
 * $treedropdownfield = new TreeDropdownField("RightContentID", "Choose a page to show on the right:", "SiteTree");
41
 * ..
42
 * }
43
 * </code>
44
 *
45
 * This will generate a tree allowing the user to expand and contract subsections
46
 * to find the appropriate page to save to the field.
47
 *
48
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
49
 * since the required frontend dependencies are included through CMS bundling.
50
 *
51
 * @see TreeMultiselectField for the same implementation allowing multiple selections
52
 * @see DropdownField for a simple dropdown field.
53
 * @see CheckboxSetField for multiple selections through checkboxes.
54
 * @see OptionsetField for single selections via radiobuttons.
55
 */
56
class TreeDropdownField extends FormField
57
{
58
    protected $schemaDataType = self::SCHEMA_DATA_TYPE_SINGLESELECT;
59
60
    /** @skipUpgrade */
61
    protected $schemaComponent = 'TreeDropdownField';
62
63
    private static $url_handlers = array(
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
64
        '$Action!/$ID' => '$Action'
65
    );
66
67
    private static $allowed_actions = array(
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
68
        'tree'
69
    );
70
71
    /**
72
     * @config
73
     * @var int
74
     * @see {@link Hierarchy::$node_threshold_total}.
75
     */
76
    private static $node_threshold_total = 30;
0 ignored issues
show
introduced by
The private property $node_threshold_total is not used, and could be removed.
Loading history...
77
78
    /**
79
     * @var string
80
     */
81
    protected $emptyString = null;
82
83
    /**
84
     * @var bool
85
     */
86
    protected $hasEmptyDefault = false;
87
88
    /**
89
     * Class name for underlying object
90
     *
91
     * @var string
92
     */
93
    protected $sourceObject = null;
94
95
    /**
96
     * Name of key field on underlying object
97
     *
98
     * @var string
99
     */
100
    protected $keyField = null;
101
102
    /**
103
     * Name of label field on underlying object
104
     *
105
     * @var string
106
     */
107
    protected $labelField = null;
108
109
    /**
110
     * Similar to labelField but for non-html equivalent of field
111
     *
112
     * @var string
113
     */
114
    protected $titleField = 'Title';
115
116
    /**
117
     * Callback for filtering records
118
     *
119
     * @var callable
120
     */
121
    protected $filterCallback = null;
122
123
    /**
124
     * Callback for marking record as disabled
125
     *
126
     * @var callable
127
     */
128
    protected $disableCallback = null;
129
130
    /**
131
     * Callback for searching records. This callback takes the following arguments:
132
     *  - sourceObject Object class to search
133
     *  - labelField Label field
134
     *  - search Search text
135
     *
136
     * @var callable
137
     */
138
    protected $searchCallback = null;
139
140
    /**
141
     * Filter for base record
142
     *
143
     * @var int
144
     */
145
    protected $baseID = 0;
146
147
    /**
148
     * Default child method in Hierarchy->getChildrenAsUL
149
     *
150
     * @var string
151
     */
152
    protected $childrenMethod = 'AllChildrenIncludingDeleted';
153
154
    /**
155
     * Default child counting method in Hierarchy->getChildrenAsUL
156
     *
157
     * @var string
158
     */
159
    protected $numChildrenMethod = 'numChildren';
160
161
    /**
162
     * Current string value for search text to filter on
163
     *
164
     * @var string
165
     */
166
    protected $search = null;
167
168
    /**
169
     * List of ids in current search result (keys are ids, values are true)
170
     * This includes parents of search result children which may not be an actual result
171
     *
172
     * @var array
173
     */
174
    protected $searchIds = [];
175
176
    /**
177
     * List of ids which matches the search result
178
     * This excludes parents of search result children
179
     *
180
     * @var array
181
     */
182
    protected $realSearchIds = [];
183
184
    /**
185
     * Determine if search should be shown
186
     *
187
     * @var bool
188
     */
189
    protected $showSearch = false;
190
191
    /**
192
     * List of ids which have their search expanded (keys are ids, values are true)
193
     *
194
     * @var array
195
     */
196
    protected $searchExpanded = [];
197
198
    /**
199
     * Show full path for selected options, only applies for single select
200
     * @var bool
201
     */
202
    protected $showSelectedPath = false;
203
204
    /**
205
     * @var array
206
     */
207
    protected static $cacheKeyCache = [];
208
209
    /**
210
     * CAVEAT: for search to work properly $labelField must be a database field,
211
     * or you need to setSearchFunction.
212
     *
213
     * @param string $name the field name
214
     * @param string $title the field label
215
     * @param string $sourceObject A DataObject class name with the {@link Hierarchy} extension.
216
     * @param string $keyField to field on the source class to save as the
217
     *      field value (default ID).
218
     * @param string $labelField the field name to show as the human-readable
219
     *      value on the tree (default Title).
220
     * @param bool $showSearch enable the ability to search the tree by
221
     *      entering the text in the input field.
222
     */
223
    public function __construct(
224
        $name,
225
        $title = null,
226
        $sourceObject = null,
227
        $keyField = 'ID',
228
        $labelField = 'TreeTitle',
229
        $showSearch = true
230
    ) {
231
        if (!is_a($sourceObject, DataObject::class, true)) {
232
            throw new InvalidArgumentException("SourceObject must be a DataObject subclass");
233
        }
234
        if (!DataObject::has_extension($sourceObject, Hierarchy::class)) {
235
            throw new InvalidArgumentException("SourceObject must have Hierarchy extension");
236
        }
237
        $this->setSourceObject($sourceObject);
238
        $this->setKeyField($keyField);
239
        $this->setLabelField($labelField);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Forms\TreeD...nField::setLabelField() has been deprecated: 4.0.0:5.0.0 Use setTitleField() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

239
        /** @scrutinizer ignore-deprecated */ $this->setLabelField($labelField);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
240
        $this->setShowSearch($showSearch);
241
242
        // Extra settings for Folders
243
        if (strcasecmp($sourceObject, Folder::class) === 0) {
244
            $this->setChildrenMethod('ChildFolders');
245
            $this->setNumChildrenMethod('numChildFolders');
246
        }
247
248
        $this->addExtraClass('single');
249
250
        parent::__construct($name, $title);
251
    }
252
253
    /**
254
     * Set the ID of the root node of the tree. This defaults to 0 - i.e.
255
     * displays the whole tree.
256
     *
257
     * @return int
258
     */
259
    public function getTreeBaseID()
260
    {
261
        return $this->baseID;
262
    }
263
264
    /**
265
     * Set the ID of the root node of the tree. This defaults to 0 - i.e.
266
     * displays the whole tree.
267
     *
268
     * @param int $ID
269
     * @return $this
270
     */
271
    public function setTreeBaseID($ID)
272
    {
273
        $this->baseID = (int) $ID;
274
        return $this;
275
    }
276
277
    /**
278
     * Get a callback used to filter the values of the tree before
279
     * displaying to the user.
280
     *
281
     * @return callable
282
     */
283
    public function getFilterFunction()
284
    {
285
        return $this->filterCallback;
286
    }
287
288
    /**
289
     * Set a callback used to filter the values of the tree before
290
     * displaying to the user.
291
     *
292
     * @param callable $callback
293
     * @return $this
294
     */
295
    public function setFilterFunction($callback)
296
    {
297
        if (!is_callable($callback, true)) {
298
            throw new InvalidArgumentException('TreeDropdownField->setFilterCallback(): not passed a valid callback');
299
        }
300
301
        $this->filterCallback = $callback;
302
        return $this;
303
    }
304
305
    /**
306
     * Get the callback used to disable checkboxes for some items in the tree
307
     *
308
     * @return callable
309
     */
310
    public function getDisableFunction()
311
    {
312
        return $this->disableCallback;
313
    }
314
315
    /**
316
     * Set a callback used to disable checkboxes for some items in the tree
317
     *
318
     * @param callable $callback
319
     * @return $this
320
     */
321
    public function setDisableFunction($callback)
322
    {
323
        if (!is_callable($callback, true)) {
324
            throw new InvalidArgumentException('TreeDropdownField->setDisableFunction(): not passed a valid callback');
325
        }
326
327
        $this->disableCallback = $callback;
328
        return $this;
329
    }
330
331
    /**
332
     * Set a callback used to search the hierarchy globally, even before
333
     * applying the filter.
334
     *
335
     * @return callable
336
     */
337
    public function getSearchFunction()
338
    {
339
        return $this->searchCallback;
340
    }
341
342
    /**
343
     * Set a callback used to search the hierarchy globally, even before
344
     * applying the filter.
345
     *
346
     * @param callable $callback
347
     * @return $this
348
     */
349
    public function setSearchFunction($callback)
350
    {
351
        if (!is_callable($callback, true)) {
352
            throw new InvalidArgumentException('TreeDropdownField->setSearchFunction(): not passed a valid callback');
353
        }
354
355
        $this->searchCallback = $callback;
356
        return $this;
357
    }
358
359
    /**
360
     * Check if search is shown
361
     *
362
     * @return bool
363
     */
364
    public function getShowSearch()
365
    {
366
        return $this->showSearch;
367
    }
368
369
    /**
370
     * @param bool $bool
371
     * @return $this
372
     */
373
    public function setShowSearch($bool)
374
    {
375
        $this->showSearch = $bool;
376
        return $this;
377
    }
378
379
    /**
380
     * Get method to invoke on each node to get the child collection
381
     *
382
     * @return string
383
     */
384
    public function getChildrenMethod()
385
    {
386
        return $this->childrenMethod;
387
    }
388
389
    /**
390
     * @param string $method The parameter to ChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
391
     * {@link Hierarchy}. The method specified determines the structure of the returned list. Use "ChildFolders"
392
     * in place of the default to get a drop-down listing with only folders, i.e. not including the child elements in
393
     * the currently selected folder. setNumChildrenMethod() should be used as well for proper functioning.
394
     *
395
     * See {@link Hierarchy} for a complete list of possible methods.
396
     * @return $this
397
     */
398
    public function setChildrenMethod($method)
399
    {
400
        $this->childrenMethod = $method;
401
        return $this;
402
    }
403
404
    /**
405
     * Get method to invoke on nodes to count children
406
     *
407
     * @return string
408
     */
409
    public function getNumChildrenMethod()
410
    {
411
        return $this->numChildrenMethod;
412
    }
413
414
    /**
415
     * @param string $method The parameter to numChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
416
     * {@link Hierarchy}. Should be used in conjunction with setChildrenMethod().
417
     *
418
     * @return $this
419
     */
420
    public function setNumChildrenMethod($method)
421
    {
422
        $this->numChildrenMethod = $method;
423
        return $this;
424
    }
425
426
    public function extraClass()
427
    {
428
        return implode(' ', array(parent::extraClass(), ($this->getShowSearch() ? "searchable" : null)));
429
    }
430
431
    /**
432
     * Get the whole tree of a part of the tree via an AJAX request.
433
     *
434
     * @param HTTPRequest $request
435
     * @return HTTPResponse
436
     * @throws Exception
437
     */
438
    public function tree(HTTPRequest $request)
439
    {
440
        // Regular source specification
441
        $isSubTree = false;
442
443
        $this->search = $request->requestVar('search');
444
        $id = (is_numeric($request->latestParam('ID')))
445
            ? (int)$request->latestParam('ID')
446
            : (int)$request->requestVar('ID');
447
448
        // pre-process the tree - search needs to operate globally, not locally as marking filter does
449
        if ($this->search) {
450
            $this->populateIDs();
451
        }
452
453
        /** @var DataObject|Hierarchy $obj */
454
        $obj = null;
455
        $sourceObject = $this->getSourceObject();
456
457
        // Precache numChildren count if possible.
458
        if ($this->getNumChildrenMethod() == 'numChildren') {
459
            // We're not calling `Hierarchy::prepopulateTreeDataCache()` because we're not customising results based
460
            // on version or Fluent locales. So there would be no performance gain from additional caching.
461
            Hierarchy::prepopulate_numchildren_cache($sourceObject);
462
        }
463
464
        if ($id && !$request->requestVar('forceFullTree')) {
465
            $obj = DataObject::get_by_id($sourceObject, $id);
466
            $isSubTree = true;
467
            if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
468
                throw new Exception(
469
                    "TreeDropdownField->tree(): the object #$id of type $sourceObject could not be found"
470
                );
471
            }
472
        } else {
473
            if ($this->getTreeBaseID()) {
474
                $obj = DataObject::get_by_id($sourceObject, $this->getTreeBaseID());
475
            }
476
477
            if (!$this->getTreeBaseID() || !$obj) {
478
                $obj = DataObject::singleton($sourceObject);
479
            }
480
        }
481
482
        // Create marking set
483
        $markingSet = MarkedSet::create(
484
            $obj,
485
            $this->getChildrenMethod(),
486
            $this->getNumChildrenMethod(),
487
            $this->config()->get('node_threshold_total')
488
        );
489
490
        // Set filter on searched nodes
491
        if ($this->getFilterFunction() || $this->search) {
492
            // Rely on filtering to limit tree
493
            $markingSet->setMarkingFilterFunction(function ($node) {
494
                return $this->filterMarking($node);
495
            });
496
            $markingSet->setLimitingEnabled(false);
497
        }
498
499
        // Begin marking
500
        $markingSet->markPartialTree();
501
502
        // Explicitely mark our search results if necessary
503
        foreach ($this->searchIds as $id => $marked) {
504
            if ($marked) {
505
                $object = $this->objectForKey($id);
506
                if (!$object) {
507
                    continue;
508
                }
509
                $markingSet->markToExpose($object);
510
            }
511
        }
512
513
        // Allow to pass values to be selected within the ajax request
514
        $value = $request->requestVar('forceValue') ?: $this->value;
515
        if ($value && ($values = preg_split('/,\s*/', $value))) {
516
            foreach ($values as $value) {
517
                if (!$value) {
518
                    continue;
519
                }
520
521
                $object = $this->objectForKey($value);
522
                if (!$object) {
523
                    continue;
524
                }
525
                $markingSet->markToExpose($object);
526
            }
527
        }
528
529
        // Set title formatter
530
        $customised = function (DataObject $child) use ($isSubTree) {
531
            return [
532
                'name' => $this->getName(),
533
                'id' => $child->obj($this->getKeyField()),
534
                'title' => $child->obj($this->getTitleField()),
535
                'treetitle' => $child->obj($this->getLabelField()),
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Forms\TreeD...nField::getLabelField() has been deprecated: 4.0.0:5.0.0 Use getTitleField() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

535
                'treetitle' => $child->obj(/** @scrutinizer ignore-deprecated */ $this->getLabelField()),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
536
                'disabled' => $this->nodeIsDisabled($child),
537
                'isSubTree' => $isSubTree
538
            ];
539
        };
540
541
        // Determine output format
542
        if ($request->requestVar('format') === 'json') {
543
            // Format JSON output
544
            $json = $markingSet
545
                ->getChildrenAsArray($customised);
546
547
            if ($request->requestVar('flatList')) {
548
                // format and filter $json here
549
                $json['children'] = $this->flattenChildrenArray($json['children']);
550
            }
551
            return HTTPResponse::create()
552
                ->addHeader('Content-Type', 'application/json')
553
                ->setBody(json_encode($json));
554
        } else {
555
            // Return basic html
556
            $html = $markingSet->renderChildren(
557
                [self::class . '_HTML', 'type' => 'Includes'],
558
                $customised
559
            );
560
            return HTTPResponse::create()
561
                ->addHeader('Content-Type', 'text/html')
562
                ->setBody($html);
563
        }
564
    }
565
566
    /**
567
     * Marking public function for the tree, which combines different filters sensibly.
568
     * If a filter function has been set, that will be called. And if search text is set,
569
     * filter on that too. Return true if all applicable conditions are true, false otherwise.
570
     *
571
     * @param DataObject $node
572
     * @return bool
573
     */
574
    public function filterMarking($node)
575
    {
576
        $callback = $this->getFilterFunction();
577
        if ($callback && !call_user_func($callback, $node)) {
578
            return false;
579
        }
580
581
        if ($this->search) {
582
            return isset($this->searchIds[$node->ID]) && $this->searchIds[$node->ID] ? true : false;
583
        }
584
585
        return true;
586
    }
587
588
    /**
589
     * Marking a specific node in the tree as disabled
590
     * @param $node
591
     * @return boolean
592
     */
593
    public function nodeIsDisabled($node)
594
    {
595
        $callback = $this->getDisableFunction();
596
        return $callback && call_user_func($callback, $node);
597
    }
598
599
    /**
600
     * Attributes to be given for this field type
601
     * @return array
602
     */
603
    public function getAttributes()
604
    {
605
        $attributes = array(
606
            'class' => $this->extraClass(),
607
            'id' => $this->ID(),
608
            'data-schema' => json_encode($this->getSchemaData()),
609
            'data-state' => json_encode($this->getSchemaState()),
610
        );
611
612
        $attributes = array_merge($attributes, $this->attributes);
613
614
        $this->extend('updateAttributes', $attributes);
615
616
        return $attributes;
617
    }
618
619
    /**
620
     * HTML-encoded label for this node, including css classes and other markup.
621
     *
622
     * @deprecated 4.0.0:5.0.0 Use setTitleField()
623
     * @param string $field
624
     * @return $this
625
     */
626
    public function setLabelField($field)
627
    {
628
        $this->labelField = $field;
629
        return $this;
630
    }
631
632
    /**
633
     * HTML-encoded label for this node, including css classes and other markup.
634
     *
635
     * @deprecated 4.0.0:5.0.0 Use getTitleField()
636
     * @return string
637
     */
638
    public function getLabelField()
639
    {
640
        return $this->labelField;
641
    }
642
643
    /**
644
     * Field to use for plain text item titles.
645
     *
646
     * @return string
647
     */
648
    public function getTitleField()
649
    {
650
        return $this->titleField;
651
    }
652
653
    /**
654
     * Set field to use for item title
655
     *
656
     * @param string $field
657
     * @return $this
658
     */
659
    public function setTitleField($field)
660
    {
661
        $this->titleField = $field;
662
        return $this;
663
    }
664
665
    /**
666
     * @param string $field
667
     * @return $this
668
     */
669
    public function setKeyField($field)
670
    {
671
        $this->keyField = $field;
672
        return $this;
673
    }
674
675
    /**
676
     * @return string
677
     */
678
    public function getKeyField()
679
    {
680
        return $this->keyField;
681
    }
682
683
    /**
684
     * @param string $class
685
     * @return $this
686
     */
687
    public function setSourceObject($class)
688
    {
689
        $this->sourceObject = $class;
690
        return $this;
691
    }
692
693
    /**
694
     * Get class of source object
695
     *
696
     * @return string
697
     */
698
    public function getSourceObject()
699
    {
700
        return $this->sourceObject;
701
    }
702
703
    /**
704
     * Flattens a given list of children array items, so the data is no longer
705
     * structured in a hierarchy
706
     *
707
     * NOTE: uses {@link TreeDropdownField::$realSearchIds} to filter items by if there is a search
708
     *
709
     * @param array $children - the list of children, which could contain their own children
710
     * @param array $parentTitles - a list of parent titles, which we use to construct the contextString
711
     * @return array - flattened list of children
712
     */
713
    protected function flattenChildrenArray($children, $parentTitles = [])
714
    {
715
        $output = [];
716
717
        foreach ($children as $child) {
718
            $childTitles = array_merge($parentTitles, [$child['title']]);
719
            $grandChildren = $child['children'];
720
            $contextString = implode('/', $parentTitles);
721
722
            $child['contextString'] = ($contextString !== '') ? $contextString . '/' : '';
723
            unset($child['children']);
724
725
            if (!$this->search || in_array($child['id'], $this->realSearchIds)) {
726
                $output[] = $child;
727
            }
728
            $output = array_merge($output, $this->flattenChildrenArray($grandChildren, $childTitles));
729
        }
730
731
        return $output;
732
    }
733
734
    /**
735
     * Populate $this->searchIds with the IDs of the pages matching the searched parameter and their parents.
736
     * Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter, but modified
737
     * with pluggable search function.
738
     */
739
    protected function populateIDs()
740
    {
741
        // get all the leaves to be displayed
742
        $res = $this->getSearchResults();
743
744
        if (!$res) {
0 ignored issues
show
introduced by
$res is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
745
            return;
746
        }
747
748
        // iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
749
        foreach ($res as $row) {
750
            if ($row->ParentID) {
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
751
                $parents[$row->ParentID] = true;
752
            }
753
            $this->searchIds[$row->ID] = true;
754
        }
755
        $this->realSearchIds = $res->column();
756
757
        $sourceObject = $this->getSourceObject();
758
759
        while (!empty($parents)) {
760
            $items = DataObject::get($sourceObject)
761
                ->filter("ID", array_keys($parents));
762
            $parents = array();
763
764
            foreach ($items as $item) {
765
                if ($item->ParentID) {
766
                    $parents[$item->ParentID] = true;
767
                }
768
                $this->searchIds[$item->ID] = true;
769
                $this->searchExpanded[$item->ID] = true;
770
            }
771
        }
772
    }
773
774
    /**
775
     * Get the DataObjects that matches the searched parameter.
776
     *
777
     * @return DataList
778
     */
779
    protected function getSearchResults()
780
    {
781
        $callback = $this->getSearchFunction();
782
        if ($callback) {
783
            return call_user_func($callback, $this->getSourceObject(), $this->getLabelField(), $this->search);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Forms\TreeD...nField::getLabelField() has been deprecated: 4.0.0:5.0.0 Use getTitleField() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

783
            return call_user_func($callback, $this->getSourceObject(), /** @scrutinizer ignore-deprecated */ $this->getLabelField(), $this->search);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
784
        }
785
786
        $sourceObject = $this->getSourceObject();
787
        $filters = array();
788
        $sourceObjectInstance = DataObject::singleton($sourceObject);
789
        $candidates = array_unique([
790
            $this->getLabelField(),
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Forms\TreeD...nField::getLabelField() has been deprecated: 4.0.0:5.0.0 Use getTitleField() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

790
            /** @scrutinizer ignore-deprecated */ $this->getLabelField(),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
791
            $this->getTitleField(),
792
            'Title',
793
            'Name'
794
        ]);
795
        foreach ($candidates as $candidate) {
796
            if ($sourceObjectInstance->hasDatabaseField($candidate)) {
797
                $filters["{$candidate}:PartialMatch"] = $this->search;
798
            }
799
        }
800
801
        if (empty($filters)) {
802
            throw new InvalidArgumentException(sprintf(
803
                'Cannot query by %s.%s, not a valid database column',
804
                $sourceObject,
805
                $this->getTitleField()
806
            ));
807
        }
808
        return DataObject::get($this->getSourceObject())->filterAny($filters);
809
    }
810
811
    /**
812
     * Get the object where the $keyField is equal to a certain value
813
     *
814
     * @param string|int $key
815
     * @return DataObject
816
     */
817
    protected function objectForKey($key)
818
    {
819
        return DataObject::get($this->getSourceObject())
820
            ->filter($this->getKeyField(), $key)
821
            ->first();
822
    }
823
824
    /**
825
     * Changes this field to the readonly field.
826
     */
827
    public function performReadonlyTransformation()
828
    {
829
        /** @var TreeDropdownField_Readonly $copy */
830
        $copy = $this->castedCopy(TreeDropdownField_Readonly::class);
831
        $copy->setKeyField($this->getKeyField());
832
        $copy->setLabelField($this->getLabelField());
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Forms\TreeD...nField::setLabelField() has been deprecated: 4.0.0:5.0.0 Use setTitleField() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

832
        /** @scrutinizer ignore-deprecated */ $copy->setLabelField($this->getLabelField());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
Deprecated Code introduced by
The function SilverStripe\Forms\TreeD...nField::getLabelField() has been deprecated: 4.0.0:5.0.0 Use getTitleField() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

832
        $copy->setLabelField(/** @scrutinizer ignore-deprecated */ $this->getLabelField());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
833
        $this->setTitleField($this->getTitleField());
834
        $copy->setSourceObject($this->getSourceObject());
835
        return $copy;
836
    }
837
838
    /**
839
     * @param string|FormField $classOrCopy
840
     * @return FormField
841
     */
842
    public function castedCopy($classOrCopy)
843
    {
844
        $field = $classOrCopy;
845
846
        if (!is_object($field)) {
847
            $field = new $classOrCopy($this->name, $this->title, $this->getSourceObject());
848
        }
849
850
        return parent::castedCopy($field);
851
    }
852
853
    public function getSchemaStateDefaults()
854
    {
855
        $data = parent::getSchemaStateDefaults();
856
        /** @var Hierarchy|DataObject $record */
857
        $record = $this->Value() ? $this->objectForKey($this->Value()) : null;
858
859
        $data['data']['cacheKey'] = $this->getCacheKey();
860
        $data['data']['showSelectedPath'] = $this->getShowSelectedPath();
861
        if ($record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
862
            $titlePath = '';
863
864
            if ($this->getShowSelectedPath()) {
865
                $ancestors = $record->getAncestors(true)->reverse();
0 ignored issues
show
Bug introduced by
The method getAncestors() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

865
                $ancestors = $record->/** @scrutinizer ignore-call */ getAncestors(true)->reverse();
Loading history...
866
867
                foreach ($ancestors as $parent) {
868
                    $title = $parent->obj($this->getTitleField())->getValue();
869
                    $titlePath .= $title . '/';
870
                }
871
            }
872
            $data['data']['valueObject'] = [
873
                'id' => $record->obj($this->getKeyField())->getValue(),
874
                'title' => $record->obj($this->getTitleField())->getValue(),
875
                'treetitle' => $record->obj($this->getLabelField())->getSchemaValue(),
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Forms\TreeD...nField::getLabelField() has been deprecated: 4.0.0:5.0.0 Use getTitleField() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

875
                'treetitle' => $record->obj(/** @scrutinizer ignore-deprecated */ $this->getLabelField())->getSchemaValue(),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
876
                'titlePath' => $titlePath,
877
            ];
878
        }
879
880
        return $data;
881
    }
882
883
    /**
884
     * Ensure cache is keyed by last modified datetime of the underlying list.
885
     * Caches the key for the respective underlying list types, since it doesn't need to query again.
886
     *
887
     * @return DBDatetime
888
     */
889
    protected function getCacheKey()
890
    {
891
        $target = $this->getSourceObject();
892
        if (!isset(self::$cacheKeyCache[$target])) {
893
            self::$cacheKeyCache[$target] = DataList::create($target)->max('LastEdited');
894
        }
895
        return self::$cacheKeyCache[$target];
896
    }
897
898
    public function getSchemaDataDefaults()
899
    {
900
        $data = parent::getSchemaDataDefaults();
901
        $data['data'] = array_merge($data['data'], [
902
            'urlTree' => $this->Link('tree'),
903
            'showSearch' => $this->getShowSearch(),
904
            'emptyString' => $this->getEmptyString(),
905
            'hasEmptyDefault' => $this->getHasEmptyDefault(),
906
            'multiple' => false,
907
        ]);
908
909
        return $data;
910
    }
911
912
    /**
913
     * @param boolean $bool
914
     * @return self Self reference
915
     */
916
    public function setHasEmptyDefault($bool)
917
    {
918
        $this->hasEmptyDefault = $bool;
919
        return $this;
920
    }
921
922
    /**
923
     * @return bool
924
     */
925
    public function getHasEmptyDefault()
926
    {
927
        return $this->hasEmptyDefault;
928
    }
929
930
    /**
931
     * Set the default selection label, e.g. "select...".
932
     * Defaults to an empty string. Automatically sets
933
     * {@link $hasEmptyDefault} to true.
934
     *
935
     * @param string $string
936
     * @return $this
937
     */
938
    public function setEmptyString($string)
939
    {
940
        $this->setHasEmptyDefault(true);
941
        $this->emptyString = $string;
942
        return $this;
943
    }
944
945
    /**
946
     * @return string
947
     */
948
    public function getEmptyString()
949
    {
950
        if ($this->emptyString !== null) {
951
            return $this->emptyString;
952
        }
953
954
        $item = DataObject::singleton($this->getSourceObject());
955
        $emptyString = _t(
956
            'SilverStripe\\Forms\\DropdownField.SEARCH_OR_CHOOSE_MODEL',
957
            '(Search or choose {name})',
958
            ['name' => $item->i18n_singular_name()]
959
        );
960
        return $emptyString;
961
    }
962
963
    /**
964
     * @return bool
965
     */
966
    public function getShowSelectedPath()
967
    {
968
        return $this->showSelectedPath;
969
    }
970
971
    /**
972
     * @param bool $showSelectedPath
973
     * @return TreeDropdownField
974
     */
975
    public function setShowSelectedPath($showSelectedPath)
976
    {
977
        $this->showSelectedPath = $showSelectedPath;
978
        return $this;
979
    }
980
}
981