[Collection] Practice 1: Pricing Lamps and Wallets

Bây giờ chúng ta đã có những khái niệm cơ bản rồi, hãy xem cách sử dụng chúng thôi nào!

Chúng ta đã thấy chúng ta có thể viết Collection, nhưng những ví dụ dưới đây chúng ta sẽ sử dụng những thứ thực thi có sẵn
=> Chọn Laravel để thực thi

Bạn có thể tạo 1 Laravel Collection bằng 3 cách:

  • Truyền 1 mảng vào hàm tạo:
    1
    $collection = new Collection($items);
  • Sử dụng hàm tạo có tên làm make:
    1
    $collection = Collection::make($items);
  • Sử dụng helper collect()
    1
    $collection = collect($items);
    Theo tác giả sử dụng cách 3 với helper tiết kiệm khoảng trống, code ngắn đẹp hơn và sẽ áp dụng cách đó cho phần tiếp theo

Bài toán

Bài toán: Cho 1 JSON là products từ 1 cửa hàng, tính toán giá tiền của loại, biến thể (variants) đèn đơn và ví

Ví dụ JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"products": [
{
"title": "Small Rubber Wallet",
"product_type": "Wallet",
"variants": [
{ "title": "Blue", "price": 29.33 },
{ "title": "Turquoise", "price": 18.50 }
]
},
{
"title": "Sleek Cotton Shoes",
"product_type": "Shoes",
"variants": [
{ "title": "Sky Blue", "price": 20.00 }
]
},
{
"title": "Intelligent Cotton Wallet",
"product_type": "Wallet",
"variants": [
{ "title": "White", "price": 17.97 }
]
},
{
"title": "Enormous Leather Lamp",
"product_type": "Lamp",
"variants": [
{ "title": "Azure", "price": 65.99 },
{ "title": "Salmon", "price": 1.66 }
]
},
// ...
]
}

Như vậy chúng ra có các tập sản phẩm, 1 vài cái có product_type là “Lamp” hoặc “Wallet” và 1 vài thì không. Mỗi sản phẩm cũng có số lượng “variants” và variants là cái thực tế sẽ có giá. Cách giải quyết đơn giản:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$totalCost = 0;

// Loop over every product
foreach ($products as $product) {
$productType = $product['product_type'];
// If the product is a lamp or wallet...
if ($productType == 'Lamp' || $productType == 'Wallet') {
// Loop over the variants and add up their prices
foreach ($product['variants'] as $productVariant) {
$totalCost += $productVariant['price'];
}
}
}

return $totalCost;

OK bây giờ chúng ta cùng cải thiện vấn đề này nhé.

Replace Conditional with Filter (cái này dễ đoán)

Mục đích của chúng ta là phá vỡ foreach to và tìm cách cho vào chuỗi các bước đơn giản, độc lập, liên kết:

1
2
3
4
5
6
7
8
9
10
11
12
13
$lampsAndWallets = $products->filter(function ($product) {
$productType = $product['product_type'];
return $productType == 'Lamp' || $productType == 'Wallet';
});

$totalCost = 0;

foreach ($lampsAndWallets as $product) {
foreach ($product['variants'] as $productVariant) {
$totalCost += $productVariant['price'];
}
}
return $totalCost;

Replace || with Contains (contains methods)

Một sự khéo léo nhẹ ở đây là sử dụng in_array để check

1
2
3
$lampsAndWallets = $products->filter(function ($product) {
return in_array($product['product_type'], ['Lamp', 'Wallet']);
});

Trong collection thì in_arraycontains, 1 sự cải tiến tuyệt vời vì nó xóa đi một vài sự mơ hồ trong thứ tự tham số:
1
2
3
$lampsAndWallets = $products->filter(function ($product) {
return collect(['Lamp', 'Wallet'])->contains($product['product_type']);
});

Reduce to Sum

OK phần filter ngon rồi, giờ tới phần dưới nào:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
$lampsAndWallets = $products->filter(function ($product) {
$productType = $product['product_type'];
return $productType == 'Lamp' || $productType == 'Wallet';
});
**/
$totalCost = 0;

foreach ($lampsAndWallets as $product) {
foreach ($product['variants'] as $productVariant) {
$totalCost += $productVariant['price'];
}
}

return $totalCost;

Đoạn code này đang được tách biệt thành 2 phần

  • Lấy giá của mỗi product variant
  • Tính tổng giá để lấy tổng chung

Chúng ta có thể chia như sau, và chúng ta có thể sử dụng reduce để thay thế bước 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Get all of the product variant prices
$prices = collect();

foreach ($lampsAndWallets as $product) {
foreach ($product['variants'] as $productVariant) {
$prices[] = $productVariant['price'];
}
}

// Sum the prices to get a total cost
$totalCost = $prices->reduce(function ($total, $price) {
return $total + $price;
}, 0);

return $totalCost;

Hãy nhớ rằng những gì tôi đã nói sớm hơn về chúng ta có thể thay thế reduce bằng nhưng toán tử giàu tính biểu đạt hơn. Thực ra thì thay reduce bằng sum nhanh hơn
1
return $prices->sum();

Replace Nested Loop with FlatMap

Bây giờ còn đoạn lấy ra giá các sản phẩm ở giữa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
// Get all of the product variant prices
**/
$prices = collect();

