Completed
Push — master ( eeafb3...652cd5 )
by Nate
02:25 queued 01:01
created

SortableTrait::sequentialOrder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 0
cts 21
cp 0
rs 9.552
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 6
1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://github.com/flipboxfactory/craft-ember/blob/master/LICENSE
6
 * @link       https://github.com/flipboxfactory/craft-ember/
7
 */
8
9
namespace flipbox\craft\ember\records;
10
11
use Craft;
12
use flipbox\craft\ember\helpers\SortOrderHelper;
13
use yii\db\ActiveQuery;
14
15
/**
16
 * This class bootstraps record sorting, ensuring a sequential order is upheld.  There are a couple key concepts to
17
 * consider:
18
 *
19
 * Target Attribute - The attribute used as the anchor.  For example, if your table consists of users and categories
20
 * and you need to order users per category, the target would be the category column.  User's would be sorted
21
 * per category.
22
 *
23
 * Sort Attribute - By default this will likely be 'sortOrder', but it's possible to name it something else like
24
 * 'userOrder'.
25
 *
26
 * Sort Order Condition - A query condition used to accurately identify the records in a sort order. For example,
27
 * if your table consists of users and categories and the sort order is ordering users per category, the condition
28
 * would look like:
29
 *
30
 * ```
31
 * [
32
 *      'userId' => $this->userId
33
 * ]
34
 * ```
35
 * Additionally, some sort orders may be site specific, therefore also passing a 'siteId' condition would only apply the
36
 * re-ordering to the specified site.
37
 *
38
 * ### Usage Examples
39
 *
40
 * public function beforeSave($insert)
41
 * {
42
 *      $this->ensureSortOrder(
43
 *           [
44
 *               'userId' => $this->userId
45
 *           ],
46
 *            'userOrder' // overriding the default 'sortOrder'
47
 *        );
48
 *
49
 *      return parent::beforeSave($insert);
50
 * }
51
 *
52
 * public function afterSave($insert, $changedAttributes)
53
 * {
54
 *      $this->autoReOrder(
55
 *          'categoryId',
56
 *           [
57
 *               'userId' => $this->userId
58
 *           ],
59
 *            'userOrder' // overriding the default 'sortOrder'
60
 *        );
61
 *
62
 *      parent::afterSave($insert, $changedAttributes);
63
 * }
64
 *
65
 * public function afterDelete()
66
 * {
67
 *      $this->sequentialOrder(
68
 *          'categoryId',
69
 *           [
70
 *               'userId' => $this->userId
71
 *           ],
72
 *            'userOrder' // overriding the default 'sortOrder'
73
 *        );
74
 *
75
 *      parent::afterDelete();
76
 * }
77
 *
78
 * @author Flipbox Factory <[email protected]>
79
 * @since 2.0.0
80
 */
