El ciclo de WordPress (The Loop) es una implementación síncrona de los patrones de diseño Iterator y Context Switching dentro de la arquitectura procedural del núcleo. Su objetivo principal es el recorrido secuencial del conjunto de registros WP_Post recuperados de MySQL por la instancia WP_Query, sincronizando simultáneamente el estado global del entorno de ejecución de PHP.
El estado interno del iterador WP_Query está gobernado por tres propiedades clave:
$posts: Array indexado de objetos WP_Post devueltos por la base de datos.$post_count: Número total de registros en la selección actual (longitud del array $posts).$current_post: Puntero entero interno (cursor) de la posición activa, inicializado en -1.En cada paso del ciclo se produce una transición atómica entre los métodos:
have_posts(): Evalúa la condición de predicado ($this->current_post + 1) < $this->post_count. En la primera llamada inicializa el puntero.the_post(): Incrementa el puntero $this->current_post++, asigna el registro a la propiedad $this->post = $this->posts[$this->current_post] y delega el control en la rutina de bajo nivel setup_postdata($post).El procedimiento setup_postdata($post) realiza una operación fundamental: sobreescribe la variable global $post y desglosa en el ámbito global las entidades contextuales asociadas:
$id: Identificador entero del registro actual.$authordata: Objeto WP_User del autor de la publicación (permitiendo la resolución inmediata de the_author()).$currentday y $currentmonth: Metadatos de la fecha de publicación.$pages: Array de fragmentos de contenido divididos por la etiqueta <!--nextpage-->.$page: Número de subpágina activa en artículos paginados.$multipage: Indicador booleano de presencia de paginación interna.$more: Indicador booleano que controla el corte del extracto por la etiqueta <!--more-->.$numpages: Número total de subpáginas.Gracias a esta hidratación del contexto global, los llamados Template Tags (the_title(), the_content(), the_permalink(), get_the_ID()) pueden invocarse sin argumentos: leen implícitamente los datos desde el ámbito global preparado.
En los temas modernos basados en bloques (Full Site Editing / Gutenberg), el ciclo imperativo clásico es sustituido por el bloque declarativo core/query (Query Loop Block). En lugar de mutar variables globales de PHP, el motor de bloques emplea la Block Context API jerárquica, transmitiendo postId y postType a los componentes secundarios (core/post-title, core/post-content) mediante estructuras de datos aisladas.
El funcionamiento de The Loop debe concebirse como una máquina de estados finitos con una pila de contextos: el cursor se desplaza por los datos, proyectando de manera continua las propiedades del elemento actual en los registros globales del runtime, exigiendo al finalizar la restauración determinista del estado original.
The Loop setup_postdata Query Loop Block wp_reset_postdata rewind_posts[Selección WP_Query: $posts = [Post_0, Post_1, Post_2]]
│
▼
have_posts()? (0 < 3) ──► SÍ
│
▼
the_post()
┌────────────────────────┴────────────────────────┐
▼ ▼
$current_post = 0 setup_postdata($post)
$post = $posts[0] ┌───────────────────┴───────────────────┐
▼ ▼
$authordata = WP_User $pages = [...]
$id = 42 $more = 1
│
▼
[Ejecución del cuerpo del bucle]
the_title() ──► Lee $post->post_title
the_author() ──► Lee $authordata->display_name
│
▼
have_posts()? (1 < 3) ──► SÍ (Paso siguiente)
│
▼
have_posts()? (3 < 3) ──► NO (Fin del bucle)
│
▼
wp_reset_postdata()
│
▼
Restauración de $post = $wp_the_query->postWP_Query llena el array $posts. El puntero current_post se fija en -1. La bandera in_the_loop está activa.
have_posts() evalúa (current_post + 1 < post_count). Si no restan elementos, devuelve false.
the_post() avanza current_post++ e invoca setup_postdata(), poblando $post, $authordata, $pages y $more.
Invocación de Template Tags (the_title, the_content) sin parámetros, consumiendo automáticamente el ámbito global activo.
wp_reset_postdata() devuelve la variable global $post a la entrada de la consulta principal $wp_the_query.
yield) sin agotar la memoria asignada (Allowed memory size exhausted).WP_Post, alimentándolos a setup_postdata() para procesarlos con plantillas nativas del tema."inherit": true), con filtrado automático por taxonomías y paginación en servidor.have_posts() y the_post()#El código de WP_Query revela los detalles de su funcionamiento:
public function have_posts(): bool {
if ($this->current_post + 1 < $this->post_count) {
return true;
} elseif ($this->current_post + 1 === $this->post_count && $this->post_count > 0) {
// Conclusión del recorrido: dispara la acción loop_end
do_action_ref_array('loop_end', [&$this]);
$this->rewind_posts();
}
$this->in_the_loop = false;
return false;
}
public function the_post(): void {
$this->in_the_loop = true;
if ($this->current_post === -1) {
// Inicio del recorrido: dispara la acción loop_start
do_action_ref_array('loop_start', [&$this]);
}
$this->current_post++;
$this->post = $this->posts[$this->current_post];
setup_postdata($this->post);
}wp_reset_postdata(), wp_reset_query() y rewind_posts()#wp_reset_postdata(): Restaura la variable global $post al registro activo del objeto primario $wp_the_query. Debe invocarse siempre tras concluir un bucle secundario con new WP_Query().wp_reset_query(): Ejecuta wp_reset_postdata() y restaura el puntero global $wp_query = $wp_the_query. Se emplea únicamente en código heredado tras un uso erróneo de query_posts(). En código moderno query_posts() está estrictamente prohibido.rewind_posts(): Restablece $this->current_post = -1. ¡No realiza consultas a MySQL! Permite repetir el recorrido de las mismas entradas en otra sección de la plantilla.<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Loop Iterator & Context Guard
* Description: Iterador mediante generadores PHP 8.4+ con protección del contexto global $post y paginación Keyset.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\Loop;
use Generator;
use WP_Post;
use WP_Query;
if (!defined('ABSPATH')) {
exit;
}
final class EnterpriseLoop
{
/**
* Generador para recorrer instancias de WP_Query personalizadas.
* Ejecuta setup_postdata() en cada yield de forma automática
* y asegura la restauración de contexto en bloques finally ante excepciones.
*
* @param array<string, mixed> $queryArgs
* @return Generator<int, WP_Post>
*/
public static function iterate(array $queryArgs): Generator
{
$query = new WP_Query($queryArgs);
try {
while ($query->have_posts()) {
$query->the_post();
/** @var WP_Post $post */
global $post;
yield $query->current_post => $post;
}
} finally {
// Restauración garantizada del contexto global (patrón RAII)
wp_reset_postdata();
}
}
/**
* Procesamiento por lotes para decenas de miles de entradas (Keyset Chunking).
* Evita errores Out of Memory durante migraciones y exportaciones masivas.
*
* @param string $postType
* @param int $chunkSize
* @return Generator<int, WP_Post>
*/
public static function cursor(string $postType, int $chunkSize = 100): Generator
{
global $wpdb;
$lastId = 0;
while (true) {
$keysetWhereFilter = static function (string $where) use ($wpdb, $lastId): string {
return $where . $wpdb->prepare(" AND {$wpdb->posts}.ID > %d", $lastId);
};
add_filter('posts_where', $keysetWhereFilter);
try {
/** @var list<WP_Post> $posts */
$posts = get_posts([
'post_type' => $postType,
'post_status' => 'publish',
'posts_per_page' => $chunkSize,
'orderby' => 'ID',
'order' => 'ASC',
'no_found_rows' => true, // Desactiva recuento SQL_CALC_FOUND_ROWS
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
'suppress_filters' => false,
]);
} finally {
remove_filter('posts_where', $keysetWhereFilter);
}
if (empty($posts)) {
break;
}
foreach ($posts as $singlePost) {
$lastId = $singlePost->ID;
// Hidratación manual para compatibilidad con Template Tags
setup_postdata($GLOBALS['post'] = $singlePost);
try {
yield $singlePost->ID => $singlePost;
} finally {
// Limpieza segura en interrupciones
}
}
// Vaciado de la caché de objetos para prevenir saturación de RAM
wp_cache_flush_group('posts');
if (count($posts) < $chunkSize) {
break;
}
}
wp_reset_postdata();
}
}En qué consiste el error: Crear una consulta secundaria con new WP_Query() (por ejemplo, publicaciones relacionadas) y omitir wp_reset_postdata() al cerrar el bucle while.
Por qué lo hacen los desarrolladores: El bloque de entradas relacionadas se muestra correctamente; el fallo se manifiesta en partes posteriores de la página.
Qué ocurre en producción: La variable global $post continúa apuntando a la última entrada del bucle secundario. Todas las invocaciones posteriores de Template Tags en barras laterales, comentarios y pie de página (the_author(), get_the_ID(), formulario de comentarios) renderizan datos de esa publicación ajena. Se desconfiguran las migas de pan y las etiquetas Schema.org generan metadatos erróneos para SEO.
Forma correcta: Concluir siempre los bucles secundarios invocando wp_reset_postdata():
$related = new WP_Query(['post_type' => 'post', 'posts_per_page' => 3]);
if ($related->have_posts()) {
while ($related->have_posts()) {
$related->the_post();
the_title('<h4>', '</h4>');
}
wp_reset_postdata(); // ¡Llamada obligatoria!
}En qué consiste el error: Modificar propiedades del objeto global directamente en el cuerpo del ciclo: $GLOBALS['post']->post_title = 'Nuevo Título';.
Por qué lo hacen los desarrolladores: Intentar modificar datos mostrados rápidamente sin implementar los filtros del núcleo.
Qué ocurre en producción: Se rompe la inmutabilidad de los datos en memoria. Los plugins de caché y herramientas de SEO reciben información corrupta. Si el objeto $post se almacena en Redis o Memcached, esa cadena modificada puede persistir infectando solicitudes de otros usuarios.
Forma correcta: Emplear filtros contextuales (the_title, the_content):
// ERROR:
while (have_posts()) {
the_post();
$post->post_title = strtoupper($post->post_title); // ¡Mutación de estado global!
}
// CORRECTO:
add_filter('the_title', function(string $title, int $id): string {
if (in_the_loop() && is_main_query()) {
return mb_strtoupper($title);
}
return $title;
}, 10, 2);En qué consiste el error: Usar query_posts($args) al inicio de una plantilla de archivo o categoría para cambiar la cantidad de entradas o su orden.
Por qué lo hacen los desarrolladores: Es el procedimiento más antiguo y aparentemente sencillo para personalizar la salida.
Qué ocurre en producción: query_posts() destruye el objeto global $wp_query original y ejecuta una segunda consulta pesada contra MySQL. El servidor duplica el trabajo por cada vista, duplicando el TTFB. La paginación canónica (/page/2/) se rompe permanentemente devolviendo 404 Not Found.
Forma correcta: Modificar la consulta antes de acceder a la base de datos mediante el hook pre_get_posts:
add_action('pre_get_posts', function(\WP_Query $query): void {
if (!is_admin() && $query->is_main_query() && $query->is_category('news')) {
$query->set('posts_per_page', 24);
$query->set('orderby', 'date');
$query->set('order', 'DESC');
}
});En qué consiste el error: Consultar con 'posts_per_page' => -1 o 'numberposts' => -1 para mostrar un directorio completo o generar un mapa del sitio.
Por qué lo hacen los desarrolladores: Parece una forma cómoda de asegurar que se devuelven todos los registros.
Qué ocurre en producción: A medida que la web escala de cientos a decenas de miles de publicaciones, MySQL realiza un escaneo de tabla sin límites y PHP intenta instanciar miles de objetos pesados WP_Post. Esto desencadena un error fatal Allowed memory size exhausted, derribando los workers de PHP-FPM.
Forma correcta: Utilizar siempre paginación o procesamiento por fragmentos con Keyset Cursor (ver EnterpriseLoop::cursor).
Evalúa ($this->current_post + 1 < $this->post_count). Si el cursor alcanza el final, invoca rewind_posts() y retorna false.
Si hay registros disponibles y es la primera entrada en el bucle, dispara el hook loop_start.
Desplaza el cursor al índice siguiente. Asigna a $wp_query->post el objeto WP_Post correspondiente del array $posts.
Establece la bandera in_the_loop en true.
Vincula variables globales $id, $authordata, $currentday, $pages, $page, $more y $numpages con la entrada actual.
Obtiene los datos del autor mediante get_userdata($post->post_author) para las etiquetas de autor.
Las funciones the_title(), the_content() y the_permalink() leen el ámbito global preparado sin requerir parámetros.
Los filtros the_title y the_content se aplican contextualmente en cada iteración.
Al finalizar el bucle, el puntero global $post se devuelve al registro de la consulta principal ($wp_the_query->post).
Evita la corrupción de datos en barras laterales, pie de página y metadatos SEO.