Passed
Push — fix-1489 ( eb3277...3c1f50 )
by Sam
05:13
created

MultiSelectField::setDefaultItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\ORM\DataObject;
6
use SilverStripe\ORM\DataObjectInterface;
7
use SilverStripe\ORM\FieldType\DBMultiEnum;
8
use SilverStripe\ORM\Relation;
9
10
/**
11
 * Represents a SelectField that may potentially have multiple selections, and may have
12
 * a {@link ManyManyList} as a data source.
13
 */
14
abstract class MultiSelectField extends SelectField
15
{
16
17
    /**
18
     * List of items to mark as checked, and may not be unchecked
19
     *
20
     * @var array
21
     */
22
    protected $defaultItems = array();
23
24
    protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_MULTISELECT;
25
26
    /**
27
     * Extracts the value of this field, normalised as an array.
28
     * Scalar values will return a single length array, even if empty
29
     *
30
     * @return array List of values as an array
31
     */
32
    public function getValueArray()
33
    {
34
        return $this->getListValues($this->Value());
35
    }
36
37
    /**
38
     * Default selections, regardless of the {@link setValue()} settings.
39
     * Note: Items marked as disabled through {@link setDisabledItems()} can still be
40
     * selected by default through this method.
41
     *
42
     * @param array $items Collection of array keys, as defined in the $source array
43
     * @return $this Self reference
44
     */
45
    public function setDefaultItems($items)
46
    {
47
        $this->defaultItems = $this->getListValues($items);
48
        return $this;
49
    }
50
51
    /**
52
     * Default selections, regardless of the {@link setValue()} settings.
53
     *
54
     * @return array
55
     */
56
    public function getDefaultItems()
57
    {
58
        return $this->defaultItems;
59
    }
60
61
    /**
62
     * Load a value into this MultiSelectField
63
     *
64
     * @param mixed $value
65
     * @param null|array|DataObject $obj {@see Form::loadDataFrom}
66
     * @return $this
67
     */
68
    public function setValue($value, $obj = null)
69
    {
70
        // If we're not passed a value directly,
71
        // we can look for it in a relation method on the object passed as a second arg
72
        if ($obj instanceof DataObject) {
73
            $this->loadFrom($obj);
74
        } else {
75
            parent::setValue($value);
76
        }
77
        return $this;
78
    }
79
80
    /**
81
     * Load the value from the dataobject into this field
82
     *
83
     * @param DataObject|DataObjectInterface $record
84
     */
85
    public function loadFrom(DataObjectInterface $record)
86
    {
87
        $fieldName = $this->getName();
88
        if (empty($fieldName) || empty($record)) {
89
            return;
90
        }
91
92
        $relation = $record->hasMethod($fieldName)
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

92
        $relation = $record->/** @scrutinizer ignore-call */ hasMethod($fieldName)
Loading history...
93
            ? $record->$fieldName()
94
            : null;
95
96
        // Detect DB relation or field
97
        if ($relation instanceof Relation) {
98
            // Load ids from relation
99
            $value = array_values($relation->getIDList());
100
            parent::setValue($value);
101
        } elseif ($record->hasField($fieldName)) {
0 ignored issues
show
Bug introduced by
The method hasField() 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

101
        } elseif ($record->/** @scrutinizer ignore-call */ hasField($fieldName)) {
Loading history...
102
            // Load dataValue from field... a CSV for DBMultiEnum
103
            if ($record->obj($fieldName) instanceof DBMultiEnum) {
0 ignored issues
show
Bug introduced by
The method obj() 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

103
            if ($record->/** @scrutinizer ignore-call */ obj($fieldName) instanceof DBMultiEnum) {
Loading history...
104
                $value = $this->csvDecode($record->$fieldName);
105
106
            // ... JSON-encoded string for other fields
107
            } else {
108
                $value = $this->stringDecode($record->$fieldName);
109
            }
110
111
            parent::setValue($value);
112
        }
113
    }
114
115
116
    /**
117
     * Save the current value of this MultiSelectField into a DataObject.
118
     * If the field it is saving to is a has_many or many_many relationship,
119
     * it is saved by setByIDList(), otherwise it creates a comma separated
120
     * list for a standard DB text/varchar field.
121
     *
122
     * @param DataObject|DataObjectInterface $record The record to save into
123
     */
124
    public function saveInto(DataObjectInterface $record)
125
    {
126
        $fieldName = $this->getName();
127
        if (empty($fieldName) || empty($record)) {
128
            return;
129
        }
130
131
        $relation = $record->hasMethod($fieldName)
132
            ? $record->$fieldName()
133
            : null;
134
135
        // Detect DB relation or field
136
        $items = $this->getValueArray();
137
        if ($relation instanceof Relation) {
138
            // Save ids into relation
139
            $relation->setByIDList($items);
140
        } elseif ($record->hasField($fieldName)) {
141
            // Save dataValue into field... a CSV for DBMultiEnum
142
            if ($record->obj($fieldName) instanceof DBMultiEnum) {
143
                $record->$fieldName = $this->csvEncode($items);
144
145
            // ... JSON-encoded string for other fields
146
            } else {
147
                $record->$fieldName = $this->stringEncode($items);
148
            }
149
        }
150
    }
151
152
    /**
153
     * Encode a list of values into a string, or null if empty (to simplify empty checks)
154
     *
155
     * @param array $value
156
     * @return string|null
157
     */
158
    public function stringEncode($value)
159
    {
160
        return $value
161
            ? json_encode(array_values($value))
162
            : null;
163
    }
164
165
    /**
166
     * Extract a string value into an array of values
167
     *
168
     * @param string $value
169
     * @return array
170
     */
171
    protected function stringDecode($value)
172
    {
173
        // Handle empty case
174
        if (empty($value)) {
175
            return array();
176
        }
177
178
        // If json deserialisation fails, then fallover to legacy format
179
        $result = json_decode($value, true);
180
        if ($result !== false) {
181
            return $result;
182
        }
183
184
        throw new \InvalidArgumentException("Invalid string encoded value for multi select field");
185
    }
186
187
    /**
188
     * Encode a list of values into a string as a comma separated list.
189
     * Commas will be stripped from the items passed in
190
     *
191
     * @param array $value
192
     * @return string|null
193
     */
194
    protected function csvEncode($value)
195
    {
196
        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...
197
            return null;
198
        }
199
        return implode(
200
            ',',
201
            array_map(
202
                function ($x) {
203
                    return str_replace(',', '', $x);
204
                },
205
                array_values($value)
206
            )
207
        );
208
    }
209
210
    /**
211
     * Decode a list of values from a comma separated string.
212
     * Spaces are trimmed
213
     *
214
     * @param string $value
215
     * @return array
216
     */
217
    protected function csvDecode($value)
218
    {
219
        if (!$value) {
220
            return [];
221
        }
222
223
        return preg_split('/ *, */', trim($value));
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_split('/ *, */', trim($value)) could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
224
    }
225
226
    /**
227
     * Validate this field
228
     *
229
     * @param Validator $validator
230
     * @return bool
231
     */
232
    public function validate($validator)
233
    {
234
        $values = $this->getValueArray();
235
        $validValues = $this->getValidValues();
236
237
        // Filter out selected values not in the data source
238
        $self = $this;
239
        $invalidValues = array_filter(
240
            $values,
241
            function ($userValue) use ($self, $validValues) {
242
                foreach ($validValues as $formValue) {
243
                    if ($self->isSelectedValue($formValue, $userValue)) {
244
                        return false;
245
                    }
246
                }
247
                return true;
248
            }
249
        );
250
        if (empty($invalidValues)) {
251
            return true;
252
        }
253
254
        // List invalid items
255
        $validator->validationError(
256
            $this->getName(),
257
            _t(
258
                'SilverStripe\\Forms\\MultiSelectField.SOURCE_VALIDATION',
259
                "Please select values within the list provided. Invalid option(s) {value} given",
260
                array('value' => implode(',', $invalidValues))
261
            ),
262
            "validation"
263
        );
264
        return false;
265
    }
266
267
    /**
268
     * Transforms the source data for this CheckboxSetField
269
     * into a comma separated list of values.
270
     *
271
     * @return ReadonlyField
272
     */
273
    public function performReadonlyTransformation()
274
    {
275
        $field = $this->castedCopy('SilverStripe\\Forms\\LookupField');
276
        $field->setSource($this->getSource());
277
        $field->setReadonly(true);
278
279
        return $field;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $field returns the type SilverStripe\Forms\LookupField which is incompatible with the documented return type SilverStripe\Forms\ReadonlyField.
Loading history...
280
    }
281
}
282