Skip to content

Commit

Permalink
Merge pull request #7 from nevadskiy/dev
Browse files Browse the repository at this point in the history
Release 0.5.0
  • Loading branch information
xalaida authored Feb 9, 2023
2 parents 374ee2f + 44e5d8a commit bcf99b4
Show file tree
Hide file tree
Showing 19 changed files with 619 additions and 384 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Possibility to create model in the middle of the sequence
- Possibility to create model in the beginning of the sequence
- Extra argument for shift amount in `shiftToStart` and `shiftToEnd` methods
- Possibility to update positions without shifting other models

### Changed

- Rename method `getInitPosition` to `startPosition`
- Models now are shifted after the model update

## [0.4.1] - 2023-02-08

### Added

- Possibility to update `position` attribute along with other attributes

## [0.4.0] - 2022-05-08

### Added

- Laravel 9 support

## [0.3.0] - 2021-06-24

### Fixed

- Fix position query scoping for relations

## [0.2.0] - 2021-06-19

### Added

- Documentation
- `OrderByPosition` global scope
- Support for models delete
- `swap` method
- Add PHP 8 support

### Changed

- Rename `arrangeByIds` into `arrangeByKeys`
- Extract `arrangeByKeys` method into query builder
- Extract shift methods into query builder

## [0.1.0] - 2021-06-13

### Added

