Passed
Pull Request — master (#53)
by Roman
02:31
created

SortableUploadField::sortManyManyRelation()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 3
nop 5
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
1
<?php
2
3
namespace Bummzack\SortableFile\Forms;
4
5
use Psr\Log\LoggerInterface;
6
use SilverStripe\AssetAdmin\Forms\UploadField;
7
use SilverStripe\Assets\File;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\ORM\DataList;
10
use SilverStripe\ORM\DataObjectInterface;
11
use SilverStripe\ORM\DB;
12
use SilverStripe\ORM\ManyManyList;
13
use SilverStripe\ORM\ManyManyThroughList;
14
use SilverStripe\ORM\ManyManyThroughQueryManipulator;
15
use SilverStripe\ORM\Queries\SQLUpdate;
16
use SilverStripe\ORM\Sortable;
17
use SilverStripe\ORM\SS_List;
18
use SilverStripe\ORM\UnsavedRelationList;
19
20
/**
21
 * Extension of the UploadField to add sorting of files
22
 *
23
 * @author bummzack
24
 * @skipUpgrade
25
 */
26
class SortableUploadField extends UploadField
27
{
28
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
29
        'logger' => '%$Psr\Log\LoggerInterface',
30
    ];
31
32
    /**
33
     * The column to be used for sorting
34
     * @var string
35
     */
36
    protected $sortColumn = 'SortOrder';
37
38
    /**
39
     * Raw submitted form data
40
     * @var null|array
41
     */
42
    protected $rawSubmittal = null;
43
44
    /**
45
     * @var LoggerInterface
46
     */
47
    public $logger;
48
49
    public function getSchemaDataDefaults()
50
    {
51
        $defaults = parent::getSchemaDataDefaults();
52
        // Add a sortable prop for the react component
53
        $defaults['sortable'] = true;
54
        return $defaults;
55
    }
56
57
    /**
58
     * Set the column to be used for sorting
59
     * @param string $sortColumn
60
     * @return $this
61
     */
62
    public function setSortColumn($sortColumn)
63
    {
64
        $this->sortColumn = $sortColumn;
65
        return $this;
66
    }
67
68
    /**
69
     * Returns the column to be used for sorting
70
     * @return string
71
     */
72
    public function getSortColumn()
73
    {
74
        return $this->sortColumn;
75
    }
76
77
    /**
78
     * Return the files in sorted order
79
     * @return File[]|SS_List
80
     */
81
    public function getItems()
82
    {
83
        $items = parent::getItems();
84
85
        // An ArrayList won't contain our sort-column, thus it has to be sorted by the raw submittal data.
86
        // This is an issue that's seemingly exclusive to saving SiteConfig.
87
        if (($items instanceof ArrayList) && !empty($this->rawSubmittal)) {
88
            // flip the array, so that we can look up index by ID
89
            $sortLookup = array_flip($this->rawSubmittal);
90
            $itemsArray = $items->toArray();
91
            usort($itemsArray, function ($itemA, $itemB) use ($sortLookup) {
92
                if (isset($sortLookup[$itemA->ID]) && isset($sortLookup[$itemB->ID])) {
93
                    return $sortLookup[$itemA->ID] - $sortLookup[$itemB->ID];
94
                }
95
                return 0;
96
            });
97
98
            return ArrayList::create($itemsArray);
99
        }
100
101
        if ($items instanceof Sortable) {
102
            return $items->sort([$this->getSortColumn() => 'ASC', 'ID' => 'ASC']);
0 ignored issues
show
Unused Code introduced by
The call to SilverStripe\ORM\Sortable::sort() has too many arguments starting with array($this->getSortColu...> 'ASC', 'ID' => 'ASC'). ( Ignorable by Annotation )

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

102
            return $items->/** @scrutinizer ignore-call */ sort([$this->getSortColumn() => 'ASC', 'ID' => 'ASC']);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
103
        }
104
105
        return $items;
106
    }
107
108
    public function saveInto(DataObjectInterface $record)
109
    {
110
        parent::saveInto($record);
111
112
        // Check required relation details are available
113
        $fieldname = $this->getName();
114
        if (!$fieldname || !is_array($this->rawSubmittal)) {
115
            return $this;
116
        }
117
118
        // Check type of relation
119
        $relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null;
120
        if ($relation) {
121
            $idList = $this->getItemIDs();
122
            $rawList = $this->rawSubmittal;
123
            $sortColumn = $this->getSortColumn();
124
125
            if ($relation instanceof ManyManyList) {
126
                try {
127
                    // Apply the sorting, wrapped in a transaction.
128
                    // If something goes wrong, the DB will not contain invalid data
129
                    DB::get_conn()->withTransaction(function () use ($relation, $idList, $rawList, $record, $sortColumn) {
130
                        $this->sortManyManyRelation($relation, $idList, $rawList, $record, $sortColumn);
131
                    });
132
                } catch (\Exception $ex) {
133
                    $this->logger->warning('Unable to sort files in sortable relation.', ['exception' => $ex]);
134
                }
135
            } elseif ($relation instanceof ManyManyThroughList) {
136
                try {
137
                    // Apply the sorting, wrapped in a transaction.
138
                    // If something goes wrong, the DB will not contain invalid data
139
                    DB::get_conn()->withTransaction(function () use ($relation, $idList, $rawList, $sortColumn) {
140
                        $this->sortManyManyThroughRelation($relation, $idList, $rawList, $sortColumn);
141
                    });
142
                } catch (\Exception $ex) {
143
                    $this->logger->warning('Unable to sort files in sortable relation.', ['exception' => $ex]);
144
                }
145
            } elseif ($relation instanceof UnsavedRelationList) {
146
                // With an unsaved relation list the items can just be removed and re-added
147
                $sort = 0;
148
                $relation->removeAll();
149
                foreach ($rawList as $id) {
150
                    if (in_array($id, $idList)) {
151
                        $relation->add($id, [$sortColumn => $sort++]);
152
                    }
153
                }
154
            }
155
        }
156
157
        return $this;
158
    }
159
160
    public function setSubmittedValue($value, $data = null)
161
    {
162
        // Intercept the incoming IDs since they are properly sorted
163
        if (is_array($value) && isset($value['Files'])) {
164
            $this->rawSubmittal = $value['Files'];
165
        }
166
        return $this->setValue($value, $data);
167
    }
168
169
    /**
170
     * Apply sorting to a many_many relation
171
     * @param ManyManyList $relation
172
     * @param array $idList
173
     * @param array $rawList
174
     * @param DataObjectInterface $record
175
     * @param $sortColumn
176
     */
177
    protected function sortManyManyRelation(
178
        ManyManyList $relation,
179
        array $idList,
180
        array $rawList,
181
        DataObjectInterface $record,
182
        $sortColumn
183
    ) {
184
        $relation->getForeignID();
185
        $ownerIdField = $relation->getForeignKey();
186
        $fileIdField = $relation->getLocalKey();
187
        $joinTable = '"' . $relation->getJoinTable() . '"';
188
        $sort = 0;
189
        foreach ($rawList as $id) {
190
            if (in_array($id, $idList)) {
191
                // Use SQLUpdate to update the data in the join-table.
192
                // This is safe to do, since new records have already been written to the DB in the
193
                // parent::saveInto call.
194
                SQLUpdate::create($joinTable)
195
                    ->setWhere([
196
                        "\"$ownerIdField\" = ?" => $record->ID,
197
                        "\"$fileIdField\" = ?" => $id
198
                    ])
199
                    ->assign($sortColumn, $sort++)
200
                    ->execute();
201
            }
202
        }
203
    }
204
205
    /**
206
     * Apply sorting to a many_many_through relation
207
     * @param ManyManyThroughList $relation
208
     * @param array $idList
209
     * @param array $rawList
210
     * @param $sortColumn
211
     * @throws \SilverStripe\ORM\ValidationException
212
     */
213
    protected function sortManyManyThroughRelation(
214
        ManyManyThroughList $relation,
215
        array $idList,
216
        array $rawList,
217
        $sortColumn
218
    ) {
219
        $relation->getForeignID();
220
        $dataQuery = $relation->dataQuery();
221
        $manipulators = $dataQuery->getDataQueryManipulators();
222
        $manyManyManipulator = null;
223
        foreach ($manipulators as $manipulator) {
224
            if ($manipulator instanceof ManyManyThroughQueryManipulator) {
225
                $manyManyManipulator = $manipulator;
226
            }
227
        }
228
        $joinClass = $manyManyManipulator->getJoinClass();
0 ignored issues
show
Bug introduced by
The method getJoinClass() does not exist on null. ( Ignorable by Annotation )

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

228
        /** @scrutinizer ignore-call */ 
229
        $joinClass = $manyManyManipulator->getJoinClass();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
229
        $ownerIDField = $manyManyManipulator->getForeignKey();
230
        $fileIdField = $manyManyManipulator->getLocalKey();
231
232
        $sort = 0;
233
        foreach ($rawList as $id) {
234
            if (in_array($id, $idList)) {
235
                $fileRecord = DataList::create($joinClass)->filter([
236
                    $ownerIDField => $relation->getForeignID(),
237
                    $fileIdField  => $id
238
                ])->first();
239
240
                if ($fileRecord) {
241
                    $fileRecord->setField($sortColumn, $sort++);
242
                    $fileRecord->write();
243
                }
244
            }
245
        }
246
    }
247
}
248