PHP yield: Generadores y optimización de memoria
— php, yield, generadores, memoria, optimizacion, rendimiento — 3 minutos de lectura
Hoy me enfrenté a un desafío: necesitaba escribir un comando para procesar registros almacenados en una base de datos remota. Para listar los registros, utilicé el método fetchRecords
, que interactúa directamente con una API RESTful:
1ApiClient::create('database', $config)2 ->fetchRecords([3 'Table' => 'usuarios',4 'Limit' => 1000,5 ]);
Este método devuelve hasta mil registros por solicitud. Pero, dado que la tabla contiene cientos de miles de filas, necesitaba una forma de hacer múltiples llamadas para obtener todos los datos. En bases de datos SQL, esto se logra fácilmente con LIMIT
y OFFSET
. Sin embargo, ¿cómo manejarlo cuando trabajamos con APIs HTTP?
Por suerte, la API incluye un parámetro llamado NextPageToken
, que permite obtener páginas adicionales de resultados. Este token se incluye en cada respuesta cuando hay más datos disponibles. Así que decidí implementar algo como esto:
1public function handle(): void2{3 $apiClient = ApiClient::create('database', $config);4 $nextPageToken = null;5
6 do {7 $response = $apiClient->fetchRecords([8 'Table' => 'usuarios',9 'NextPageToken' => $nextPageToken,10 ]);11
12 foreach ($response->get('Data') as $registro) {13 $this->procesarRegistro($registro);14 }15
16 $nextPageToken = $response->get('NextPageToken');17 $hayMasResultados = $response->get('HasMore');18 } while ($hayMasResultados);19}
Aquí, el campo HasMore
indica si quedan más resultados por recuperar, mientras que NextPageToken
nos da un puntero para continuar la paginación.
Sin embargo, extraer los registros de la API es solo una parte de lo que necesito hacer. No quería que todo este código complicado llenara el método principal de mi comando. Prefería algo más limpio, como esto:
1public function handle(): void2{3 foreach ($this->obtenerRegistros() as $registro) {4 $this->procesarRegistro($registro);5 }6}
Una opción podría ser mover el bucle do-while
a un método separado y devolver todos los resultados en un array. Pero dado que estamos trabajando con cientos de miles de registros, esta solución no es práctica. Necesitaba una forma de encapsular las llamadas HTTP en un método independiente, mientras iteraba sobre los resultados de manera eficiente. Aquí es donde entra en juego yield
.
1public function handle(): void2{3 $this->apiClient = ApiClient::create('database', $config);4
5 foreach ($this->obtenerRegistros() as $registro) {6 $this->procesarRegistro($registro);7 }8}9
10// El tipo de retorno del método ahora es iterable11private function obtenerRegistros(): iterable12{13 $nextPageToken = null;14
15 do {16 $respuesta = $this->apiClient->fetchRecords([17 'Table' => 'usuarios',18 'NextPageToken' => $nextPageToken,19 ]);20
21 foreach ($respuesta->get('Data') as $fila) {22 // Usamos yield para entregar los resultados uno a uno23 yield $fila;24 }25
26 $nextPageToken = $respuesta->get('NextPageToken');27 $hayMasResultados = $respuesta->get('HasMore');28 } while ($hayMasResultados);29}
Al usar yield
en lugar de return
, convertimos este método en un Generador. Cuando un método utiliza return
, todo su contexto se descarta después de devolver el resultado. Con yield
, en cambio, el estado del generador se conserva entre llamadas. Esto significa que podemos pausar y reanudar la ejecución del método, paginando los resultados de manera eficiente según sea necesario.
Este enfoque nos permite iterar sobre cada registro en la tabla, procesándolos uno por uno, mientras realizamos llamadas adicionales a la API para obtener más resultados cuando sea necesario.
Me sorprende que haya pasado tantos años sin necesitar esta herramienta, pero ahora tengo un recurso más en mi repertorio para futuros proyectos. Si quieres saber más sobre yield
, puedes consultar la documentación oficial de PHP sobre generadores.