81
trait SortableTrait
82
{
83
    use ActiveRecordTrait;
84
85
    /**
86
     * Returns the table name
87
     *
88
     * @return string
89
     */
90
    abstract public static function tableName();
91
92
    /**
93
     * @inheritdoc
94
     *
95
     * @return ActiveQuery
96
     */
97
    abstract public static function find();
98
99
    /**
100
     * Ensure a sort order is set.  If a sort order is not provided, it will be added to the end.
101
     *
102
     * @param array $sortOrderCondition
103
     * @param string $sortOrderAttribute
104
     */
105
    protected function ensureSortOrder(
106
        array $sortOrderCondition = [],
107
        string $sortOrderAttribute = 'sortOrder'
108
    ) {
109
        if ($this->getAttribute($sortOrderAttribute) === null) {
110
            $this->setAttribute(
111
                $sortOrderAttribute,
112
                $this->nextSortOrder(
113
                    $sortOrderCondition,
114
                    $sortOrderAttribute
115
                )
116
            );
117
        }
118
    }
119
120
    /**
121
     * Ensure all sort order's following this record are in sequential order. As an
122
     * example, a record may update the sort order from '4' to '1' which would result in all records after
123
     * this one to be altered in sequential order.
124
     *
125
     * @param string $targetAttribute
126
     * @param array $sortOrderCondition
127
     * @param string $sortOrderAttribute
128
     * @throws \yii\db\Exception
129
     */
130
    protected function sequentialOrder(
131
        string $targetAttribute,
132
        array $sortOrderCondition = [],
133
        string $sortOrderAttribute = 'sortOrder'
134
    ) {
135
        // All records (sorted)
136
        $sortOrder = $this->sortOrderQuery($sortOrderCondition, $sortOrderAttribute)
137
            ->indexBy($targetAttribute)
138
            ->select([$sortOrderAttribute])
139
            ->column();
140
141
        if (count($sortOrder) > 0) {
142
            $this->saveNewOrder(
143
                array_flip(array_combine(
144
                    range($sortOrder, count($sortOrder)),
145
                    array_keys($sortOrder)
146
                )),
147
                $targetAttribute,
148
                $sortOrderCondition,
149
                $sortOrderAttribute
150
            );
151
        }
152
    }
153
154
    /**
155
     * Ensure all sort order's following this record are in sequential order. As an
156
     * example, a record may update the sort order from '4' to '1' which would result in all records after
157
     * this one to be altered in sequential order.
158
     *
159
     * @param string $targetAttribute
160
     * @param array $sortOrderCondition
161
     * @param string $sortOrderAttribute
162
     * @throws \yii\db\Exception
163
     */
164
    protected function autoReOrder(
165
        string $targetAttribute,
166
        array $sortOrderCondition = [],
167
        string $sortOrderAttribute = 'sortOrder'
168
    ) {
169
        // All records (sorted)
170
        $sortOrder = $this->sortOrderQuery($sortOrderCondition, $sortOrderAttribute)
171
            ->indexBy($targetAttribute)
172
            ->select([$sortOrderAttribute])
173
            ->column();
174
175
        $affectedItems = SortOrderHelper::insertSequential(
176
            $sortOrder,
177
            $this->getAttribute($targetAttribute),
178
            $this->{$sortOrderAttribute}
179
        );
180
181
        if (empty($affectedItems) || is_bool($affectedItems)) {
182
            return;
183
        }
184
185
        $this->saveNewOrder(
186
            $affectedItems,
187
            $targetAttribute,
188
            $sortOrderCondition,
189
            $sortOrderAttribute
190
        );
191
    }
192
193
    /**
194
     * Get the next available sort order available
195
     *
196
     * @param array $sortOrderCondition
197
     * @param string $sortOrderAttribute
198
     * @return int
199
     */
200
    protected function nextSortOrder(
201
        array $sortOrderCondition = [],
202
        string $sortOrderAttribute = 'sortOrder'
203
    ): int {
204
        $maxSortOrder = $this->sortOrderQuery(
205
            $sortOrderCondition,
206
            $sortOrderAttribute
207
        )->max($sortOrderAttribute);
208
209
        return ++$maxSortOrder;
210
    }
211
212
213
    /**
214
     * Creates a sort order query which will display all siblings ordered by their sort order
215
     *
216
     * @param array $sortOrderCondition
217
     * @param string $sortOrderAttribute
218
     * @return ActiveQuery
219
     */
220
    protected function sortOrderQuery(
221
        array $sortOrderCondition = [],
222
        string $sortOrderAttribute = 'sortOrder'
223
    ): ActiveQuery {
224
        return static::find()
225
            ->andWhere($sortOrderCondition)
226
            ->orderBy([
227
                $sortOrderAttribute => SORT_ASC,
228
                'dateUpdated' => SORT_DESC
229
            ]);
230
    }
231
232
    /**
233
     * Saves a new sort order.
234
     *
235
     * @param array $sortOrder The new sort order that needs to be saved.  The 'key' represents the target value and
236
     * the 'value' represent the sort order.
237
     * @param string $targetAttribute The target attribute that the new order is keyed on.
238
     * @param array $sortOrderCondition Additional condition params used to accurately identify the sort order that
239
     * need to be changed.  For example, some sort orders may be site specific, therefore passing a 'siteId' condition
240
     * would only apply the re-ordering to the specified site.
241
     * @param string $sortOrderAttribute The sort order attribute that needs to be updated
242
     * @return bool
243
     * @throws \yii\db\Exception
244
     */
245
    protected function saveNewOrder(
246
        array $sortOrder,
247
        string $targetAttribute,
248
        array $sortOrderCondition = [],
249
        string $sortOrderAttribute = 'sortOrder'
250
    ): bool {
251
        foreach ($sortOrder as $target => $order) {
252
            Craft::$app->getDb()->createCommand()
253
                ->update(
254
                    static::tableName(),
255
                    [$sortOrderAttribute => $order],
256
                    array_merge(
257
                        $sortOrderCondition,
258
                        [
259
                            $targetAttribute => $target
260
                        ]
261
                    )
262
                )
263
                ->execute();
264
        }
265
266
        return true;
267
    }
268
}
269