thunderwolf/sortable

基于 Propel sortable 行为的 Laravel Eloquent 排序组件

安装: 275

依赖者: 0

建议者: 0

安全: 0

星级: 0

分支: 0

类型:package

1.0.2 2024-08-25 22:25 UTC

This package is auto-updated.

Last update: 2024-09-25 20:46:37 UTC


README

排序行为允许模型成为有序列表,并提供多种方法高效地遍历此列表。

要使用它,需要在模型中使用 Sortable 特性 并配置,最简单的配置如下所示

    public static function sortable(): array
    {
        return [];
    }

Sortable 特性 将覆盖默认的 Builder,如下所示

    /**
     * Override -> Create a new Eloquent query builder for the model.
     * If you have more Behaviors using this kind on Override create own and use Trait SortableBuilderTrait
     *
     * @param  Builder  $query
     * @return SortableBuilder
     */
    public function newEloquentBuilder($query): SortableBuilder
    {
        return new SortableBuilder($query);
    }

如果您使用多个特性或自定义覆盖,请只使用 SortableBuilderTrait 特性,因为 SortableBuilder 类看起来如下

<?php

namespace Thunderwolf\EloquentSortable;

use Illuminate\Database\Eloquent\Builder;

class SortableBuilder extends Builder
{
    use SortableBuilderTrait;
}

基本用法

使用此包最基本的方法是创建一个使用 Sortable 特性 的模型,如下所示

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentSortable\Sortable;

class Task extends Model
{
    use Sortable;

    protected $table = 'tasks';

    protected $fillable = ['title'];

    public $timestamps = false;

    public static function sortable(): array
    {
        return [];
    }
}

注册 SortableServiceProvider 之后,您也可以使用蓝图通过 createSortable 辅助方法创建表,如下所示

$schema->create('tasks', function (Blueprint $table1) {
    $table1->increments('id');
    $table1->string('title');
    $table1->createSortable([]);
});

以类似的方式处理迁移。

现在,模型具有被插入到有序列表中的能力,如下所示

<?php
$t1 = new Task();
$t1->setAttribute('title', 'Wash the dishes');
$t1->save();
echo $t1->getSortableRank(); // 1, the first rank to be given (not 0)

$t2 = new Task();
$t2->setAttribute('title', 'Do the laundry');
$t2->save();
echo $t2->getSortableRank(); // 2

$t3 = new Task();
$t3->setAttribute('title', 'Rest a little');
$t3->save();
echo $t3->getSortableRank(); // 3

只要您保存新对象,Propel 就会自动将它们分配到列表中的第一个可用排名。

这些方法返回的通常是带有属性和相关模型的常规 Propel 模型对象。排序行为还向对象添加了检查方法

<?php
echo $t2->isFirst();         // false
echo $t2->isLast();          // false
echo $t2->getSortableRank(); // 2

一旦您构建了有序列表,您可以使用排序行为添加的任何方法遍历它。例如

<?php
$firstTask = Task::query()->findOneByRank(1); // $t1
$secondTask = $firstTask->getNext();          // $t2
$lastTask = $secondTask->getNext();           // $t3
$secondTask = $lastTask->getPrevious();       // $t2

$allTasks = Task::query()->findList();                                // => collection($t1, $t2, $t3)
$allTasksInReverseOrder = Task::query()->orderByRank('desc')->get();  // => collection($t3, $t2, $t2)

在列表中操作对象

您可以使用 moveUp()、moveDown()、moveToTop()、moveToBottom()、moveToRank() 和 swapWith() 方法中的任何一种移动列表中的对象。这些操作是即时的,不需要保存模型

<?php
$t1 = Task::query()->findOneByRank(1);
$t2 = Task::query()->findOneByRank(2);
// Initial list is: 1 - Wash the dishes, 2 - Do the laundry, 3 - Rest a little

