Passed
Push — 4.1 ( fecedc...90a506 )
by
unknown
09:42
created

TreeMultiselectField::objectForKey()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 23
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 3
nop 1
dl 0
loc 23
rs 10
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->value = 'unchanged';
70
    }
71
72
    public function getSchemaDataDefaults()
73
    {
74
        $data = parent::getSchemaDataDefaults();
75
76
        $data['data'] = array_merge($data['data'], [
77
            'hasEmptyDefault' => false,
78
            'multiple' => true,
79
        ]);
80
        return $data;
81
    }
82
83
    public function getSchemaStateDefaults()
84
    {
85
        $data = parent::getSchemaStateDefaults();
86
        unset($data['data']['valueObject']);
87
88
        $items = $this->getItems();
89
        $values = [];
90
        foreach ($items as $item) {
91
            if ($item instanceof DataObject) {
92
                $values[] = [
93
                    'id' => $item->obj($this->getKeyField())->getValue(),
94
                    'title' => $item->obj($this->getTitleField())->getValue(),
95
                    '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...
96
                    '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...5.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

96
                    '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...
97
                ];
98
            } else {
99
                $values[] = $item;
100
            }
101
        }
102
        $data['data']['valueObjects'] = $values;
103
104
        // cannot rely on $this->value as this could be a many-many relationship
105
        $value = array_column($values, 'id');
106
        $data['value'] = ($value) ? $value : 'unchanged';
107
108
        return $data;
109
    }
110
111
    /**
112
     * Return this field's linked items
113
     * @return ArrayList|DataList $items
114
     */
115
    public function getItems()
