通过 ID 数组对 Laravel 集合进行排序

Sorting Laravel Collection via Array of ID's

是否可以在仍然通过关系访问的同时使用单独的 ID 数组对关系集合进行排序?

设置 Checklist 有许多 ChecklistItem,并且相关项的所需顺序存在 属性 Checklist::$item_order。它只是一组按用户所需顺序排列的数字 ID。 :

class Checklist extends Model {
    protected $casts = ['item_order' => 'array'];

    public function items() {
        return $this->hasMany(ChecklistItem::class);
    }
}
class ChecklistItem extends Model {
    public function list() {
        return $this->belongsTo(Checklist::class);
    }
}

我以正常方式访问此关系:

$list = Checklist::find(1234);

foreach ($list->items as $item) {
    // More Code Here
}

有没有可行的方法根据 $list->item_order 数组中的值对 $list->items 进行排序?

(我不能只在 'item' table 中添加一个 'order' 列,b/c 每个 'list' 的顺序都会改变。)

你可以这样做:

$order = $list->item_order;
$list->items->sortBy(function($model) use ($order){
    return array_search($model->getKey(), $order);
}

您也可以向您的模型添加一个属性访问器,它具有相同的功能

public function getSortedItemsAttribute() 
{
    if ( ! is_null($this->item_order)) {
        $order = $this->item_order;

        $list = $this->items->sortBy(function($model) use ($order){
            return array_search($model->getKey(), $order);
        });
        return $list;
    }
    return $this->items;
}

用法:

foreach ($list->sortedItems as $item) {
    // More Code Here
}

如果您在多个地方需要这种功能,我建议您创建自己的 Collection class:

class MyCollection extends Illuminate\Database\Eloquent\Collection {

    public function sortByIds(array $ids){
        return $this->sortBy(function($model) use ($ids){
            return array_search($model->getKey(), $ids);
        }
    }
}

然后,在您的模型中实际使用 class 覆盖 newCollection()。在这种情况下,它将位于 ChecklistItems class:

public function newCollection(array $models = array())
{
    return new MyCollection($models);
}

您可以尝试建立一个关系,returns 结果按您要查找的顺序排列。您仍然应该能够预先加载关系,并以指定的顺序获得结果。这一切都假设 item_order 字段是逗号分隔的 id 字符串列表。

public function itemsOrdered()
{
    /**
     * Adds conditions to the original 'items' relationship. Due to the
     * join, we must specify to only select the fields for the related
     * table. Then, order by the results of the FIND_IN_SET function.
     */
    return $this->items()
        ->select('checklist_items.*')
        ->join('checklists', 'checklist_items.checklist_id', '=', 'checklists.id')
        ->orderByRaw('FIND_IN_SET(checklist_items.id, checklists.item_order)');
}

或者,如果您不想对 tables/fields 进行硬编码:

public function itemsOrdered()
{
    $orderField = 'item_order';

    $relation = $this->items();
    $parentTable = $relation->getParent()->getTable();
    $related = $relation->getRelated();
    $relatedTable = $related->getTable();

    return $relation
        ->select($relatedTable.'.*')
        ->join($parentTable, $relation->getForeignKey(), '=', $relation->getQualifiedParentKeyName())
        ->orderByRaw('FIND_IN_SET('.$related->getQualifiedKeyName().', '.$parentTable.'.'.$orderField.')');
}

现在,您可以:

$list = Checklist::with('items', 'itemsOrdered')->first();
var_export($list->item_order);
var_export($list->items->lists('id'));
var_export($list->itemsOrdered->lists('id'));

只是一个警告:这是相当实验性的代码。它看起来可以使用我可用的少量测试数据,但我没有在生产环境中做过这样的事情。此外,这仅在 HasMany 关系上进行了测试。如果你在 BelongsToMany 或类似的东西上尝试这个确切的代码,你会遇到问题。

我最近不得不做这样的事情,但额外的要求是并非所有项目都有特定的顺序。其余没有指定顺序的项目必须按字母顺序排列。

所以我尝试了接受的答案中的策略,使用自定义回调对集合进行排序,该回调试图解决丢失的项目。我通过多次调用 orderBy() 使其工作,但速度很慢。

接下来我尝试了另一个答案,首先使用 MySQL FIND_IN_SET() 函数排序,然后使用项目名称排序。我不得不按 FIND_IN_SET(...) DESC 排序以确保列出的项目排在列表的顶部,但这也导致订购的项目顺序相反。


所以我最终做了这样的事情,通过对数组中的每个项目进行相等性检查,然后通过项目名称在数据库中进行排序。与接受的答案不同,这是作为一种简单的关系方法完成的。这意味着我可以保留 Laravel 的神奇属性,并且每当我获得该项目列表时,它总是正确排序,而无需调用特殊方法。由于它是在数据库级别完成的,因此比在 PHP.

中执行速度更快
class Checklist extends Model {
    protected $casts = ['item_order' => 'array'];

    public function items() {
        return $this->hasMany(ChecklistItem::class)
            ->when(count($this->item_order), function ($q) {
                $table = (new ChecklistItem)->getTable();
                $column = (new ChecklistItem)->getKeyName();
                foreach ($this->item_order as $id) {
                    $q->orderByRaw("`$table`.`$column`!=?", [$id]);
                }
                $q->orderBy('name');
            });
    }
}

现在这样做:

$list = Checklist::with('items')->find(123);
// assume $list->item_order === [34, 81, 28]

查询结果如下:

select * from `checklist_items`
where `checklist_items`.`checklist_id` = 123 and `checklist_items`.`checklist_id` is not null
order by `checklist_items`.`id`!=34, `checklist_items`.`id`!=81, `checklist_items`.`id`!=28, `name` asc;