Completed
Push — master ( 1f3908...281bd1 )
by Maxime
34s queued 18s
created

TreeMultiselectField::saveInto()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

117
                    '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...
118
                ];
119
            } else {
120
                $values[] = $item;
121
            }
122
        }
123
        $data['data']['valueObjects'] = $values;
124
125
        // cannot rely on $this->value as this could be a many-many relationship
126
        $value = array_column($values, 'id');
127
        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...
128
            sort($value);
129
            $data['value'] = $value;
130
        } else {
131
            $data['value'] = null;
132
        }
133
134
        return $data;
135
    }
136
137
    /**
138
     * Return this field's linked items
139
     * @return ArrayList|DataList $items
140
     */
141
    public function getItems()
142
    {
143
        $items = new ArrayList();
144
145
        // If the value has been set, use that
146
        if (count($this->Value())) {
147
            $sourceObject = $this->getSourceObject();
148
            if (is_array($sourceObject)) {
0 ignored issues
show
introduced by
The condition is_array($sourceObject) is always false.
Loading history...
149
                $values = $this->Value();
150
151
                foreach ($values as $value) {
152
                    $item = new stdClass;
153
                    $item->ID = $value;
154
                    $item->Title = $sourceObject[$value];
155
                    $items->push($item);
156
                }
157
                return $items;
158
            }
159
160
            // Otherwise, look data up from the linked relation
161
            foreach ($this->Value() as $id) {
162
                if (!is_numeric($id)) {
163
                    continue;
164
                }
165
                $item = DataObject::get_by_id($sourceObject, $id);
166
                if ($item) {
167
                    $items->push($item);
168
                }
169
            }
170
            return $items;
171
        }
172
173
        if ($this->form) {
174
            $fieldName = $this->name;
175
            $record = $this->form->getRecord();
176
            if (is_object($record) && $record->hasMethod($fieldName)) {
177
                return $record->$fieldName();
178
            }
179
        }
180
181
        return $items;
182
    }
183
184
    /**
185
     * We overwrite the field attribute to add our hidden fields, as this
186
     * formfield can contain multiple values.
187
     *
188
     * @param array $properties
189
     * @return DBHTMLText
190
     */
191
    public function Field($properties = array())
192
    {
193
        $value = '';
194
        $titleArray = array();
195
        $idArray = array();
196
        $items = $this->getItems();
197
        $emptyTitle = _t('SilverStripe\\Forms\\DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
198
199
        if ($items && count($items)) {
200
            foreach ($items as $item) {
201
                $idArray[] = $item->ID;
202
                $titleArray[] = ($item instanceof ViewableData)
203
                    ? $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

203
                    ? $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...
204
                    : 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

204
                    : 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...
205
            }
206
207
            $title = implode(", ", $titleArray);
208
            sort($idArray);
209
            $value = implode(",", $idArray);
210
        } else {
211
            $title = $emptyTitle;
212
        }
213
214
        $dataUrlTree = '';
215
        if ($this->form) {
216
            $dataUrlTree = $this->Link('tree');
217
            if (!empty($idArray)) {
218
                $dataUrlTree = Controller::join_links($dataUrlTree, '?forceValue=' . implode(',', $idArray));
219
            }
220
        }
221
        $properties = array_merge(
222
            $properties,
223
            array(
224
                'Title' => $title,
225
                'EmptyTitle' => $emptyTitle,
226
                'Link' => $dataUrlTree,
227
                'Value' => $value
228
            )
229
        );
230
        return FormField::Field($properties);
231
    }
232
233
    /**
234
     * Save the results into the form
235
     * Calls function $record->onChange($items) before saving to the assummed
236
     * Component set.
237
     *
238
     * @param DataObjectInterface $record
239
     */
240
    public function saveInto(DataObjectInterface $record)
241
    {
242
        $items = $this->Value();
243
        $fieldName = $this->name;
244
        $saveDest = $record->$fieldName();
245
246
        if (!$saveDest) {
247
            $recordClass = get_class($record);
248
            user_error(
249
                "TreeMultiselectField::saveInto() Field '$fieldName' not found on"
250
                . " {$recordClass}.{$record->ID}",
0 ignored issues
show
Bug Best Practice introduced by
The property ID does not exist on SilverStripe\ORM\DataObjectInterface. Since you implemented __get, consider adding a @property annotation.
Loading history...
251
                E_USER_ERROR
252
            );
253
        }
254
255
        // Allows you to modify the items on your object before save
256
        $funcName = "onChange$fieldName";
257
        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

257
        if ($record->/** @scrutinizer ignore-call */ hasMethod($funcName)) {
Loading history...
258
            $result = $record->$funcName($items);
259
            if (!$result) {
260
                return;
261
            }
262
        }
263
264
        $saveDest->setByIDList($items);
265
    }
266
267
    /**
268
     * Changes this field to the readonly field.
269
     */
270
    public function performReadonlyTransformation()
271
    {
272
        /** @var TreeMultiselectField_Readonly $copy */
273
        $copy = $this->castedCopy(TreeMultiselectField_Readonly::class);
274
        $copy->setKeyField($this->getKeyField());
275
        $copy->setLabelField($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

275
        $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...
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

275
        /** @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...
276
        $copy->setSourceObject($this->getSourceObject());
277
        $copy->setTitleField($this->getTitleField());
278
        return $copy;
279
    }
280
}
281