Skip to content

Commit d972093

Browse files
authored
Feature Position Lock (#21)
Rework position lock * Update README.md
1 parent 8590e64 commit d972093

9 files changed

+159
-196
lines changed

README.md

+11-49
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public function getStartPosition(): int
8181
}
8282
```
8383

84-
By doing this, the first record will be assigned a position value of `1`.
84+
In this example, the first record will be assigned a position value of `1`.
8585

8686
#### Ordering
8787

@@ -177,18 +177,6 @@ The `swap` method swaps the position of two models. For example:
177177
$category->swap($anotherCategory);
178178
```
179179

180-
#### Without shifting
181-
182-
By default, the positions of other models are automatically shifted when the model position is updated.
183-
184-
If you want to change the model position without shifting the position of other models, you can use the `withoutShiftingPosition` method. For example:
185-
186-
```php
187-
Category::withoutShiftingPosition(function () {
188-
$category->move(5);
189-
})
190-
```
191-
192180
#### Arrange models
193181

194182
It is also possible to arrange models by their IDs.
@@ -214,50 +202,20 @@ public function groupPositionBy(): array
214202
}
215203
```
216204

217-
### Locking positions
205+
### Position lock
218206

219-
By default, when a model is created at the end of the sequence, an extra database query is executed to calculate the latest position in the sequence.
220-
Similarly, when a model is created at the beginning or any other position but the latest, additional database queries are needed to shift the positions of other models accordingly.
221-
In some cases, you may want to insert models without these additional queries associated with calculating and shifting positions.
222-
For such scenarios, you can use the `lockPositions` method, which disables all post-insert database queries and assigns positions to models using a specified locker.
223-
This can be particularly useful to speed up your tests.
224-
225-
By default, the positions are locked to the value returned by the `getStartPosition` method in your model:
207+
By default, when you change the position or group of a model, the `PositionObserver` automatically updates the positions of other models in the sequence by performing additional database queries. If you need to disable this behavior for any reason, you can do it like so:
226208

227209
```php
228-
Categogy::lockPositions();
210+
use Nevadskiy\Position\PositionObserver;
229211
230-
$category = Category::create();
231-
echo $category->position; // 0
212+
PositionObserver::lockFor(Category::class);
232213
233-
$category = Category::create();
234-
echo $category->position; // 0
214+
$category->update(['position' => 1]);
235215
236-
$category = Category::create();
237-
echo $category->position; // 0
216+
PositionObserver::unlockFor(Category::class);
238217
```
239218

240-
Alternatively, you can provide a callback function as the locker. For example:
241-
242-
```php
243-
Category::lockPositions(static function () {
244-
static $count = 0;
245-
246-
return $count++;
247-
});
248-
249-
$category = Category::create();
250-
echo $category->position; // 0
251-
252-
$category = Category::create();
253-
echo $category->position; // 1
254-
255-
$category = Category::create();
256-
echo $category->position; // 2
257-
```
258-
259-
In this example, the callback function is used to increment the position for each new model created, starting from `0`.
260-
261219
## 📑 Changelog
262220

263221
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
@@ -273,3 +231,7 @@ If you discover any security related issues, please [e-mail me](mailto:nevadskiy
273231
## 📜 License
274232

275233
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
234+
235+
## 🛠️ Todo List
236+
237+
- [ ] support `swap` for models from different groups

src/HasPosition.php

+17-28
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,6 @@
1010
*/
1111
trait HasPosition
1212
{
13-
use PositionLocker;
14-
15-
/**
16-
* Indicates if the model should shift position of other models in the sequence.
17-
*
18-
* @var bool
19-
*/
20-
protected static $shiftPosition = true;
21-
2213
/**
2314
* Indicates if the model was positioned at the end of the sequence during the current request lifecycle.
2415
*
@@ -113,23 +104,25 @@ public function scopeOrderByReversePosition(Builder $query): Builder
113104
/**
114105
* Move the model to the new position.
115106
*/
116-
public function move(int $newPosition): bool
107+
public function move(int $position): bool
117108
{
118-
$oldPosition = $this->getPosition();
109+
$originalPosition = $this->getPosition();
119110

120-
if ($oldPosition === $newPosition) {
111+
if ($originalPosition === $position) {
121112
return false;
122113
}
123114

124-
return $this->setPosition($newPosition)->save();
115+
$this->setPosition($position);
116+
117+
return $this->save();
125118
}
126119

127120
/**
128121
* Swap the model position with another model.
129122
*/
130123
public function swap(self $that): void
131124
{
132-
static::withoutShiftingPosition(function () use ($that) {
125+
static::withPositionLock(function () use ($that) {
133126
$thisPosition = $this->getPosition();
134127
$thatPosition = $that->getPosition();
135128

@@ -178,26 +171,22 @@ public function newOriginalPositionQuery(): Builder
178171
}
179172

180173
/**
181-
* Execute a callback without shifting positions of models.
174+
* Execute the callback with the position lock.
175+
*
176+
* @template TValue
177+
* @param callable(): TValue $callback
178+
* @return TValue
182179
*/
183-
public static function withoutShiftingPosition(callable $callback)
180+
public static function withPositionLock(callable $callback)
184181
{
185-
$shiftPosition = static::$shiftPosition;
186-
187-
static::$shiftPosition = false;
188-
189-
$result = $callback();
190-
191-
static::$shiftPosition = $shiftPosition;
192-
193-
return $result;
182+
return PositionObserver::withLockFor(static::class, $callback);
194183
}
195184

196185
/**
197-
* Determine if the model should shift positions of other models in the sequence.
186+
* Force the next position for the model.
198187
*/
199-
public static function shouldShiftPosition(): bool
188+
public static function forcePosition(?int $position): void
200189
{
201-
return static::$shiftPosition && is_null(static::$positionLocker);
190+
PositionObserver::forceFor(static::class, $position);
202191
}
203192
}

src/PositionLocker.php

-46
This file was deleted.

src/PositionObserver.php

+92-17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,83 @@
66

77
class PositionObserver
88
{
9+
/**
10+
* The list of models classes that should lock position.
11+
*
12+
* @var array
13+
*/
14+
protected static $lockFor = [];
15+
16+
/**
17+
* The list of models classes that should force position.
18+
*
19+
* @var array
20+
*/
21+
protected static $forceFor = [];
22+
23+
/**
24+
* Enable the position lock for the given model.
25+
*/
26+
public static function lockFor(string $model): void
27+
{
28+
static::$lockFor[$model] = true;
29+
}
30+
31+
/**
32+
* Disable the position lock for the given model.
33+
*/
34+
public static function unlockFor(string $model): void
35+
{
36+
unset(static::$lockFor[$model]);
37+
}
38+
39+
/**
40+
* Determine whether the position is locked for the given model.
41+
*
42+
* @param Model|string $model
43+
*/
44+
public static function isLockedFor($model): bool
45+
{
46+
$model = is_object($model) ? get_class($model) : $model;
47+
48+
return isset(static::$lockFor[$model]);
49+
}
50+
51+
/**
52+
* Force the position for the given model.
53+
*/
54+
public static function forceFor(string $model, ?int $position): void
55+
{
56+
static::$forceFor[$model] = $position;
57+
}
58+
59+
/**
60+
* Execute the callback with the position lock.
61+
*
62+
* @template TValue
63+
* @param Model|string $model
64+
* @param callable(): TValue $callback
65+
* @return TValue
66+
*/
67+
public static function withLockFor($model, callable $callback)
68+
{
69+
$model = is_object($model) ? get_class($model) : $model;
70+
71+
$isLocked = static::isLockedFor($model);
72+
73+
if (! $isLocked) {
74+
static::lockFor($model);
75+
}
76+
77+
$result = $callback();
78+
79+
if (! $isLocked) {
80+
static::unlockFor($model);
81+
}
82+
83+
return $result;
84+
}
85+
986
/**
1087
* Handle the "saving" event for the model.
1188
*
@@ -14,7 +91,9 @@ class PositionObserver
1491
public function saving(Model $model): void
1592
{
1693
$this->assignPosition($model);
94+
1795
$this->markAsTerminalPosition($model);
96+
1897
$this->normalizePosition($model);
1998
}
2099

@@ -51,33 +130,29 @@ protected function shouldSetPosition(Model $model): bool
51130
}
52131

53132
/**
54-
* Determine if the position group is changing for the model.
133+
* Get the next position for the model.
55134
*
56135
* @param Model|HasPosition $model
57136
*/
58-
protected function isGroupChanging(Model $model): bool
137+
protected function getNextPosition(Model $model): int
59138
{
60-
$groupPositionColumns = $model->groupPositionBy();
61-
62-
if (! $groupPositionColumns) {
63-
return false;
64-
}
65-
66-
return $model->isDirty($groupPositionColumns);
139+
return static::$forceFor[get_class($model)] ?? $model->getNextPosition();
67140
}
68141

69142
/**
70-
* Get the next position for the model.
143+
* Determine if the position group is changing for the model.
71144
*
72145
* @param Model|HasPosition $model
73146
*/
74-
protected function getNextPosition(Model $model): int
147+
protected function isGroupChanging(Model $model): bool
75148
{
76-
if ($model::positionLocker()) {
77-
return $model::positionLocker()($model);
149+
$groupPositionColumns = $model->groupPositionBy();
150+
151+
if (! $groupPositionColumns) {
152+
return false;
78153
}
79154

80-
return $model->getNextPosition();
155+
return $model->isDirty($groupPositionColumns);
81156
}
82157

83158
/**
@@ -117,7 +192,7 @@ protected function normalizePosition(Model $model): void
117192
*/
118193
public function created(Model $model): void
119194
{
120-
if (! $model::shouldShiftPosition()) {
195+
if (static::isLockedFor($model)) {
121196
return;
122197
}
123198

@@ -145,7 +220,7 @@ protected function handleAddToGroup(Model $model): void
145220
*/
146221
public function updated(Model $model): void
147222
{
148-
if (! $model::shouldShiftPosition()) {
223+
if (static::isLockedFor($model)) {
149224
return;
150225
}
151226

@@ -222,7 +297,7 @@ protected function handlePositionChange(Model $model): void
222297
*/
223298
public function deleted(Model $model): void
224299
{
225-
if (! $model::shouldShiftPosition()) {
300+
if (static::isLockedFor($model)) {
226301
return;
227302
}
228303

0 commit comments

Comments
 (0)