Completed
Push — master ( 3509e4...df4deb )
by Pavel
03:03
created

SortableBehavior::moveToPosition()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * Behavior for Yii2 to support sorting in ActiveRecord models
4
 *
5
 * @link https://github.com/inblank/yii2-sortable
6
 * @copyright Copyright (c) 2016 Pavel Aleksandrov <[email protected]>
7
 * @license http://opensource.org/licenses/MIT
8
 */
9
namespace inblank\sortable;
10
11
use Yii;
12
use yii\base\Behavior;
13
use yii\db\ActiveRecord;
14
use yii\db\Expression;
15
use yii\db\QueryBuilder;
16
17
/**
18
 * Behavior for Yii2 to support sorting in ActiveRecord models
19
 *
20
 * @property ActiveRecord $owner
21
 */
22
class SortableBehavior extends Behavior
23
{
24
    /**
25
     * Attribute to store the sort order of records
26
     * @var string
27
     */
28
    public $sortAttribute = 'sort';
29
30
    /**
31
     * The list of attributes used as sorting conditions of the records.
32
     * null mean no condition and sort all records
33
     * One attribute can be define as string.
34
     * @var array|string
35
     */
36
    public $conditionAttributes = null;
37
38
    /**
39
     * @inheritdoc
40
     */
41 8
    public function events()
42
    {
43
        return [
44 8
            ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
45 8
            ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate',
46 8
            ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
47 8
        ];
48
    }
49
50
    /** Before validate event */
51 3
    public function beforeValidate()
52
    {
53 3
        if ($this->owner->getIsNewRecord()) {
54
            // added new record always on top
55 1
            $this->owner->{$this->sortAttribute} = $this->owner->find()->andWhere($this->_buildCondition())->count();
56 1
        } else {
57
            // prevent direct change of sort field
58 2
            $this->owner->{$this->sortAttribute} = $this->owner->getOldAttribute($this->sortAttribute);
59
        }
60 3
    }
61
62
    /** After update event */
63 2
    public function afterUpdate($event)
64
    {
65 2
        $oldCondition = [];
66 2
        $currentCondition = [];
67
        /** @var ActiveRecord $owner */
68 2
        $owner = $this->owner;
69 2
        foreach ((array)$this->conditionAttributes as $attribute) {
70 2
            if (!$owner->hasAttribute($attribute)) {
71 1
                continue;
72
            }
73 2
            $oldCondition[$attribute] = $currentCondition[$attribute] = $owner->getAttribute($attribute);
74 2
            if (array_key_exists($attribute, $event->changedAttributes)) {
75 1
                $oldCondition[$attribute] = $event->changedAttributes[$attribute];
76 1
            }
77 2
        }
78 2
        if (array_diff($currentCondition, $oldCondition) != []) {
79
            // condition was changed
80
            /** @var ActiveRecord $changedModel */
81 1
            $changedModel = Yii::createObject($owner->className());
82 1
            $changedModel->setAttributes($oldCondition, false);
83
            // recalculate old model range
84 1
            $changedModel->recalculateSort();
85
            // move model to top in new model range
86 1
            $owner->updateAttributes(['sort' => $owner->find()->andWhere($this->_buildCondition())->count() - 1]);
87 1
        }
88 2
    }
89
90
    /** After delete event */
91 1
    public function afterDelete()
92
    {
93 1
        $owner = $this->owner;
94 1
        $condition = $this->_buildCondition();
95 1
        $condition[] = ['>', 'sort', $owner->{$this->sortAttribute}];
96 1
        $owner->updateAllCounters([$this->sortAttribute => -1], $condition);
97 1
    }
98
99
    /**
100
     * Move record to the specific sorting position
101
     * @param int $position new sorting position
102
     */
103 4
    public function moveToPosition($position)
104
    {
105 4
        $this->sortChange($position - $this->owner->{$this->sortAttribute});
106 4
    }
107
108
    /**
109
     * Move record to the top of sorting order
110
     */
111 1
    public function moveToTop()
112
    {
113 1
        $this->moveToPosition(PHP_INT_MAX);
114 1
    }
115
116
    /**
117
     * Move record to the bottom of sorting order
118
     */
119 2
    public function moveToBottom()
120
    {
121 2
        $this->moveToPosition(0);
122 2
    }
123
124
    /**
125
     * Move records
126
     * @param int $value
127
     */
128 5
    public function sortChange($value)
129
    {
130 5
        if ($value == 0) {
131 1
            return;
132
        }
133 5
        $owner = $this->owner;
134 5
        $condition = $this->_buildCondition();
135 5
        $newSort = $owner->{$this->sortAttribute} + $value;
136 5
        if ($value > 0) {
137
            // move up
138 4
            $max = $this->owner->find()->andWhere($condition)->count() - 1;
139 4
            if ($owner->{$this->sortAttribute} === $max) {
140 1
                return;
141
            }
142 3
            if ($newSort >= $max) {
143 2
                $newSort = $max;
144 2
            }
145 3
            $condition[] = [">", 'sort', $owner->{$this->sortAttribute}];
146 3
            $condition[] = ["<=", 'sort', $newSort];
147 3
            $counterChanger = -1;
148 3
        } else {
149
            // move down
150 4
            if ($owner->{$this->sortAttribute} === 0) {
151 1
                return;
152
            }
153 4
            if ($newSort < 0) {
154 1
                $newSort = 0;
155 1
            }
156 4
            $condition[] = ['<', 'sort', $owner->{$this->sortAttribute}];
157 4
            $condition[] = ['>=', 'sort', $newSort];
158 4
            $counterChanger = 1;
159
        }
160 5
        $owner->updateAllCounters([$this->sortAttribute => $counterChanger], $condition);
161 5
        $owner->updateAttributes(['sort' => $newSort]);
162 5
    }
163
164
    /**
165
     * Recalculate sorting
166
     */
167 2
    public function recalculateSort()
168
    {
169 2
        $owner = $this->owner;
170 2
        $db = $this->owner->getDb();
171 2
        $builder = new QueryBuilder($db);
172
173 2
        $orderFields = ['sort' => 'asc'];
174 2
        foreach ($owner->primaryKey() as $field) {
175 2
            if ($field != 'sort') {
176 2
                $orderFields[$field] = 'asc';
177 2
            }
178 2
        }
179
        // recalculate sort
180 2
        $query = $builder->update(
181 2
                $owner->tableName(),
182 2
                [$this->sortAttribute => new Expression('(@sortingCount:=(@sortingCount+1))')],
183 2
                $this->_buildCondition(),
184
                $params
185 2
            ) . ' ' . $builder->buildOrderBy($orderFields);
186 2
        $db->createCommand('set @sortingCount=-1;' . $query, $params)->execute();
187
        // update in current record
188 2
        if (!$owner->getIsNewRecord()) {
189 1
            $owner->{$this->sortAttribute} = $owner->findOne($owner->getPrimaryKey())->{$this->sortAttribute};
190 1
        }
191 2
    }
192
193
    /**
194
     * Build WHERE condition for sort change query
195
     * @return array
196
     */
197 8
    protected function _buildCondition()
198
    {
199 8
        $condition = ['and'];
200 8
        foreach ((array)$this->conditionAttributes as $attribute) {
201 8
            if ($this->owner->hasAttribute($attribute)) {
202 8
                $condition[] = [$attribute => $this->owner->$attribute];
203 8
            }
204 8
        }
205 8
        return $condition;
206
    }
207
208
}
209