$t2->moveToTop();    // will end with this order: 1 - Do the laundry,  2 - Wash the dishes, 3 - Rest a little
$t2->moveToBottom(); // will end with this order: 1 - Wash the dishes, 2 - Rest a little,   3 - Do the laundry
$t2->moveUp();       // will end with this order: 1 - Wash the dishes, 2 - Do the laundry,  3 - Rest a little
$t2->swapWith($t1);  // will end with this order: 1 - Do the laundry,  2 - Wash the dishes, 3 - Rest a little
$t2->moveToRank(3);  // will end with this order: 1 - Wash the dishes, 2 - Rest a little,   3 - Do the laundry
$t2->moveToRank(2);  // will end with this order: 1 - Wash the dishes, 2 - Do the laundry,  3 - Rest a little

默认情况下,新对象被添加到列表的底部。但是,您也可以使用 insertAtTop()、insertAtBottom() 和 insertAtRank() 方法将它们插入到特定位置。请注意,insertAtXXX 方法不会保存对象

<?php
// Initial list is: 1 - Wash the dishes, 2 - Do the laundry, 3 - Rest a little

$t4 = new Task();
$t4->setAttribute('title', 'Clean windows');
$t4->insertAtRank(2);
$t4->save();  // The list is now  1 - Wash the dishes, 2 - Clean Windows, 3 - Do the laundry, 4 - Rest a little

每次您删除() 对象时,排名都会重新排列以填补空缺

<?php
$t4->delete();
// The list is now 1 - Wash the dishes, 2 - Do the laundry, 3 - Rest a little

提示

您可以通过调用 removeFromList() 从列表中删除对象而无需删除它。不要忘记之后调用 save() 以便重新排列列表中的其他对象以填补空缺。

多个列表

当您需要为单个模型存储多个列表时(例如,每个用户一个任务列表) - 使用每个列表的 Scope。这需要在行为定义中启用 Scope 支持,将 use_scope 参数设置为 true 并设置 scope_columns,如下面的示例所示。创建具有 Sortable 特性 和如下配置的模型

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Thunderwolf\EloquentSortable\Sortable;

class SingleScopedTask extends Model
{
    use Sortable;

    protected $table = 'single-scoped-tasks';

    protected $fillable = ['title'];

    public $timestamps = false;

    public static function sortable(): array
    {
        return ['use_scope' => true, 'scope_columns' => ['single_scoped_user_id']];
    }

    /**
     * When planning to use `Inserting Related Models` way of set user id we need to override setter - we are using 8.x
     *
     * @param $value
     * @return void
     */
    public function setSingleScopedUserIdAttribute($value)
    {
        $this->oldScope['single_scoped_user_id'] = $this->attributes['single_scoped_user_id']??null;
        $this->attributes['single_scoped_user_id'] = $value;
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(SingleScopedUser::class, 'single_scoped_user_id');
    }
}

在这个示例中,我们还在使用 SingleScopedUser 模型,如下所示

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class SingleScopedUser extends Model
{
    protected $table = 'single-scoped-users';

    protected $fillable = ['username'];

    public $timestamps = false;

    public function tasks(): HasMany
    {
        return $this->hasMany(SingleScopedTask::class);
    }
}

注册 SortableServiceProvider 之后,您也可以使用蓝图通过 createSortable 辅助方法创建表,如下所示

$schema->create('single-scoped-tasks', function (Blueprint $table2) {
    $table2->increments('id');
    $table2->unsignedInteger('single_scoped_user_id');
    $table2->string('title');
    $table2->createSortable(['use_scope' => true, 'scope_columns' => ['single_scoped_user_id']]);
});

$schema->create('single-scoped-users', function (Blueprint $table3) {
    $table3->increments('id');
    $table3->string('username');
});

以类似的方式处理迁移。

在上面的示例中,您可以有您需要的任意多个列表。

您可以直接设置 Scope,如下所示

<?php
// We are assuming this table have those 2 records:
$paul = SingleScopedUser::query()->find(1); // paul
$john = SingleScopedUser::query()->find(2); // john

$t1 = new SingleScopedTask();
$t1->setAttribute('title', 'Wash the dishes');
$t1->setSortableScope('single_scoped_user_id', $paul->getKey());
$t1->save();
echo $t1->getSortableRank(); // 1

