Completed
Push — master ( 59527d...93c6d6 )
by Nate
10:09 queued 08:52
created

SortableTrait::ignoreSortOrder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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
     * Whether sort order should be checked.
87
     *
88
     * @var bool
89
     */
90
    private $saveSortOrder = true;
91
92
    /**
93
     * @return static
94
     */
95
    public function enforceSortOrder(): self
96
    {
97
        $this->saveSortOrder = false;
98
        return $this;
99
    }
100
101
    /**
102
     * @return static
103
     */
104
    public function ignoreSortOrder(): self
105
    {
106
        $this->saveSortOrder = true;
107
        return $this;
108
    }
109
110
    /**
111
     * Returns the table name
112
     *
113
     * @return string
114
     */
115
    abstract public static function tableName();
116
117
    /**
118
     * @inheritdoc
119
     *
120
     * @return ActiveQuery
121
     */
122
    abstract public static function find();
123
124
    /**
125
     * Ensure a sort order is set.  If a sort order is not provided, it will be added to the end.
126
     *
127
     * @param array $sortOrderCondition
128
     * @param string $sortOrderAttribute
129
     */
130
    protected function ensureSortOrder(
131
        array $sortOrderCondition = [],
132
        string $sortOrderAttribute = 'sortOrder'
133
    ) {
134
        if (!$this->saveSortOrder) {
135
            return;
136
        }
137
138
        if ($this->getAttribute($sortOrderAttribute) === null) {
139
            $this->setAttribute(
140
                $sortOrderAttribute,
141
                $this->nextSortOrder(
142
                    $sortOrderCondition,
143
                    $sortOrderAttribute
144
                )
145
            );
146
        }
147
    }
148
149
    /**
150
     * Ensure all sort order's following this record are in sequential order. As an
151
     * example, a record may update the sort order from '4' to '1' which would result in all records after
152
     * this one to be altered in sequential order.
153
     *
154
     * @param string $targetAttribute
155
     * @param array $sortOrderCondition
156
     * @param string $sortOrderAttribute
157
     * @throws \yii\db\Exception
158
     */
159
    protected function sequentialOrder(
160
        string $targetAttribute,
161
        array $sortOrderCondition = [],
162
        string $sortOrderAttribute = 'sortOrder'
163
    ) {
164
        if (!$this->saveSortOrder) {
165
            return;
166
        }
167
168
        // All records (sorted)
169
        $sortOrder = $this->sortOrderQuery($sortOrderCondition, $sortOrderAttribute)
170
            ->indexBy($targetAttribute)
171
            ->select([$sortOrderAttribute])
172
            ->column();
173
174
        if (count($sortOrder) > 0) {
175
            $this->saveNewOrder(
176
                array_flip(array_combine(
177
                    range($sortOrder, count($sortOrder)),
178
                    array_keys($sortOrder)
179
                )),
180
                $targetAttribute,
181
                $sortOrderCondition,
182
                $sortOrderAttribute
183
            );
184
        }
185
    }
186
187
    /**
188
     * Ensure all sort order's following this record are in sequential order. As an
189
     * example, a record may update the sort order from '4' to '1' which would result in all records after
190
     * this one to be altered in sequential order.
191
     *
192
     * @param string $targetAttribute
193
     * @param array $sortOrderCondition
194
     * @param string $sortOrderAttribute
195
     * @throws \yii\db\Exception
196
     */
197
    protected function autoReOrder(
198
        string $targetAttribute,
199
        array $sortOrderCondition = [],
200
        string $sortOrderAttribute = 'sortOrder'
201
    ) {
202
        if (!$this->saveSortOrder) {
203
            return;
204
        }
205
206
        // All records (sorted)
207
        $sortOrder = $this->sortOrderQuery($sortOrderCondition, $sortOrderAttribute)
208
            ->indexBy($targetAttribute)
209
            ->select([$sortOrderAttribute])
210
            ->column();
211
212
        $affectedItems = SortOrderHelper::insertSequential(
213
            $sortOrder,
214
            $this->getAttribute($targetAttribute),
215
            $this->{$sortOrderAttribute}
216
        );
217
218
        if (empty($affectedItems) || is_bool($affectedItems)) {
219
            return;
220
        }
221
222
        $this->saveNewOrder(
223
            $affectedItems,
224
            $targetAttribute,
225
            $sortOrderCondition,
226
            $sortOrderAttribute
227
        );
228
    }
229
230
    /**
231
     * Get the next available sort order available
232
     *
233
     * @param array $sortOrderCondition
234
     * @param string $sortOrderAttribute
235
     * @return int
236
     */
237
    protected function nextSortOrder(
238
        array $sortOrderCondition = [],
239
        string $sortOrderAttribute = 'sortOrder'
240
    ): int {
241
        $maxSortOrder = $this->sortOrderQuery(
242
            $sortOrderCondition,
243
            $sortOrderAttribute
244
        )->max('[[' . $sortOrderAttribute . ']]');
245
246
        return ++$maxSortOrder;
247
    }
248
249
250
    /**
251
     * Creates a sort order query which will display all siblings ordered by their sort order
252
     *
253
     * @param array $sortOrderCondition
254
     * @param string $sortOrderAttribute
255
     * @return ActiveQuery
256
     */
257
    protected function sortOrderQuery(
258
        array $sortOrderCondition = [],
259
        string $sortOrderAttribute = 'sortOrder'
260
    ): ActiveQuery {
261
        return static::find()
262
            ->andWhere($sortOrderCondition)
263
            ->orderBy([
264
                $sortOrderAttribute => SORT_ASC,
265
                'dateUpdated' => SORT_DESC
266
            ]);
267
    }
268
269
    /**
270
     * Saves a new sort order.
271
     *
272
     * @param array $sortOrder The new sort order that needs to be saved.  The 'key' represents the target value and
273
     * the 'value' represent the sort order.
274
     * @param string $targetAttribute The target attribute that the new order is keyed on.
275
     * @param array $sortOrderCondition Additional condition params used to accurately identify the sort order that
276
     * need to be changed.  For example, some sort orders may be site specific, therefore passing a 'siteId' condition
277
     * would only apply the re-ordering to the specified site.
278
     * @param string $sortOrderAttribute The sort order attribute that needs to be updated
279
     * @return bool
280
     * @throws \yii\db\Exception
281
     */
282
    protected function saveNewOrder(
283
        array $sortOrder,
284
        string $targetAttribute,
285
        array $sortOrderCondition = [],
286
        string $sortOrderAttribute = 'sortOrder'
287
    ): bool {
288
        foreach ($sortOrder as $target => $order) {
289
            Craft::$app->getDb()->createCommand()
290
                ->update(
291
                    static::tableName(),
292
                    [$sortOrderAttribute => $order],
293
                    array_merge(
294
                        $sortOrderCondition,
295
                        [
296
                            $targetAttribute => $target
297
                        ]
298
                    )
299
                )
300
                ->execute();
301
        }
302
303
        return true;
304
    }
305
}
306