Positionable::updateSiblingsPosition()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 14
nc 1
nop 3
dl 0
loc 25
ccs 15
cts 15
cp 1
crap 1
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
namespace roaresearch\yii2\formgenerator\behaviors;
4
5
use yii\base\InvalidConfigException;
6
use yii\db\ActiveQuery;
7
use yii\db\ActiveRecord;
8
use yii\db\Expression as DbExpression;
9
use yii\validators\Validator;
10
11
/**
12
 * Handles position for a record and its siblings determined by a common
13
 * `$parentAttribute`.
14
 *
15
 * @property ActiveRecord $owner
16
 */
17
class Positionable extends \yii\base\Behavior
18
{
19
    /**
20
     * @var string attribute used to determine all the sibling records.
21
     */
22
    public $parentAttribute;
23
24
    /**
25
     * @var string attribute which will store the position among siblings.
26
     */
27
    public $positionAttribute = 'position';
28
29
    /**
30
     * @var bool whether to attach validators to the `$owner` before validation.
31
     */
32
    public $attachValidators = true;
33
34
    /**
35
     * @var DbExpression sentence used to increase the position by 1.
36
     */
37
    protected $positionIncrease;
38
39
    /**
40
     * @var DbExpression sentence used to decrease the position by 1.
41
     */
42
    protected $positionDecrease;
43
44
    /**
45
     * @inheritdoc
46
     */
47 16
    public function attach($owner)
48
    {
49 16
        if (null === $this->parentAttribute) {
50 1
            throw new InvalidConfigException(
51 1
                static::class . '::$parentAttribute must be set.'
52
            );
53
        }
54 16
        if (!$owner instanceof ActiveRecord) {
55 1
            throw new InvalidConfigException(
56 1
                static::class . '::$owner must extend ' .  ActiveRecord::class
57
            );
58
        }
59 16
        if (!$owner->hasAttribute($this->parentAttribute)) {
60 1
            throw new InvalidConfigException(
61 1
                get_class($owner) . '::$'
62 1
                . $this->parentAttribute
63 1
                . ' is not an attribute.'
64
            );
65
        }
66 16
        if (!$owner->hasAttribute($this->positionAttribute)) {
67 1
            throw new InvalidConfigException(
68 1
                get_class($owner) . '::$'
69 1
                . $this->positionAttribute
70 1
                . ' is not an attribute.'
71
            );
72
        }
73 15
        parent::attach($owner);
74 15
        $this->positionIncrease = new DbExpression(
75 15
            $this->positionAttribute . ' + 1'
76
        );
77 15
        $this->positionDecrease = new DbExpression(
78 15
            $this->positionAttribute . ' - 1'
79
        );
80 15
    }
81
82
    /**
83
     * @inheritdoc
84
     */
85 15
    public function events()
86
    {
87
        return [
88 15
            ActiveRecord::EVENT_BEFORE_VALIDATE => 'attachValidators',
89
            ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert',
90
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
91
            ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
92
        ];
93
    }
94
95
    /**
96
     * Attaches validators to the `$owner`.
97
     */
98 6
    public function attachValidators()
99
    {
100 6
        if (!$this->attachValidators) {
101 2
            return;
102
        }
103 4
        $this->owner->validators[] = Validator::createValidator(
104 4
            'default',
105 4
            $this->owner,
106 4
            $this->positionAttribute,
107
            [
108 4
                'when' => function () {
109 4
                    return !$this->owner->hasErrors($this->parentAttribute);
110 4
                },
111 4
                'value' => function () {
112 2
                    return $this->getSiblings()->max('position') + 1;
113 4
                }
114
            ]
115
        );
116 4
        $this->owner->validators[] = Validator::createValidator(
117 4
            'integer',
118 4
            $this->owner,
119 4
            $this->positionAttribute,
120 4
            ['min' => 1]
121
        );
122 4
    }
123
124
    /**
125
     * @return ActiveQuery
126
     */
127 2
    public function getSiblings(): ActiveQuery
128
    {
129 2
        return $this->owner->hasMany(get_class($this->owner), [
130 2
            $this->parentAttribute => $this->parentAttribute
131
        ]);
132
    }
133
134
    /**
135
     * Increases the position by 1 of siblings with equal or bigger position as
136
     * the new record.
137
     */
138 2
    public function beforeInsert()
139
    {
140 2
        $this->increaseSiblingsPosition([
141 2
            '>=',
142 2
            $this->positionAttribute,
143 2
            $this->owner->getAttribute($this->positionAttribute),
144
        ]);
145 2
    }
146
147
    /**
148
     * Forbids updating `$parentAttribute` on the `$owner` record and reorganize
149
     * position among siblings when changing a record position.
150
     */
151 2
    public function beforeUpdate()
152
    {
153 2
        if ($this->owner->isAttributeChanged($this->parentAttribute)) {
154
            throw new \yii\base\NotSupportedException(
155
                get_class($this->owner)
156
                . '::$' . $this->parentAttribute
157
                . ' is not editable.'
158
            );
159
        }
160 2
        if ($this->owner->isAttributeChanged($this->positionAttribute)) {
161 1
            $attribute = $this->positionAttribute;
162 1
            $newPosition = $this->owner->getAttribute($attribute);
163 1
            $oldPosition = $this->owner->getOldAttribute($attribute);
164 1
            $this->updateSiblingsPosition(0, [$attribute => $oldPosition]);
165 1
            if ($newPosition < $oldPosition) {
166 1
                $this->increaseSiblingsPosition(
167 1
                    ['between', $attribute, $newPosition, $oldPosition]
168
                );
169
            } else {
170
                $this->decreaseSiblingsPosition(
171
                    ['between', $attribute, $oldPosition, $newPosition]
172
                );
173
            }
174
        }
175 2
    }
176
177
    /**
178
     * Decreases the position by 1 of siblings with equal or bigger position as
179
     * the deleted record.
180
     */
181 2
    public function afterDelete()
182
    {
183 2
        $this->decreaseSiblingsPosition([
184 2
            '>',
185 2
            $this->positionAttribute,
186 2
            $this->owner->getAttribute($this->positionAttribute),
187
        ]);
188 2
    }
189
190
    /**
191
     * Update the position of siblings on the database.
192
     * @param integer|DbExpression $position the new position.
193
     * @param array $condition the extra condition to update siblings.
194
     * @return int the number of updated siblings.
195
     */
196 5
    protected function updateSiblingsPosition(
197
        $position,
198
        array $condition,
199
        array $orderBy = []
200
    ): int {
201 5
        $params = [];
202 5
        $queryBuilder = $this->owner->getDb()->getQueryBuilder();
203 5
        return $this->owner->getDb()->createCommand(
204 5
            $queryBuilder->update(
205 5
                $this->owner->tableName(),
206 5
                [$this->positionAttribute => $position],
207
                [
208 5
                    'and',
209
                    [
210 5
                        $this->parentAttribute => $this->owner->getAttribute(
211 5
                            $this->parentAttribute
212
                        )
213
                    ],
214 5
                    $condition,
215
                ],
216 5
                $params // params
217
            )
218 5
            . ' ' . $queryBuilder->buildOrderBy($orderBy),
219 5
            $params
220 5
        )->execute();
221
    }
222
223
    /**
224
     * Increases the position of siblings by 1.
225
     *
226
     * @param array $conditoin the extra condition to update siblings.
227
     * @return int the number of updated siblings.
228
     */
229 3
    protected function increaseSiblingsPosition(array $condition): int
230
    {
231 3
        return $this->updateSiblingsPosition(
232 3
            $this->positionIncrease,
233 3
            $condition,
234 3
            [$this->positionAttribute => SORT_DESC]
235
        );
236
    }
237
238
    /**
239
     * Decreases the position of siblings by 1.
240
     *
241
     * @param array $conditoin the extra condition to update siblings.
242
     * @return int the number of updated siblings.
243
     */
244 2
    protected function decreaseSiblingsPosition(array $condition): int
245
    {
246 2
        return $this->updateSiblingsPosition(
247 2
            $this->positionDecrease,
248 2
            $condition,
249 2
            [$this->positionAttribute => SORT_ASC]
250
        );
251
    }
252
}
253