- Base ordering features
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,26 @@ Models simply have an integer `position` attribute corresponding to the model's
The `position` attribute is a kind of array index and is automatically inserted when a new model is created.
By default, the model takes a position at the very end of the sequence.
The initial position gets a `0` value by default. To change that, override the `getInitPosition` method in the model.
The starting position gets a `0` value by default. To change that, override the `startPosition` method in the model:
```php
public function getInitPosition(): int
public function startPosition(): int
{
return 0;
}
```
By default, the created model takes a position at the very end of the sequence. If you need to customize that behaviour, you can override the `nextPosition` method:
```php
public function nextPosition(): ?int
{
return $this->startPosition();
}
```
In that example, a new model will be created in the beginning of the sequence.
### Deleting models
When a model is deleted, the positions of other records in the sequence are updated automatically.
Expand Down Expand Up @@ -110,12 +119,12 @@ $category->update([
The positions of other models will be automatically recalculated as well.
#### Move
#### Shift / Move
You can also use the `move` method that sets a new position value and updates the model immediately:
You can also use the `shift method that sets a new position value and updates the model immediately:
```php
$category->move(3);
$category->shift(3);
```
#### Swap
Expand All @@ -126,6 +135,18 @@ The `swap` method swaps the position of two models.
$category->swap($anotherCategory);
```
#### Without shifting
By default, the package automatically updates the position of other models when the model position is updated.
If you want to update the model position without shifting the positions of other models, you can use the `withoutShifting` method:
```php
Category::withoutShifting(function () {
$category->move(5);
})
```
#### Arrange
It is also possible to arrange models by their IDs.
Expand Down
118 changes: 88 additions & 30 deletions src/HasPosition.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,58 @@
trait HasPosition
{
/**
* Boot the position trait.
* Indicates if the model should shift position of other models in the sequence.
*
* @var bool
*/
protected static $shiftPosition = true;

/**
* Boot the trait.
*/
public static function bootHasPosition(): void
{
static::addGlobalScope(new PositioningScope());

static::creating(static function (self $model) {
$model->assignPositionIfMissing();
$model->assignPosition();
});

static::updating(static function (self $model) {
if ($model->isDirty($model->getPositionColumn())) {
$model->shiftBeforeMove($model->getPosition(), $model->getOriginal($model->getPositionColumn()));
static::created(static function (self $model) {
if (static::shouldShiftPosition() && $model->isMoving()) {
$model->newPositionQuery()->whereKeyNot($model->getKey())->shiftToEnd($model->getPosition());
}
});

static::updated(static function (self $model) {
if (static::shouldShiftPosition() && $model->isMoving()) {
[$currentPosition, $previousPosition] = [$model->getPosition(), $model->getOriginal($model->getPositionColumn())];

if ($currentPosition < $previousPosition) {
$model->newPositionQuery()->whereKeyNot($model->getKey())->shiftToEnd($currentPosition, $previousPosition);
} elseif ($currentPosition > $previousPosition) {
$model->newPositionQuery()->whereKeyNot($model->getKey())->shiftToStart($previousPosition, $currentPosition);
}
}
});

static::deleted(static function (self $model) {
$model->newPositionQuery()->shiftToStart($model->getPosition());
if (static::shouldShiftPosition()) {
$model->newPositionQuery()->shiftToStart($model->getPosition());
}
});
}

/**
* Initialize the trait.
*/
public function initializeHasPosition(): void
{
$this->mergeCasts([
$this->getPositionColumn() => 'int',
]);
}

/**
* Get the name of the "position" column.
*/
Expand All @@ -44,7 +75,7 @@ public function getPositionColumn(): string
/**
* Get a value of the starting position.
*/
public function getInitPosition(): int
public function startPosition(): int
{
return 0;
}
Expand All @@ -57,6 +88,30 @@ public function alwaysOrderByPosition(): bool
return false;
}

/**
* Execute a callback without shifting position of models.
*/
public static function withoutShiftingPosition(callable $callback)
{
$shifting = static::$shiftPosition;

static::$shiftPosition = false;

$result = $callback();

static::$shiftPosition = $shifting;

return $result;
}

/**
* Determine if the model should shift position of other models in the sequence.
*/
public static function shouldShiftPosition(): bool
{
return static::$shiftPosition;
}

/**
* Get the position value of the model.
*/
Expand All @@ -68,7 +123,7 @@ public function getPosition(): ?int
/**
* Set the position to the given value.
*/
public function setPosition(int $position): Model
public function setPosition(?int $position): Model
{
return $this->setAttribute($this->getPositionColumn(), $position);
}
Expand Down Expand Up @@ -103,12 +158,20 @@ public function move(int $newPosition): bool
return $this->setPosition($newPosition)->save();
}

/**
* Determine if the model is currently moving to a new position.
*/
public function isMoving(): bool
{
return $this->isDirty($this->getPositionColumn());
}

/**
* Swap the model position with another model.
*/
public function swap(self $that): void
{
static::withoutEvents(function () use ($that) {
static::withoutShiftingPosition(function () use ($that) {
$thisPosition = $this->getPosition();
$thatPosition = $that->getPosition();

Expand All @@ -129,32 +192,39 @@ protected function newPositionQuery(): Builder
}

/**
* Assign the next position value to the model if it is missing.
* Assign the next position value to the model.
*/
protected function assignPositionIfMissing(): void
protected function assignPosition(): void
{
if (null === $this->getPosition()) {
$this->assignNextPosition();
if ($this->getPosition() === null) {
$this->setPosition($this->nextPosition());
}

if ($this->getPosition() === null) {
$this->setPosition($this->getEndPosition());

// Sync original attribute to not shift other models when the model will be created
$this->syncOriginalAttribute($this->getPositionColumn());
}
}

/**
* Assign the next position value to the model.
* Get the next position in the sequence for the model.
*/
protected function assignNextPosition(): Model
protected function nextPosition(): ?int
{
return $this->setPosition($this->getNextPosition());
return null;
}

/**
* Determine the next position value in the model sequence.
*/
protected function getNextPosition(): int
protected function getEndPosition(): int
{
$maxPosition = $this->getMaxPosition();

if (null === $maxPosition) {
return $this->getInitPosition();
return $this->startPosition();
}

return $maxPosition + 1;
Expand All @@ -167,16 +237,4 @@ protected function getMaxPosition(): ?int
{
return $this->newPositionQuery()->max($this->getPositionColumn());
}

/**
* Shift models in a sequence before the move to a new position.
*/
protected function shiftBeforeMove(int $newPosition, int $oldPosition): void
{
if ($newPosition < $oldPosition) {
$this->newPositionQuery()->shiftToEnd($newPosition, $oldPosition);
} elseif ($newPosition > $oldPosition) {
$this->newPositionQuery()->shiftToStart($oldPosition, $newPosition);
}
}
}
Loading

0 comments on commit bcf99b4

Please sign in to comment.