Lazy Collection trong Laravel

8 min

Thumbnail

Nếu đã là một lập trình viên về PHP nói chung và Laravel nói riêng thì Collection có lẽ là một thư viện không mấy xa lạ. Tuy nhiên Laravel còn có thêm một thư viện hết sức mạnh mẽ, tiện lợi nhưng lại sử dụng rất ít memory. Đó chính là Lazy Collection

Collection

Như các bạn đã biết Collection là một wrapper của array, cung cấp những methods hữu ích để làm việc với array một cách dễ dàng. Tất nhiên đằng sau Collection vẫn là những function có sẵn của PHP. Tuy nhiên những function này có cách dùng không đồng nhất, đây cũng là lý do Collection ra đời.

Collection có một method rất hữu ích là times. Method này được dùng để tạo ra n lần kết quả của callback function

1Collection::times(3, fn ($number) =>
2 User::factory()->create(['email' => "user_{$number}@huygo.me"]);
3)->map(fn (User $user) => $user->email);

Dùng Collection trong trường hợp như trên là hoàn toàn hợp lý, chẳng có gì phải bàn cãi ở đây cả. Tuy nhiên nó sẽ trở thành một vấn đề nếu chúng ta cần tạo ra một số lượng lớn data, vài trăm ngàn, và có thể là vài triệu records. Lý do là vì tất cả các giá trị được tạo ra sẽ được lưu trữ in-memory.

Out of Memory

1Allowed memory size of 16777216 bytes exhausted...

Lỗi trên trông có quen không nhỉ? Gặp phải lỗi trên có nghĩa là chương trình của bạn đã sử dụng hết memory cho phép vì nó đã lưu toàn bộ kết quả và không còn chỗ để chứa những giá trị mới nữa.

Tình hình sẽ càng tệ hơn nếu bạn liên kết với các method khác tại vì nó sẽ tạo một dãy các giá trị mới và lưu trong memory