foreach ($lampsAndWallets as $product) {
foreach ($product['variants'] as $productVariant) {
$prices[] = $productVariant['price'];
}
}
/**
// Sum the prices to get a total cost
$totalCost = $prices->sum()

return $totalCost;
**/

Nhìn qua thì có vẻ như chỉ cần map product variants với giá của nó nhưng chúng ta lại đang làm việc với tập products chứ không phải là với variants. Vậy chúng ta có thể làm ntn xây dựng 1 collection là tập variants và sau đó map chúng với giá là ngon :D.

Ừ ok, dùng map lấy ra

1
2
3
$variants = $lampsAndWallets->map(function ($product) {
return $product['variants'];
});

Vấn đề là bây giờ chúng ta có 1 colelction của mảng các variants, không phải mảng các vartiants
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
//
...
[
{ "title": "Blue", "price": 29.33 },
{ "title": "Turquoise", "price": 18.50 }
],
[
{ "title": "Sky Blue", "price": 20.00 }
],
[
{ "title": "White", "price": 17.97 }
],
{ "title": "Azure", "price": 65.99 },
{ "title": "Salmon", "price": 1.66 }
],
// ...
]

Thật may khi chúng ta có 1 toán tử “làm phẳng” 1 collection về 1 level. Và kết hợp với map nữa thì nó là flatMap. Sử dụng nó để thay thế 2 cái foreach:
1
2
3
4
5
6
7
$variants = $lampsAndWallets->flatMap(function ($product) {
return $product['variants'];
});

$prices = $variants->map(function ($productVariant) {
return $productVariant['price'];
});

Rồi OK, đến thời điểm này có thể đặp tất cả mọi thứ thành 1 single pipeline:
1
2
3
4
5
6
7
return $products->filter(function ($product) {
return collect(['Lamp', 'Wallet'])->contains($product['product_type']);
})->flatMap(function ($product) {
return $product['variants'];
})->map(function ($productVariant) {
return $productVariant['price'];
})->sum();

Haha, nhìn có vẻ ngon rồi nhỉ. Nhưng tin tôi đi, vẫn có thể ngon hơn đó

Plucking for Fun and Profit

pluck là shortcut của mapping 1 trường đơn với mỗi phần tử của collection. Ví dụ

1
2
3
$emails = $users->map(function ($user) {
return $user['email'];
});

Thì tương đương với:
1
$emails = $users->pluck('email');

Do đó có thể thay thế đoạn map trên bằng pluck
1
2
3
4
5
return $products->filter(function ($product) {
return collect(['Lamp', 'Wallet'])->contains($product['product_type']);
})->flatMap(function ($product) {
return $product['variants'];
})->pluck('price')->sum();

Xong vẫn chưa hết, ta lại có thể thay thế ->pluck('price')->sum() bằng ->sum('price') , và kết quả cuối cùng
1
2
3
4
5
return $products->filter(function ($product) {
return collect(['Lamp', 'Wallet'])->contains($product['product_type']);
})->flatMap(function ($product) {
return $product['variants'];
})->sum('price');

Không có vòng lặp đơn, điều kiện hay biến tạm thời nào được tìm thấy. Rất là thanh lịch nếu bạn hỏi tôi :D

Tổng kết vấn đề

Đoạn code ban đầu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$totalCost = 0;

// Loop over every product
foreach ($products as $product) {
$productType = $product['product_type'];
// If the product is a lamp or wallet...
if ($productType == 'Lamp' || $productType == 'Wallet') {
// Loop over the variants and add up their prices
foreach ($product['variants'] as $productVariant) {
$totalCost += $productVariant['price'];
}
}
}

return $totalCost;

và sau khi được refactor:
1
2
3
4
5
return $products->filter(function ($product) {
return collect(['Lamp', 'Wallet'])->contains($product['product_type']);
})->flatMap(function ($product) {
return $product['variants'];
})->sum('price');

Các helper đã được dùng:

flatten(): san phẳng mảng đa chiều thành mảng 1 chiều. Bạn cũng có thể truyền “độ sâu” của mảng như 1 đối số

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$collection = collect([
'Apple' => [
['name' => 'iPhone 6S', 'brand' => 'Apple'],
],
'Samsung' => [
['name' => 'Galaxy S7', 'brand' => 'Samsung']
],
]);

$products = $collection->flatten(1);

$products->values()->all();

/*
[
['name' => 'iPhone 6S', 'brand' => 'Apple'],
['name' => 'Galaxy S7', 'brand' => 'Samsung'],
]
*/

flatMap(): flatMap is a shortcut for mapping a collection and then flattening it by one

  • Sự kết hợp giữa map để xử lý dữ liệu
  • Sau đó “san phẳng” chúng thành 1 mảng.

pluck(): pluck is a shortcut for mapping a single field out of each element in a collection.

sum()

contains(): kiểm tra 1 collection có chưa 1 phần tử nhận vào hay không. Nó rất hữu ích để sử dụng cho phép hoặc (||)

reduce(): thu nhỏ, cắt giảm cả collection thành một giá trị đơn duy nhất.

N.V.M

Author

Ming

Posted on

2019-12-27

Updated on

2021-04-10

Licensed under

Comments