Passed
Push — 4.2 ( 96bd17...993971 )
by
unknown
10:29
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
        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...
107
            sort($value);
108
            $data['value'] = $value;
109
        } else {
110
            $data['value'] = 'unchanged';
111
        }
112
113
        return $data;
114
    }
115
116
    /**
117
     * Return this field's linked items
118
     * @return ArrayList|DataList $items
119
     */
120
    public function getItems()
121
    {
122
        $items = new ArrayList();
123
124
        // If the value has been set, use that
125
        if ($this->value != 'unchanged') {
126
            $sourceObject = $this->getSourceObject();
127
            if (is_array($sourceObject)) {
0 ignored issues
show
introduced by
The condition is_array($sourceObject) is always false.
Loading history...
128
                $values = is_array($this->value) ? $this->value : preg_split('/ *, */', trim($this->value));
129
130
                foreach ($values as $value) {
131
                    $item = new stdClass;
132
                    $item->ID = $value;
133
                    $item->Title = $sourceObject[$value];
134
                    $items->push($item);
135
                }
136
                return $items;
137
            }
138
139
            // Otherwise, look data up from the linked relation
140
            if (is_string($this->value)) {
141
                $ids = explode(',', $this->value);
142
                foreach ($ids as $id) {
143
                    if (!is_numeric($id)) {
144
                        continue;
145
                    }
146
                    $item = DataObject::get_by_id($sourceObject, $id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type boolean|integer expected by parameter $idOrCache 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

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

185
                    ? $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...
186
                    : 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

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

248
        if ($record->/** @scrutinizer ignore-call */ hasMethod($funcName)) {
Loading history...
249
            $result = $record->$funcName($items);
250
            if (!$result) {
251
                return;
252
            }
253
        }
254
        $saveDest->setByIDList($items);
255
    }
256
257
    /**
258
     * Changes this field to the readonly field.
259
     */
260
    public function performReadonlyTransformation()
261
    {
262
        /** @var TreeMultiselectField_Readonly $copy */
263
        $copy = $this->castedCopy(TreeMultiselectField_Readonly::class);
264
        $copy->setKeyField($this->getKeyField());
265
        $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

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

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