Passed
Push — pulls/manymanylist-add-callbac... ( 7e0693...0d7c5a )
by Ingo
09:03
created

TreeMultiselectField::setValue()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
c 0
b 0
f 0
nc 6
nop 2
dl 0
loc 17
rs 10
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Core\Convert;
7
use SilverStripe\ORM\ArrayList;
8
use SilverStripe\ORM\DataList;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\DataObjectInterface;
11
use SilverStripe\ORM\FieldType\DBHTMLText;
12
use SilverStripe\ORM\Relation;
13
use SilverStripe\ORM\SS_List;
14
use SilverStripe\Security\Group;
15
use SilverStripe\View\ViewableData;
16
17
/**
18
 * This formfield represents many-many joins using a tree selector shown in a dropdown styled element
19
 * which can be added to any form usually in the CMS.
20
 *
21
 * This form class allows you to represent Many-Many Joins in a handy single field. The field has javascript which
22
 * generates a AJAX tree of the site structure allowing you to save selected options to a component set on a given
23
 * {@link DataObject}.
24
 *
25
 * <b>Saving</b>
26
 *
27
 * This field saves a {@link ComponentSet} object which is present on the {@link DataObject} passed by the form,
28
 * returned by calling a function with the same name as the field. The Join is updated by running setByIDList on the
29
 * {@link ComponentSet}
30
 *
31
 * <b>Customizing Save Behaviour</b>
32
 *
33
 * Before the data is saved, you can modify the ID list sent to the {@link ComponentSet} by specifying a function on
34
 * the {@link DataObject} called "onChange[fieldname](&items)". This will be passed by reference the IDlist (an array
35
 * of ID's) from the Treefield to be saved to the component set.
36
 *
37
 * Returning false on this method will prevent treemultiselect from saving to the {@link ComponentSet} of the given
38
 * {@link DataObject}
39
 *
40
 * <code>
41
 * // Called when we try and set the Parents() component set
42
 * // by Tree Multiselect Field in the administration.
43
 * function onChangeParents(&$items) {
44
 *  // This ensures this DataObject can never be a parent of itself
45
 *  if($items){
46
 *      foreach($items as $k => $id){
47
 *          if($id == $this->ID){
48
 *              unset($items[$k]);
49
 *          }
50
 *      }
51
 *  }
52
 *  return true;
53
 * }
54
 * </code>
55
 *
56
 * @see TreeDropdownField for the sample implementation, but only allowing single selects
57
 */
58
class TreeMultiselectField extends TreeDropdownField
59
{
60
    public function __construct(
61
        $name,
62
        $title = null,
63
        $sourceObject = Group::class,
64
        $keyField = "ID",
65
        $labelField = "Title"
66
    ) {
67
        parent::__construct($name, $title, $sourceObject, $keyField, $labelField);
68
        $this->removeExtraClass('single');
69
        $this->addExtraClass('multiple');
70
        $this->value = 'unchanged';
71
    }
72
73
    public function getSchemaDataDefaults()
74
    {
75
        $data = parent::getSchemaDataDefaults();
76
77
        $data['data'] = array_merge($data['data'], [
78
            'hasEmptyDefault' => false,
79
            'multiple' => true,
80
        ]);
81
        return $data;
82
    }
83
84
    public function getSchemaStateDefaults()
85
    {
86
        $data = parent::getSchemaStateDefaults();
87
        unset($data['data']['valueObject']);
88
89
        $items = $this->getItems();
90
        $values = [];
91
        foreach ($items as $item) {
92
            if ($item instanceof DataObject) {
93
                $values[] = [
94
                    'id' => $item->obj($this->getKeyField())->getValue(),
95
                    'title' => $item->obj($this->getTitleField())->getValue(),
96
                    'parentid' => $item->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...
97
                    'treetitle' => $item->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

97
                    'treetitle' => $item->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...
98
                ];
99
            } else {
100
                $values[] = $item;
101
            }
102
        }
103
        $data['data']['valueObjects'] = $values;
104
105
        // cannot rely on $this->value as this could be a many-many relationship
106
        $value = array_column($values, 'id');
107
        if ($value) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
108
            sort($value);
109
            $data['value'] = $value;
110
        } else {
111
            $data['value'] = 'unchanged';
112
        }
113
114
        return $data;
115
    }
116
117
    /**
118
     * Return this field's linked items
119
     * @return ArrayList|DataList $items
120
     */
121
    public function getItems()
122
    {
123
        $value = $this->Value();
124
125
        // If unchanged, load from record
126
        if ($value === 'unchanged') {
127
            // Verify a form exists
128
            $form = $this->getForm();
129
            if (!$form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
130
                return ArrayList::create();
131
            }
132
133
            // Verify this form has an attached record with the necessary relation
134
            $fieldName = $this->getName();
135
            $record = $form->getRecord();
136
            if ($record instanceof DataObject && $record->hasMethod($fieldName)) {
137
                return $record->$fieldName();
138
            }
139
140
            // No relation on parent record found
141
            return ArrayList::create();
142
        }
143
144
        // Value is a list
145
        if ($value instanceof SS_List) {
146
            return $value;
147
        }
148
149
        // Parse ids from value string / array
150
        $ids = [];
151
        if (is_string($value)) {
152
            $ids = preg_split("#\s*,\s*#", trim($value));
153
        } elseif (is_array($value)) {
154
            $ids = array_values($value);
155
        }
156
157
        // No value
158
        if (empty($ids)) {
159
            return ArrayList::create();
160
        }
161
162
        // Query source records by value field
163
        return DataObject::get($this->getSourceObject())
164
            ->filter($this->getKeyField(), $ids);
165
    }
166
167
    public function setValue($value, $source = null)
