baril / orderly
Eloquent 模型可排序/有序行为。
Requires
- illuminate/cache: ^8.0|^9.0|^10.0|^11.0
- illuminate/console: ^8.0|^9.0|^10.0|^11.0
- illuminate/database: ^8.0|^9.0|^10.0|^11.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0
Requires (Dev)
- laravel/legacy-factories: ^1.3.2
- orchestra/testbench: ^6.23|^7.0|^8.0|^9.0
- squizlabs/php_codesniffer: ^2.8
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
范围来取消此范围的效果。)
可以使用 previous
和 next
方法查询前一个和下一个模型
$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