
Gemas no tan conocidas: Lazy Collections
Si utilizas Laravel, conocerás -o al menos habrás leído sobre- la clase Collection. Esta es una clase que nos permite manejar listas para poder hacer casi cualquier operación con ellas. Es de los features que mas me gustan de Laravel. Sin embargo, como todo en la vida, no es perfecta. Veamos un ejemplo.
De una lista de números enteros, busquemos cuántos son múltiplos de 4:
use Illuminate\Support\Collection;
$numbers = new Collection([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
->filter(fn ($number) => $number % 4 === 0); // [4, 8, 12]
Avancemos. ¿Qué sucederá en cambio si es que utilizamos una lista mucho más grande de números?
Para ilustrarlo, haremos uso de un conveniente método de la clase Collection: times()
. Este método crea una lista de enteros desde el 1
hasta el valor que le indiquemos. Creemos la misma lista anterior solo para ver que retorne el mismo resultado:
$numbers = Collection::times(12)
->filter(fn ($number) => $number % 4 === 0)
->all(); // [4, 8, 12]
Ahora sí, utilicemos un valor mucho más grande, como por ejemplo 50000 * 1000
(osea, 50 millones):
$numbers = Collection::times(50000 * 1000)
->filter(fn ($number) => $number % 4 === 0)
->all();
Esta vez, en lugar de mostrarnos los valores filtrados, nuestra app nos mostrará un mensaje de error puesto que superamos el máximo de memoria disponible:
PHP Warning: range(): The supplied range exceeds the maximum array size: start=1 end=9223372036854775807 in /.../Contents/Resources/Laravel/8.0/vendor/laravel/framework/src/Illuminate/Collections/Collection.php on line 42

¿Qué sucedió? 🤔 En nuestra operación, todos los valores tienen que ser cargados en memoria. Dado que nuestra lista es inmensa, eventualmente ocupamos toda la memoria que teníamos asignada.
¿Qué pasaría si primero filtramos los valores a tan solo los menores de 10
? El tamaño de nuestra lista ya no será tan grande, cierto? 🤷
$numbers = Collection::times(50000 * 1000)
->filter(fn ($number) => $number < 10) // <---
->filter(fn ($number) => $number % 4 === 0)
->all();
Pues obtenemos el mismo resultado:
PHP Warning: range(): The supplied range exceeds the maximum array size: start=1 end=9223372036854775807 in /.../Contents/Resources/Laravel/8.0/vendor/laravel/framework/src/Illuminate/Collections/Collection.php on line 42
Esto es porque al momento de creación de la lista nuestra app no tiene idea que luego la filtraremos. Esto quiere decir que antes de cualquier operación, el arreglo completo será creado y almacenado en memoria.
Cambia a Lazy Collections
LazyCollection es otra clase de Laravel introducida en Laravel 6. Es muy similar a Collection
, con la diferencia de que todos los métodos que provee emplean generadores.
Lee más sobre Generadores en este otro artículo
Probemos el mismo ejemplo, pero utilizando Lazy Collections:
$numbers = Collection::times(50000 * 1000)
->filter(fn ($number) => $number < 10)
->filter(fn ($number) => $number % 4 === 0)
->all(); // [4, 8]
¿Por qué funcionó esta vez? Pues porque, al emplear generadores, LazyCollection solo mantiene en memoria un único elemento a la vez. No necesita construir toda la lista de números antes de empezar a realizar el filtrado. De hecho, hasta que pedimos los datos, ninguna operación había sido realizada.

Casos de uso
Puedes utilizar LazyCollection para muchas cosas, como por ejemplo:
- Para leer archivos y realizar operaciones con estos, como búsquedas y tareas de agregación (suma, promedio, etc). No importará el tamaño del archivo, pues solo nos enfocaremos en un elemento a la vez lo cual significa que no utilizaremos casi nada de memoria.
- Otro uso práctico es para realizar descargas transmitidas (Streamed Downloads). Los streamed downloads sirven cuando no queremos devolver un archivo al usuario, sin necesidad de escribir en el disco el resultado de nuestra operación. Piensa en un reporte generado, de manera tradicional tendríamos que crear primero nuestro
.csv
,.xlsx
, etc para luego devolverlo al usuario. Con (streamed downloads) nos ahorramos ese paso.
Eloquent y Query Builder
La cereza del pastel es cuando nos enteramos que podemos utilizar Lazy Collections para interactuar con nuestra base de datos. Laravel nos provee de métodos como cursor()
y lazy()
para poder cargar registros utilizando generadores.
En el trabajo trabajamos con bases de datos con tablas gigantes -decenas/cientos de GB- por lo que cuando queremos realizar reportes tenemos que buscar una manera eficiente de poder procesar los datos cuando los recibamos en nuestra app. Tanto cursor()
como lazy()
devuelve instancias de LazyCollection
. Lo cual quiere decir que si bien solo se ejecuta una query, los datos serán cargados cuando realmente se necesiten procesar. Puedes leer más sobre esto en la documentación.

Cierre
Evidentemente, el manejo de grandes volúmenes de datos de manera eficiente se puede realizar de la mano de Lazy Collections. Esto no significa que debas de emplearlo para todo, pero sí que lo tengas presente cuando la situación lo requiera. Espero que te sirva en el futuro 😉