Passed
Push — 4 ( 24d652...bbb208 )
by Loz
43:59 queued 35:01
created

MultiSelectField::loadFrom()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 9
nop 1
dl 0
loc 27
rs 8.8333
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('/\s*,\s*/', trim($value));
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());
0 ignored issues
show
Bug introduced by
The method setSource() does not exist on SilverStripe\Forms\FormField. 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

276
        $field->/** @scrutinizer ignore-call */ 
277
                setSource($this->getSource());
Loading history...
277
        $field->setReadonly(true);
278
279
        return $field;
280
    }
281
}
282