Laravel 5.1 - 过滤 Collections 无效

Laravel 5.1 - Filtering on Collections Not Working

我有以下两个collection:

$credits = collect([
   ['quantity' => 3, 'product_id' => 1],
   ['quantity' => 2, 'product_id' => 5]
]);

$basketItems = collect([
   ['id' => 1, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 2, 'basket_id' => 4, 'product_id' => 2],
   ['id' => 3, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 4, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 5, 'basket_id' => 4, 'product_id' => 1]
]);

$credits collection 告诉我们用户有 3 个可用积分可用于 product_id 1.

现在我想创建两个新的 collection。如果用户拥有他们有可用积分的购物篮商品 - 这可以由 product_id 确定,那么我想将这些商品添加到名为 $basketItemsUseCredits.[=27 的新 collection 中=]

如果购物篮中的商品没有该产品类型的可用积分,我想将这些商品添加到另一个名为 $basketItemsPay 的 collection 中。

所以在上面的例子中,我应该以 $basketItemsUseCredits 和 id 为 1,3 和 4 的 basketitems 结束。 $basketItemsPay 应该以 id 为 2 和 5 的 basketsitems 结束。以下不起作用。

  $basketItemsUseCredits = $basketItems->map(function ($basketItem) use ($credits) {

        $qty = $credits->where('product_id', $basketItem->product_id)->get('quantity', 0);

        if ($qty > 0) {
             // update quantity
             $credits->transform(function ($credit) use ($basketItem) {
                    if ($credit->product_id == $basketItem->product_id) {
                         $credit->quantity = $credit->quantity - 1;
                         return $credit;
                    }
                    else
                        return $credit
            });               

           return $basketItem;
        }
  })


   $basketItemsPay = $basketItems->map(function ($basketItem) use ($basketItemsUseCredits) {

        if (!$basketItemsUseCredits->contains('id', $basketItem->id))
              return $basketItem;
    });

    if(!$basketItemsPay->isEmpty()) {
        // do some extra stuff
    }

下一行总是返回 0:

$qty = $credits->where('product_id', $basketItem->product_id)->get('quantity', 0);

我还注意到另一件事。如果 $basketItemsPay 为空,例如如果我 dd($basketItemsPay) 我得到以下信息:

Collection {#316 ▼
    #items: array:1 [▼
       0 => null
    ]
 }

那么为什么在上面的场景中以下总是为真?

if(!$basketItemsPay->isEmpty()) {
        // do some extra stuff
}

感谢任何帮助。

* 更新 *

通过执行以下操作设法修复 - 除非有人知道更好的解决方案:

$qty = $credits->where('product_id', $basketItem->product_id)->first()['quantity'];

并按如下方式链接拒绝方法以去除空值 - 有谁知道更优雅的解决方案吗?

$basketItemsPay = $basketItems->map(function ($basketItem) use ($basketItemsUseCredits) {

      if (!$basketItemsUseCredits->contains('id', $basketItem->id))
             return $basketItem;

})->reject(function ($basketItem) {
        return empty($basketItem);
});

我也花了一段时间才弄明白这一点。以下是我将如何处理这个问题。这是一种稍微更线性的方法,比 Collection 方法中的嵌套闭包多 self-explanatory。它也比你的例子多了一点代码,但这是个人喜好的问题。我试图在代码注释中尽可能地进行描述。

让我们自己更轻松的最重要的部分在于从一开始就改变 $credits 集合的结构:

// First, let's simplify the $credits structure for easy checking. NOTE: this collection WILL mutate by the code below.
// If the original version is to be preserved for whatever reason, now's the time to duplicate it :)
$credits = collect([
   ['quantity' => 3, 'product_id' => 1],
   ['quantity' => 2, 'product_id' => 5]
])
    ->pluck('quantity', 'product_id')

/* Results in a collection where keys are product_id, and values are quantity:

    Illuminate\Support\Collection {
        all: [
            1 => 3,
            5 => 2
        ]
    }

*/