-Collection::times(3, fn ($number) =>
+Collection::times(100000, fn ($number) =>
3 User::factory()->create(['email' => "user_{$number}@huygo.me"]);
4)
5->map(fn (User $user) => $user->email)
+ 
+->filter(fn (User $user) => $user->isMember())
+->take(500);
+ 

Hmmm vậy thì làm thế nào để xử lý vấn đề trên. Việc đầu tiên có thể chúng ta sẽ nghĩ ngay tới đó là tăng memory cho phép. Tuy nhiên cách làm này không hề ổn tí nào, tại vì application của chúng ta không cần dùng nhiều memory đến như vậy. Chưa kể là việc tăng memory cho phép có thể gây những ảnh hưởng nhất định đến các chương trình khác chạy trên Server.

Lazy Collection

Đọc đến đây chắc các bạn có thể nói "Ahhh dùng Lazy Collection thôi". Mình hy vọng các bạn đã đọc đến đoạn này. Chỉ đơn giản thay đổi Collection thành Lazy Collection là chúng ta đã giải quyết được vấn đề mà không cần phải tăng memory cho phép.

-Collection::times(100000, fn ($number) =>
+LazyCollection::times(100000, fn ($number) =>
3 User::factory()->create(['email' => "user_{$number}@huygo.me"]);
4)
5->map(fn (User $user) => $user->email)
6->filter(fn (User $user) => $user->isMember())
7->take(500);

Thực tế cách dùng trên dường như chẳng dùng memory nào cả. Làm sao có thể như vậy được? Chỉ thay đổi Collection thành LazyCollection thôi mà?

Lý giải những câu hỏi trên là tại vì chẳng có giá trị nào được tạo ra cả. Tất cả đơn giản là nhờ vào Generators trong PHP. Mình sẽ không nói nhiều về cách dùng Lazy Collection cũng như những method hỗ trợ của nó vì Laravel Documentation đã đề cập rất rõ và cụ thể rồi. Các bạn có thể tham khảo thêm ở đây nhé. Bài viết này mình sẽ tập trung giải thích về Generator Functions để các bạn có thể hiểu sâu hơn về nó.

Generators

Thật ngạc nhiên là Generators đã được giới thiệu và sử dụng từ ... PHP 5.5. Tuy nhiên không phải ai cũng nắm rõ và sử dụng nó một cách thoải mái.

Function thông thường

Function trong PHP chỉ có thể cho ra một kết quả thôi. Sau từ khóa return tất cả code ngay sau nó sẽ bị bỏ qua hoàn toàn

1function run() {
2 return 'Hello';
3 return 'World';
4}
5 
6dump(run()); // 'Hello'

Generator

Về mặt syntax thì khác nhau rõ nhất giữa Function thông thường và Generator chính là từ khóa returnyield. Bây giờ chúng ta thử thay đổi return bằng yield để xem thử điều gì xảy ra.

1function run() {
2 dump('Executed');
3 yield 'Hello';
4 yield 'World';
5}
6 
7dump(run());
8// Generator {
9// executing: {...}
10// closed: false
11// }

Kết quả nhận được là một Generator Object !!! Không hề có chỗ nào liên quan đến Hello hay là World cả. Thậm chí dump cũng không cho ra kết quả nào ???

Generator sau khi gọi sẽ không chạy bất kì code nào bên trong nó cả. Thay vào đó chúng ta sẽ nhận được một Generator Object cho mỗi từ khóa yield bên trong.

Sử dụng currentnext để lấy giá trị của Generator

Để thật sự bắt đầu chạy Generator thì chúng ta sẽ cùng method current. Method này sẽ kích hoạt code bên trong Generator cho đến khi nào nó bắt gặp từ khóa yield đầu tiên.

1function run() {
2 yield 'Hello';
3 yield 'World';
4}
5 
6 
7$generator = run();
8dump($generator->current());
9 
10// Output:
11// 'Hello'

Không giống như Function thông thường, Generator sẽ không tự chấm dứt sau khi giá trị đầu tiên được trả lại. Generator vẫn còn đó, sẵn sàng chờ chúng ta gọi và trả về kết quả tiếp theo.

1function run() {
2 yield 'Hello';
3 yield 'World';
4}
5 
6$generator = run();
+ 
+$generator->current();
+$generator->next();
+ 
11dump($generator->current());
12// Output:
13// 'World'

Sử dụng từ khóa yield trong vòng lặp.

Nhìn vào những ví dụ đơn giản trên các bạn có thể nghĩ rằng Generator Function chưa có điểm nào nổi bật cả. Tuy nhiên sức mạnh thật sự của yield sẽ được bộc lộ khi chúng ta sử dụng nó trong loop (vòng lặp).

1function generateNumbers()
2{
3 $number = 1;
4 
5 while (true) {
6 dump($number);
7 
8 $number++;
9 }
10}

Thử chạy function trên...whoops...infinite loop. Chắc chắn chúng ta sẽ không bao giờ viết function như trên. Để sửa lỗi trên thì hoặc là chúng ta thay đổi điều kiện while, hoặc là thử dùng yield xem sao.

1function generateNumbers()
2{
3 $number = 1;
4 
5 while (true) {
- dump($number);
+ yield($number);
8 $number++;
9 }
10}
11 
12 
13$generator = generateNumbers();
14 
15dump($generator->current()); // 1
16$generator->next();
17dump($generator->current()); // 2
18$generator->next();
19dump($generator->current()); // 3
20 

Hooray!!! Lý do chúng ta không còn vướng phải infinite loop tại vì code sẽ không tự động chạy như thông thường. Code sẽ chỉ hoạt động khí chúng ta gọi current()next().

Sử dụng foreach với Generators

Theo như ví dụ trên, chúng ta phải liên tục gọi currentnext để có thể chạy Generator theo ý muốn. Làm thế này có khi chúng ta sẽ không còn hứng thú gì với Generator nữa. Ví dụ sau đây sẽ giúp chúng ta không phải chạy một cách thủ công.

1$generator = generateNumbers();
2 
3foreach ($generator as $number) {
4 dump($number);
5 if ($number === 5) break;
6}

Các bạn có chú ý từ khóa break không. Như mình miêu tả ở trên Generate không thật sự tạo ra infinite loop, tuy nhiên khi dùng nó với vòng lặp thông thường thì chúng ta vẫn phải chú ý điều kiện để dừng vòng lặp đó.

Lazy Collections

Ở trên mình giải thích cơ bản về bản chất của Generator và cách nó hoạt động. Thực tế Laravel 6 trở lên đã cung cấp Lazy Collection với những method gần giống như Collection.

Không giống như Collection thông thường khi nó nhận PHP array, Lazy Collection sẽ nhận Generator Function.

1use Illuminate\Support\LazyCollection;
2 
3$lazyCollection = LazyCollection::make(function () {
4 $number = 1;
5 
6 while (true) {
7 yield $number++;
8 }
9});

Chúng ta vừa mới tạo một collection nắm giữ một chuỗi tất cả các số nguyên. Chú ý mình dùng nắm giữ ở đây tại vì, như chúng ta đã đọc ở trên, thì chưa hề có một giá trị nào được tạo ra cả.

Ví dụ bây giờ chúng ta muốn lấy 50 số nguyên đầu tiên:

1$lazyCollection->take(50);

Hoặc ví dụ muốn lấy 10000 số lẻ:

1LazyCollection::times(INF)
2 ->filter(fn ($number) => $number % 2 !== 0)
3 ->take(10000)
4 ->each(fn ($number) => dump($number));

Streaming File Downloads với Lazy Collections

Một trong những ứng dụng hữu ích nhất của Lazy Collection chính là streaming data. Những lợi ích kể đến như:

  • Không cần phải lưu giữ các records trong memory.

  • Không phải giữ file export trong memory.

  • User không cần phải đợi cho tới khi file được xuất ra hoàn toàn trên Server. Dowload có thể tiến hành ngay lập tức.

Ví dụ dưới đây sẽ chỉ rõ cách sử dụng Lazy Collection. Để đơn giản hóa ví dụ thì chúng ta sẽ sử dụng thư viện league/csv.

1use Illuminate\Support\LazyCollection;
2use League\Csv\Writer;
3 
4Route::get('login-audit', function () {
5 $logins = LazyCollection::times(1000 * 1000, fn () => [
6 'user_id' => 24,
7 'logged_in_at' => now()->toIsoString(),
8 ]);
9 
10 return response()->streamDownload(function () use ($logins) {
11 $csvWriter = Writer::createFromFileObject(
12 new SplFileObject('php://output', 'w+')
13 );
14 
15 $csvWriter->insertOne(['User ID', 'Login Time']);
16 
17 $csvWriter->insertAll($logins);
18 }, 'logins.csv');
19});

Sau đây là cụ thể những gì xảy ra ở trên:

  1. Tạo Lazy Collection với 1 triệu record.

  2. Sử dụng CSV Writer để viết dư liệu trực tiếp vào file đang được streaming.

  3. Tạo header trong file CSV.

  4. Method insertAll thực chất sử dụng foreach để lấy toàn bộ dữ liệu từ Lazy Collection.

Như các bạn thấy, chúng ta không hề lưu tất cả dữ liệu trong memory, bao gồm cả dữ liệu gốc, và cả file CSV. Record sẽ được tạo ra mỗi một lần, và được viết trực tiếp vào file đang được streaming cho người dùng.

Lời kết

Hy vọng các bạn đã đọc đến đây và nắm được cách hoạt động cũng như những ứng dụng của Lazy Collection.

Để hiểu sâu hơn nữa về Lazy Collection thì các bạn có thể tìm đọc source code của Laravel.

Copyright © 2026. All rights reserved.