$t2 = new SingleScopedTask();
$t2->setAttribute('title', 'Do the laundry');
$t2->setSortableScope('single_scoped_user_id', $paul->getKey());
$t2->save();
echo $t2->getSortableRank(); // 2

$t3 = new SingleScopedTask();
$t3->setAttribute('title', 'Rest a little');
$t3->setSortableScope('single_scoped_user_id', $john->getKey());
$t3->save();
echo $t3->getSortableRank(); // 1, because John has his own task list

或者您可以使用关系,如下所示

// We are assuming this table have those 2 records:
$paul = SingleScopedUser::query()->find(1); // paul
$john = SingleScopedUser::query()->find(2); // john

$t1 = new SingleScopedTask(['title' => 'Wash the dishes']);
$paul->tasks()->save($t1);
echo $t1->getSortableRank(); // 1

$t2 = new SingleScopedTask(['title' => 'Do the laundry']);
$paul->tasks()->save($t2);
echo $t2->getSortableRank(); // 2

$t3 = new SingleScopedTask(['title' => 'Rest a little']);
$john->tasks()->save($t3);
echo $t3->getSortableRank(); // 1, because John has his own task list

现在生成的方法接受一个 $scope 参数,以将查询限制在给定的 Scope 中

<?php
$firstPaulTask = SingleScopedTask::query()->findOneByRank(1, ['single_scoped_user_id' => $paul->getKey()]); // $t1
$lastPaulTask = $firstPaulTask->getNext();                                                                  // $t2
$firstJohnTask = SingleScopedTask::query()->findOneByRank(1, ['single_scoped_user_id' => $john->getKey()]); // $t3

使用排序行为和 Scope 的模型还受益于一个名为 inList() 的额外 Builder 方法

<?php
$allPaulsTasks = SingleScopedTask::query()->inList(['single_scoped_user_id' => $paul->getKey()])->get();
// => collection($t1, $t2)

多列 Scope

我们可以使用多个列作为 Scope。

要实现这一点,只需创建具有 Sortable 特性 和如下配置的模型

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Thunderwolf\EloquentSortable\Sortable;

class MultiScopedTask extends Model
{
    use Sortable;

    protected $table = 'multi-scoped-tasks';

    protected $fillable = ['title'];

    public $timestamps = false;

    public static function sortable(): array
    {
        return ['use_scope' => true, 'scope_columns' => ['multi_scoped_user_id', 'multi_scoped_group_id']];
    }

    /**
     * When planning to use `Inserting Related Models` way of set user id we need to override setter - we are using 8.x
     *
     * @param $value
     * @return void
     */
    public function setMultiScopedUserIdAttribute($value)
    {
        $this->oldScope['multi_scoped_user_id'] = $this->attributes['multi_scoped_user_id']??null;
        $this->attributes['multi_scoped_user_id'] = $value;
    }

    /**
     * When planning to use `Inserting Related Models` way of set user id we need to override setter - we are using 8.x
     *
     * @param $value
     * @return void
     */
    public function setMultiScopedGroupIdAttribute($value)
    {
        $this->oldScope['multi_scoped_group_id'] = $this->attributes['multi_scoped_group_id']??null;
        $this->attributes['multi_scoped_group_id'] = $value;
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(MultiScopedUser::class, 'multi_scoped_user_id');
    }

    public function group(): BelongsTo
    {
        return $this->belongsTo(MultiScopedGroup::class, 'multi_scoped_group_id');
    }

}

在这个示例中,我们还在使用 MultiScopedUser 模型,如下所示

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class MultiScopedUser extends Model
{
    protected $table = 'multi-scoped-users';

    protected $fillable = ['username'];

    public $timestamps = false;

    public function tasks(): HasMany
    {
        return $this->hasMany(MultiScopedTask::class);
    }
}

和一个 MultiScopedGroup 模型,如下所示

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class MultiScopedGroup extends Model
{
    protected $table = 'multi-scoped-groups';

    protected $fillable = ['name'];

    public $timestamps = false;

    public function tasks(): HasMany
    {
        return $this->hasMany(MultiScopedTask::class);
    }
}