$basketItems = collect([
   ['id' => 1, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 2, 'basket_id' => 4, 'product_id' => 2],
   ['id' => 3, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 4, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 5, 'basket_id' => 4, 'product_id' => 1]
]);


$basketItemsUseCredits  = new \Illuminate\Support\Collection;

// First, we will filter the $basketItems collection by the condition that the product_id for each item
// does NOT occur as key inside $credits. NOTE: filter() returns a new collection. The original $basketItems
// does not mutate because of this call.
$basketItemsPay = $basketItems->reject(function ($basketItem) use ($credits) {

    // If credits has a key corresponding the product_id of the current $basketItem, REJECT the $basketItem
    return $credits->has($basketItem['product_id']);
});



// Now, we will create an intermediate collection of basket items that need to be compared against the quantity
// of credits available for products of the ID's present in this collection:
$basketItemsCountCredits = $basketItems->filter(function ($basketItem) use ($credits) {

    // If credits has a key corresponding the product_id of the current $basketItem, KEEP the $basketItem
    return $credits->has($basketItem['product_id']);
});

// Lastly we will iterate the $basketItemsCountCredits collection, and determine whether credits are still available
// for each item. If so, push the item to $basketItemsUseCredits AND decrement the amount of available credits for 
// the item's ID. otherwise just push the item to $basketItemsPay.
foreach ($basketItemsCountCredits as $item) {

    $productId = $item['product_id'];
    $remainingCredits = $credits->get($productId);

    // If more than 0 credits are available for products with ID of $item['product_id']
    if ($remainingCredits > 0) {

        // .. push the $item to $basketItemsUseCredits,
        $basketItemsUseCredits->push($item);

        // .. and decrement the amount of available credits. 
        // Collection->put() overwrites the key => value pair in the collection if the key already exists.
        $credits->put($productId, $remainingCredits - 1);
    } else {

        // The amount of available credits might have become 0 in previous iterations of this for-loop
        $basketItemsPay->push($item);
    }
}

There are a lot of pretty powerful methods available 在 Illuminate 系列上。当以正确的方式使用时,它们可以允许一些非常干净和简洁的代码!

但是这些相同的方法也会让您在 code-readability 方面倒退 并且在不必要的地方使用时 - 复杂性。传递回调函数的方法在某些情况下主要是在别处完成该回调的定义时有用,并且它依赖于该上下文来完成其工作。从你的例子来看,这里似乎不是这种情况。

不同于连续使用 filterreject 方法,就像我在上面的示例中所做的那样,您可以使用很好的旧 foreach 循环同时执行这两个操作。但是,这确实需要将 $basketItemsCountCredits$basketItemsPay 变量预先初始化为新集合。如果我们也去掉注释,实际上代码并不多,而且仍然完全可读:)

$credits = collect([
   ['quantity' => 3, 'product_id' => 1],
   ['quantity' => 2, 'product_id' => 5]
])->pluck('quantity', 'product_id')

$basketItems = collect([
   ['id' => 1, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 2, 'basket_id' => 4, 'product_id' => 2],
   ['id' => 3, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 4, 'basket_id' => 4, 'product_id' => 1],
   ['id' => 5, 'basket_id' => 4, 'product_id' => 1]
]);


$basketItemsCountCredits = new \Illuminate\Support\Collection;
$basketItemsUseCredits   = new \Illuminate\Support\Collection;
$basketItemsPay          = new \Illuminate\Support\Collection;

// A foreach loop is perfectly sane here
foreach ($basketItems as $item) {
   if ($credits->has($item['product_id'])) {
       $basketItemsCountCredits->push($item);
   } else {
       $basketItemsPay->push($item);
   }
});

foreach ($basketItemsCountCredits as $item) {

    $productId = $item['product_id'];
    $remainingCredits = $credits->get($productId);

    if ($remainingCredits > 0) {
        $basketItemsUseCredits->push($item);
        $credits->put($productId, $remainingCredits - 1);
    } else {
        $basketItemsPay->push($item);
    }
}