168
    {
169
        // If loading from a dataobject, get items by relation
170
        if ($source instanceof DataObject) {
171
            $name = $this->getName();
172
            if ($source->hasMethod($name)) {
173
                $value = $source->$name();
174
            }
175
        }
176
177
        // Handle legacy value; form-submitted `unchanged` implies empty set.
178
        // See TreeDropdownField.js
179
        if ($value === 'unchanged') {
180
            $value = [];
181
        }
182
183
        return parent::setValue($value);
184
    }
185
186
    public function dataValue()
187
    {
188
        return $this->getItems()->column($this->getKeyField());
189
    }
190
191
    /**
192
     * We overwrite the field attribute to add our hidden fields, as this
193
     * formfield can contain multiple values.
194
     *
195
     * @param array $properties
196
     * @return DBHTMLText
197
     */
198
    public function Field($properties = [])
199
    {
200
        $value = '';
201
        $titleArray = [];
202
        $idArray = [];
203
        $items = $this->getItems();
204
        $emptyTitle = _t('SilverStripe\\Forms\\DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
205
206
        if ($items && count($items)) {
207
            foreach ($items as $item) {
208
                $idArray[] = $item->ID;
209
                $titleArray[] = ($item instanceof ViewableData)
210
                    ? $item->obj($this->getLabelField())->forTemplate()
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

210
                    ? $item->obj(/** @scrutinizer ignore-deprecated */ $this->getLabelField())->forTemplate()

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...
211
                    : Convert::raw2xml($item->{$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

211
                    : Convert::raw2xml($item->{/** @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...
212
            }
213
214
            $title = implode(", ", $titleArray);
215
            sort($idArray);
216
            $value = implode(",", $idArray);
217
        } else {
218
            $title = $emptyTitle;
219
        }
220
221
        $dataUrlTree = '';
222
        if ($this->form) {
223
            $dataUrlTree = $this->Link('tree');
224
            if (!empty($idArray)) {
225
                $dataUrlTree = Controller::join_links($dataUrlTree, '?forceValue=' . implode(',', $idArray));
226
            }
227
        }
228
        $properties = array_merge(
229
            $properties,
230
            [
231
                'Title' => $title,
232
                'EmptyTitle' => $emptyTitle,
233
                'Link' => $dataUrlTree,
234
                'Value' => $value
235
            ]
236
        );
237
        return FormField::Field($properties);
238
    }
239
240
    /**
241
     * Save the results into the form
242
     * Calls function $record->onChange($items) before saving to the assummed
243
     * Component set.
244
     *
245
     * @param DataObjectInterface $record
246
     */
247
    public function saveInto(DataObjectInterface $record)
248
    {
249
        $fieldName = $this->getName();
250
251
        /** @var Relation $saveDest */
252
        $saveDest = $record->$fieldName();
253
        if (!$saveDest) {
0 ignored issues
show
introduced by
$saveDest is of type SilverStripe\ORM\Relation, thus it always evaluated to true.
Loading history...
254
            $recordClass = get_class($record);
255
            user_error(
256
                "TreeMultiselectField::saveInto() Field '$fieldName' not found on"
257
                . " {$recordClass}.{$record->ID}",
258
                E_USER_ERROR
259
            );
260
        }
261
262
        $itemIDs = $this->getItems()->column('ID');
263
264
        // Allows you to modify the itemIDs on your object before save
265
        $funcName = "onChange$fieldName";
266
        if ($record->hasMethod($funcName)) {
0 ignored issues
show
Bug introduced by
The method hasMethod() does not exist on SilverStripe\ORM\DataObjectInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to SilverStripe\ORM\DataObjectInterface. ( Ignorable by Annotation )

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

266
        if ($record->/** @scrutinizer ignore-call */ hasMethod($funcName)) {
Loading history...
267
            $result = $record->$funcName($itemIDs);
268
            if (!$result) {
269
                return;
270
            }
271
        }
272
        $saveDest->setByIDList($itemIDs);
273
    }
274
275
    /**
276
     * Changes this field to the readonly field.
277
     */
278
    public function performReadonlyTransformation()
279
    {
280
        /** @var TreeMultiselectField_Readonly $copy */
281
        $copy = $this->castedCopy(TreeMultiselectField_Readonly::class);
282
        $copy->setKeyField($this->getKeyField());
283
        $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

283
        /** @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

283
        $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...
284
        $copy->setSourceObject($this->getSourceObject());
285
        $copy->setTitleField($this->getTitleField());
286
        return $copy;
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     *
292
     * @internal To be removed in 5.0
293
     */
294
    protected function objectForKey($key)
295
    {
296
        /**
297
         * Fixes https://github.com/silverstripe/silverstripe-framework/issues/8332
298
         *
299
         * Due to historic reasons, the default (empty) value for this field is 'unchanged', even though
300
         * the field is usually integer on the database side.
301
         * MySQL handles that gracefully and returns an empty result in that case,
302
         * whereas some other databases (e.g. PostgreSQL) do not support comparison
303
         * of numeric types with string values, issuing a database error.
304
         *
305
         * This fix is not ideal, but supposed to keep backward compatibility for SS4.
306
         *
307
         * In 5.0 this method to be removed and NULL should be used instead of 'unchanged' (or an empty array. to be decided).
308
         * In 5.0 this class to be refactored so that $this->value is always an array of values (or null)
309
         */
310
        if ($this->getKeyField() === 'ID' && $key === 'unchanged') {
311
            $key = null;
312
        } elseif (is_string($key)) {
313
            $key = preg_split('/\s*,\s*/', trim($key));
314
        }
315
316
        return parent::objectForKey($key);
317
    }
318
}
319