116
    {
117
        $items = new ArrayList();
118
119
        // If the value has been set, use that
120
        if ($this->value != 'unchanged') {
121
            $sourceObject = $this->getSourceObject();
122
            if (is_array($sourceObject)) {
0 ignored issues
show
introduced by
The condition is_array($sourceObject) is always false.
Loading history...
123
                $values = is_array($this->value) ? $this->value : preg_split('/ *, */', trim($this->value));
124
125
                foreach ($values as $value) {
126
                    $item = new stdClass;
127
                    $item->ID = $value;
128
                    $item->Title = $sourceObject[$value];
129
                    $items->push($item);
130
                }
131
                return $items;
132
            }
133
134
            // Otherwise, look data up from the linked relation
135
            if (is_string($this->value)) {
136
                $ids = explode(',', $this->value);
137
                foreach ($ids as $id) {
138
                    if (!is_numeric($id)) {
139
                        continue;
140
                    }
141
                    $item = DataObject::get_by_id($sourceObject, $id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of SilverStripe\ORM\DataObject::get_by_id(). ( Ignorable by Annotation )

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

141
                    $item = DataObject::get_by_id($sourceObject, /** @scrutinizer ignore-type */ $id);
Loading history...
142
                    if ($item) {
143
                        $items->push($item);
144
                    }
145
                }
146
                return $items;
147
            }
148
        }
149
150
        if ($this->form) {
151
            $fieldName = $this->name;
152
            $record = $this->form->getRecord();
153
            if (is_object($record) && $record->hasMethod($fieldName)) {
154
                return $record->$fieldName();
155
            }
156
        }
157
158
        return $items;
159
    }
160
161
    /**
162
     * We overwrite the field attribute to add our hidden fields, as this
163
     * formfield can contain multiple values.
164
     *
165
     * @param array $properties
166
     * @return DBHTMLText
167
     */
168
    public function Field($properties = array())
169
    {
170
        $value = '';
171
        $titleArray = array();
172
        $idArray = array();
173
        $items = $this->getItems();
174
        $emptyTitle = _t('SilverStripe\\Forms\\DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
175
176
        if ($items && count($items)) {
177
            foreach ($items as $item) {
178
                $idArray[] = $item->ID;
179
                $titleArray[] = ($item instanceof ViewableData)
180
                    ? $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...5.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

180
                    ? $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...
181
                    : 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...5.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

181
                    : 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...
182
            }
183
184
            $title = implode(", ", $titleArray);
185
            $value = implode(",", $idArray);
186
        } else {
187
            $title = $emptyTitle;
188
        }
189
190
        $dataUrlTree = '';
191
        if ($this->form) {
192
            $dataUrlTree = $this->Link('tree');
193
            if (!empty($idArray)) {
194
                $dataUrlTree = Controller::join_links($dataUrlTree, '?forceValue=' . implode(',', $idArray));
195
            }
196
        }
197
        $properties = array_merge(
198
            $properties,
199
            array(
200
                'Title' => $title,
201
                'EmptyTitle' => $emptyTitle,
202
                'Link' => $dataUrlTree,
203
                'Value' => $value
204
            )
205
        );
206
        return FormField::Field($properties);
207
    }
208
209
    /**
210
     * Save the results into the form
211
     * Calls function $record->onChange($items) before saving to the assummed
212
     * Component set.
213
     *
214
     * @param DataObjectInterface $record
215
     */
216
    public function saveInto(DataObjectInterface $record)
217
    {
218
        $items = [];
219
        $fieldName = $this->name;
220
        $saveDest = $record->$fieldName();
221
222
        if (!$saveDest) {
223
            $recordClass = get_class($record);
224
            user_error(
225
                "TreeMultiselectField::saveInto() Field '$fieldName' not found on"
226
                . " {$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...
227
                E_USER_ERROR
228
            );
229
        }
230
231
        // Detect whether this field has actually been updated
232
        if ($this->value !== 'unchanged') {
233
            if (is_array($this->value)) {
234
                $items = $this->value;
235
            } elseif ($this->value) {
236
                $items = preg_split("/ *, */", trim($this->value));
237
            }
238
        }
239
240
        // Allows you to modify the items on your object before save
241
        $funcName = "onChange$fieldName";
242
        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

242
        if ($record->/** @scrutinizer ignore-call */ hasMethod($funcName)) {
Loading history...
243
            $result = $record->$funcName($items);
244
            if (!$result) {
245
                return;
246
            }
247
        }
248
        $saveDest->setByIDList($items);
249
    }
250
251
    /**
252
     * Changes this field to the readonly field.
253
     */
254
    public function performReadonlyTransformation()
255
    {
256
        /** @var TreeMultiselectField_Readonly $copy */
257
        $copy = $this->castedCopy(TreeMultiselectField_Readonly::class);
258
        $copy->setKeyField($this->getKeyField());
259
        $copy->setLabelField($this->getLabelField());
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Forms\TreeD...nField::setLabelField() has been deprecated: 4.0...5.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

259
        /** @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...5.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

259
        $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...
260
        $copy->setSourceObject($this->getSourceObject());
261
        $copy->setTitleField($this->getTitleField());
262
        return $copy;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     *
268
     * @internal To be removed in 5.0
269
     */
270
    protected function objectForKey($key)
271
    {
272
        /**
273
         * Fixes https://github.com/silverstripe/silverstripe-framework/issues/8332
274
         *
275
         * Due to historic reasons, the default (empty) value for this field is 'unchanged', even though
276
         * the field is usually integer on the database side.
277
         * MySQL handles that gracefully and returns an empty result in that case,
278
         * whereas some other databases (e.g. PostgreSQL) do not support comparison
279
         * of numeric types with string values, issuing a database error.
280
         *
281
         * This fix is not ideal, but supposed to keep backward compatibility for SS4.
282
         *
283
         * In 5.0 this method to be removed and NULL should be used instead of 'unchanged' (or an empty array. to be decided).
284
         * In 5.0 this class to be refactored so that $this->value is always an array of values (or null)
285
         */
286
        if ($this->getKeyField() === 'ID' && $key === 'unchanged') {
287
            $key = null;
288
        } elseif (is_string($key)) {
289
            $key = preg_split('/\s*,\s*/', trim($key));
290
        }
291
292
        return parent::objectForKey($key);
293
    }
294
}
295