注册 SortableServiceProvider 之后,您也可以使用蓝图通过 createSortable 辅助方法创建表,如下所示

$schema->create('multi-scoped-tasks', function (Blueprint $table4) {
    $table4->increments('id');
    $table4->unsignedInteger('multi_scoped_user_id');
    $table4->unsignedInteger('multi_scoped_group_id');
    $table4->string('title');
    $table4->createSortable(['use_scope' => true, 'scope_columns' => ['multi_scoped_user_id', 'multi_scoped_group_id']]);
});

$schema->create('multi-scoped-users', function (Blueprint $table5) {
    $table5->increments('id');
    $table5->string('username');
});

$schema->create('multi-scoped-groups', function (Blueprint $table6) {
    $table6->increments('id');
    $table6->string('name');
});

以类似的方式处理迁移。

通过上述配置,Trait可以为每个用户和每个组管理一个可排序的任务列表。

您可以直接设置如下所示的“用户-组”范围。

<?php
// We are assuming this table have those 2 records:
$paul = MultiScopedUser::query()->find(1); // paul
$john = MultiScopedUser::query()->find(2); // john

// We are assuming this table have those 2 records:
$adminGroup = MultiScopedGroup::query()->find(1); // admin
$userGroup = MultiScopedGroup::query()->find(2); // user

// now onto the tasks
$t1 = new MultiScopedTask();
$t1->setAttribute('title', 'Create permissions');
$t1->setSortableScope('multi_scoped_user_id', $paul->getKey());
$t1->setSortableScope('multi_scoped_group_id', $adminGroup->getKey());
$t1->save();
echo $t1->getSortableRank(); // 1

$t2 = new MultiScopedTask();
$t2->setAttribute('title', 'Grant permissions to users');
$t2->setSortableScope('multi_scoped_user_id', $paul->getKey());
$t2->setSortableScope('multi_scoped_group_id', $adminGroup->getKey());
$t2->save();
echo $t2->getSortableRank(); // 2

$t3 = new MultiScopedTask();
$t3->setAttribute('title', 'Install servers');
$t3->setSortableScope('multi_scoped_user_id', $john->getKey());
$t3->setSortableScope('multi_scoped_group_id', $adminGroup->getKey());
$t3->save();
echo $t3->getSortableRank(); // 1, because John has his own task list inside the admin-group

$t4 = new MultiScopedTask();
$t4->setAttribute('title', 'Manage content');
$t4->setSortableScope('multi_scoped_user_id', $john->getKey());
$t4->setSortableScope('multi_scoped_group_id', $userGroup->getKey());
$t4->save();
echo $t4->getSortableRank(); // 1, because John has his own task list inside the user-group

或者您可以使用关系,如下所示

// We are assuming this table have those 2 records:
$paul = MultiScopedUser::query()->find(1); // paul
$john = MultiScopedUser::query()->find(2); // john

// We are assuming this table have those 2 records:
$adminGroup = MultiScopedGroup::query()->find(1); // admin
$userGroup = MultiScopedGroup::query()->find(2); // user

// now onto the tasks
$t1 = new MultiScopedTask(['title' => 'Create permissions']);
$t1->user()->associate($paul);
$t1->group()->associate($adminGroup);
$t1->save();
echo $t1->getSortableRank(); // 1

$t2 = new MultiScopedTask(['title' => 'Grant permissions to users']);
$t2->user()->associate($paul);
$t2->group()->associate($adminGroup);
$t2->save();
echo $t2->getSortableRank(); // 2

$t3 = new MultiScopedTask(['title' => 'Install servers']);
$t3->user()->associate($john);
$t3->group()->associate($adminGroup);
$t3->save();
echo $t3->getSortableRank(); // 1, because John has his own task list inside the admin-group

$t4 = new MultiScopedTask(['title' => 'Manage content']);
$t4->user()->associate($john);
$t4->group()->associate($userGroup);
$t4->save();
echo $t4->getSortableRank(); // 1, because John has his own task list inside the user-group

