baril/orderly

Eloquent 模型可排序/有序行为。

v3.2.0 2024-05-21 16:50 UTC

This package is auto-updated.

Last update: 2024-09-21 17:26:33 UTC


README

此包为 Eloquent 模型添加可排序/有序行为。它受到 rutorika/sortable 的启发。最初它是 Smoothie 包的一部分。

版本兼容性

设置

新安装

如果你没有使用包发现,请在 config/app.php 文件中注册服务提供者

return [
    // ...
    'providers' => [
        Baril\Orderly\OrderlyServiceProvider::class,
        // ...
    ],
];

在你的表中添加一个列来存储位置。此列的默认名称为 position,但如果你想要的话可以使用另一个名称(见下文)。

public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        // ... other fields ...
        $table->unsignedInteger('position');
    });
}

然后,在你的模型中使用 \Baril\Orderly\Concerns\Orderable 特性。position 字段应该被保护,因为它不会手动填充。

class Article extends Model
{
    use \Baril\Orderly\Concerns\Orderable;

    protected $guarded = ['position'];
}

如果你想使用不同于 position 的名称,还需要设置 $orderColumn 属性

class Article extends Model
{
    use \Baril\Orderly\Concerns\Orderable;

    protected $orderColumn = 'order';
    protected $guarded = ['order'];
}

基本用法

你可以使用以下方法来更改模型的位置(无需之后保存,方法已经完成了)

  • moveToOffset($offset) ($offset 从 0 开始,可以是负数,例如 $offset = -1 是最后一个位置),
  • moveToStart(),
  • moveToEnd(),
  • moveToPosition($position) ($position 从 1 开始,必须是有效位置),
  • moveUp($positions = 1, $strict = true):将模型向上移动 $positions 位置($strict 参数控制当你尝试将模型移动“超出范围”时会发生什么:如果设置为 false,则模型将简单地移动到第一个或最后一个位置,否则将抛出 PositionException),
  • moveDown($positions = 1, $strict = true),
  • swapWith($anotherModel),
  • moveBefore($anotherModel),
  • moveAfter($anotherModel).
$model = Article::find(1);
$anotherModel = Article::find(10)
$model->moveAfter($anotherModel);
// $model is now positioned after $anotherModel, and both have been saved

此外,这个特性

  • 自动在 create 事件上定义模型的位置,因此你不需要手动设置 position
  • delete 事件上自动减少后续模型的位置,以确保没有“间隙”。
$article = new Article();
$article->title = $request->input('title');
$article->body = $request->input('body');
$article->save();

此模型的位置将是 MAX(position) + 1

要获取有序模型,请使用 ordered 范围

$articles = Article::ordered()->get();
$articles = Article::ordered('desc')->get();

(你可以通过调用 unordered 范围来取消此范围的效果。)

可以使用 previousnext 方法查询前一个和下一个模型

$entity = Article::find(10);
$entity->next(10); // returns a QueryBuilder on the next 10 entities, ordered
$entity->previous(5)->get(); // returns a collection with the previous 5 entities, in reverse order
$entity->next()->first(); // returns the next entity

大量重排序

上面描述的 move* 方法不适合大量重排序,因为

  • 它们将执行许多不必要的查询,
  • 更改模型的位置会影响到其他模型的位置,并且如果不小心可能会导致副作用。

示例

$models = Article::orderBy('publication_date', 'desc')->get();
$models->map(function($model, $key) {
    return $model->moveToOffset($key);
});

上面的示例代码会损坏数据,因为你需要每个模型在更改其位置之前都是“新鲜的”。另一方面,以下代码将正常工作

$collection = Article::orderBy('publication_date', 'desc')->get();
$collection->map(function($model, $key) {
    return $model->fresh()->moveToOffset($key);
});

尽管如此,这仍然不是最好的方法,因为它执行了许多不必要的查询。处理大量重排序的更好方法是使用集合上的 saveOrder 方法

$collection = Article::orderBy('publication_date', 'desc')->get();
// $collection is not a regular Eloquent collection object, it's a custom class
// with the following additional method:
$collection->saveOrder();

这就完成了!现在集合中项目的顺序已应用到数据库的 position 列中。

你还可以使用 setOrder 方法显式地对集合进行排序。它需要一个包含 ID 的数组作为参数

$ordered = $collection->setOrder([4, 5, 2]);

返回的集合已按顺序排列,使得具有 IDs 4、5 和 2 的项位于集合的开始处。此外,新的顺序会自动保存到数据库中(您不需要调用 saveOrder)。

⚠️ 注意:只有集合内的模型之间会重新排序/交换。表中的其他行保持不变。

您还可以使用 setOrder 方法,无论是静态地在模型上还是在查询构建器上。

// This will reorder all statuses (assuming there are 5 statuses in the table):
Status::setOrder([2, 1, 5, 3, 4]);

// This will put the status with id 4 at the beginning, and move the other
// statuses' positions accordingly:
Status::setOrder([4]);

// This will only swap the statuses 3, 4 and 5, and won't change the position
// of the other statuses:
Status::whereKey([3, 4, 5])->setOrder([4, 5, 3]);

使用这种方式时,setOrder 方法将返回受影响的行数。

可排序组/一对多关系

有时,表的数据会根据某些列进行“分组”,您需要分别对每个组进行排序,而不是全局排序。为了实现这一点,您只需设置 $groupColumn 属性

class Article extends Model
{
    use \Baril\Orderly\Concerns\Orderable;

    protected $guarded = ['position'];
    protected $groupColumn = 'section_id';
}

如果组由多个列定义,则可以使用数组

protected $groupColumn = ['field_name1', 'field_name2'];

可排序组可以用来处理可排序的一对多关系

class Section extends Model
{
    public function articles()
    {
        return $this->hasMany(Article::class)->ordered();
        // Chaining the ->ordered() method is optional here, but you can do
        // it if you want the relation ordered by default.
    }
}

class Article extends Model
{
    protected $groupColumn = 'section_id';
}

可排序多对多关系

如果您需要排序多对多关系,则需要在中继表中有一个 position 列(或另一个名称)。

让您的模型使用 \Baril\Orderly\Concerns\HasOrderableRelationships 特性

class Post extends Model
{
    use \Baril\Orderly\Concerns\HasOrderableRelationships;

    public function tags()
    {
        return $this->belongsToManyOrderable(Tag::class);
    }
}

belongsToManyOrderable 方法的原型与 belongsToMany 类似,增加了一个名为 $orderColumn 的第二个参数

public function belongsToManyOrderable(
        $related,
        $orderColumn = 'position',
        $table = null,
        $foreignPivotKey = null,
        $relatedPivotKey = null,
        $parentKey = null,
        $relatedKey = null,
        $relation = null)

现在,所有的 BelongsToMany 类的常规方法都将设置附加模型的正确位置

$post->tags()->attach($tag->id); // will attach $tag and give it the last position
$post->tags()->sync([$tag1->id, $tag2->id, $tag3->id]) // will keep the provided order
$post->tags()->detach($tag->id); // will decrement the position of subsequent $tags

您可以通过链式调用 ordered 方法来排序关系的查询结果

$orderedTags = $post->tags()->ordered()->get();
$tagsInReverseOrder = $post->tags()->ordered('desc')->get();

如果您希望关系默认排序,可以在关系定义中使用 belongsToManyOrdered 方法,而不是 belongsToManyOrderable

class Post extends Model
{
    use \Baril\Orderly\Concerns\HasOrderableRelationships;

    public function tags()
    {
        return $this->belongsToManyOrdered(Tag::class);
        // the line above is actually just a shortcut to:
        // return $this->belongsToManyOrderable(Tag::class)->ordered();
    }
}

在这种情况下,如果您偶尔想根据某个其他字段对相关模型进行排序,您需要先使用 unordered 范围

$post->tags; // ordered by position, because of the definition above
$post->tags()->ordered('desc')->get(); // reverse order
$post->tags()->unordered()->get(); // unordered

// Note that orderBy has no effect here since the tags are already ordered by position:
$post->tags()->orderBy('id')->get();

// This is the proper way to do it:
$post->tags()->unordered()->orderBy('id')->get();

BelongsToManyOrderable 类具有与 Orderable 特性相同的方法,但您需要传递一个相关 $model 来进行操作

  • moveToOffset($model, $offset),
  • moveToStart($model),
  • moveToEnd($model),
  • moveToPosition($model, $position),
  • moveUp($model, $positions = 1, $strict = true),
  • moveDown($model, $positions = 1, $strict = true),
  • swap($model, $anotherModel),
  • moveBefore($model, $anotherModel) ($model 将被移动到 $anotherModel 之前),
  • moveAfter($model, $anotherModel) ($model 将被移动到 $anotherModel 之后),
  • before($model) (类似于 Orderable 特性中的 previous 方法),
  • after($model) (类似于 next).
$tag1 = $article->tags()->ordered()->first();
$tag2 = $article->tags()->ordered()->last();
$article->tags()->moveBefore($tag1, $tag2);
// now $tag1 is at the second to last position

请注意,如果 $model 不属于该关系,则任何这些方法都会抛出 Baril\Orderly\GroupException

还有一个用于批量重新排序的方法

$article->tags()->setOrder([$id1, $id2, $id3]);

在上面的示例中,ID 为 $id1$id2$id3 的标签现在将位于文章的 tags 集合的开头。任何其他附加到文章的标签将按原来的顺序排列在后面。

可排序的 morph-to-many 关系

类似地,该包定义了一个 MorphToManyOrderable 类型的关系。 morphToManyOrderable 方法的第三个参数是排序列的名称(默认为 position

class Post extends Model
{
    use \Baril\Orderly\Concerns\HasOrderableRelationships;

    public function tags()
    {
        return $this->morphToManyOrderable('App\Tag', 'taggable', 'tag_order');
    }
}

morphedByManyOrderable 方法相同

class Tag extends Model
{
    use \Baril\Orderable\Concerns\HasOrderableRelationships;

    public function posts()
    {
        return $this->morphedByManyOrderable('App\Post', 'taggable', 'order');
    }

    public function videos()
    {
        return $this->morphedByManyOrderable('App\Video', 'taggable', 'order');
    }
}

Artisan 命令

orderly:fix-positions 命令将重新计算 position 列中的数据(例如,如果您已手动删除行并存在“间隙”)。

对于可排序模型

php artisan orderly:fix-positions "App\\YourModel"

对于可排序的多对多关系

php artisan orderly:fix-positions "App\\YourModel" relationName