现在生成的函数接受每个作用域列的一个参数,以将查询限制在给定的作用域内。

<?php
// $t1
$firstPaulAdminTask = MultiScopedTask::query()->findOneByRank(
    1,
    ['multi_scoped_user_id' => $paul->getKey(), 'multi_scoped_group_id' => $adminGroup->getKey()]
);

// $t2
$lastPaulTask = $firstPaulAdminTask->getNext();

// $t4
$firstJohnUserTask = MultiScopedTask::query()->findOneByRank(
    1,
    ['multi_scoped_user_id' => $john->getKey(), 'multi_scoped_group_id' => $userGroup->getKey()]
);

使用排序行为和 Scope 的模型还受益于一个名为 inList() 的额外 Builder 方法

<?php
$allJohnsUserTasks = MultiScopedTask::query()
    ->inList(['multi_scoped_user_id' => $john->getKey(), 'multi_scoped_group_id' => $userGroup->getKey()])
    ->get();
// => collection($t4)

配置

默认情况下,使用以下配置

    public static function sortable(): array
    {
        return [];
    }

模型中会添加列sortable_rank。您可以使用如下配置进行配置

    public static function sortable(): array
    {
        return ['rank_column' => 'my_rank_column'];
    }

如上章节所述,您还可以使用作用域,它可以是一个或多个列。例如配置如下

    public static function sortable(): array
    {
        return ['use_scope' => true, 'scope_columns' => ['multi_scoped_user_id', 'multi_scoped_group_id']];
    }

要使用作用域,您必须use_scope设置为true

无论您为您的列指定什么名称,可排序行为总是添加以下代理方法,这些方法映射到正确的列

<?php
$task->getSortableRankName();          // returns name of the rank column
$task->getSortableRank();              // returns value of the rank column
$task->setSortableRank($rank);         // allows set rank value
$task->isSortableScopeUsed();          // returns use_scope configuration value
$task->getSortableScopeNames();        // returns scope_columns configuration value
$task->getSortableScope($key);         // returns value of the scope column by the scope column name
$task->getSortableScopes();            // returns array of scope values where key is scope column name and value scope column value
$task->setSortableScope($key, $scope); // allows set scope column value by the scope column name as a key

提示

如果您计划使用关系和作用域,请检查上面的示例,因为您需要覆盖每个作用域列的设置器。

完整API

以下是行为添加到模型对象的函数列表

<?php
// storage columns accessors
public function getSortableRankName(): string
public function getSortableRank(): int
public function setSortableRank(int $rank): void

// only for behavior with use_scope
public function isSortableScopeUsed(): bool
public function getSortableScopeNames(): array
public function getSortableScope(string $key): ?int
public function getSortableScopes(): array
public function setSortableScope(string $key, int $scope): void

// inspection methods
public function isFirst(): bool
public function isLast(): bool

// list traversal methods
public function getNext(): Model
public function getPrevious(): Model

// methods to insert an object in the list (require calling save() afterwards)
public function insertAtRank(int $rank): Model
public function insertAtBottom(): Model
public function insertAtTop(): Model

// methods to move an object in the list (immediate, no need to save() afterwards)
public function moveToRank(int $newRank): Model
public function swapWith(Model $object): Model
public function moveUp(): Model
public function moveDown(): Model
public function moveToTop(): Model
public function moveToBottom(): Model

// method to remove an object from the list (requires calling save() afterwards)
public function removeFromList(): Model

以下是行为添加到构建器的函数列表

<?php
public function filterByRank(int $rank, array $scopes = []): self
public function orderByRank(string $order = 'asc'): self
public function findOneByRank(int $rank, array $scopes = []): Model
public function findList(array $scopes = []): ?Collection
public function countList(array $scopes = []): int
public function deleteList(array $scopes = []): int
public function getMaxRank(array $scopes = []): ?int
public function reorder(array $order): bool
// only for behavior with use_scope
public function inList(array $scopes): self