
# Repeticiones

Queuety workflows son cadenas ordenadas de pasos. Usa un paso de control de repetición para volver a un paso anterior con nombre, en lugar de escribir a mano la lógica de `_next_step`.

Elige la forma que mejor se adapte al flujo de trabajo:

- un `state_key` público cuando la condición es sencilla
- un `condition_class` cuando la repetición necesita una lógica más elaborada
- `max_iterations` cuando la repetición necesita un límite estricto

Ambos métodos apuntan a un paso anterior con nombre, por lo que la repetición sigue siendo serializable y reproducible, y está protegida por el mismo entorno de ejecución del flujo de trabajo que los demás pasos.

La condición de repetición se evalúa a partir del estado público persistido del flujo de trabajo una vez completado el paso anterior, por lo que cada iteración se conserva aunque se reinicien los workers.

## `repeat_until()` con una clave de estado

```php
Queuety::workflow( 'approval_revision_loop' )
    ->max_transitions( 12 )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->then( NormalizeReviewOutcomeStep::class, 'review_outcome' )
    ->repeat_until( 'draft', 'review_approved', true, 'repeat_draft_until_approved' )
    ->then( PublishBriefStep::class )
    ->dispatch( [ 'brief_id' => 42 ] );
```

`NormalizeReviewOutcomeStep` puede convertir los datos de decisión en una clave pública sencilla:

```php
class NormalizeReviewOutcomeStep implements \Queuety\Step {
    public function handle( array $state ): array {
        return [
            'review_approved' => 'approved' === ( $state['review']['outcome'] ?? null ),
        ];
    }

    public function config(): array {
        return [];
    }
}
```

Si `review_approved` todavía no es `true`, el paso de repetición vuelve a `draft`. Cuando pasa a ser `true`, el flujo de trabajo continúa en `PublishBriefStep`.

Las señales se almacenan, no se consumen. En la segunda pasada por `wait_for_decision()`, la señal anterior de `rejected` todavía coincide y la espera termina de inmediato, por lo que el bucle se repetiría hasta que `max_transitions()` lo detuviera. Asigna una clave a cada ronda: haz que `DraftBriefStep` devuelva un `review_round` incremental, pasa `correlation_key: 'review_round'` a `wait_for_decision()` y haz que el revisor envíe el mismo `review_round` en los datos de la señal. Así, la espera solo termina cuando `review_round` coincide con el valor actual del estado.

## `repeat_until()` con una clase de condición

```php
use App\Workflows\ReviewApprovedCondition;

Queuety::workflow( 'approval_revision_loop' )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->repeat_until(
        target_step: 'draft',
        condition_class: ReviewApprovedCondition::class,
        max_iterations: 5,
        name: 'repeat_draft_until_approved',
    )
    ->then( PublishBriefStep::class )
    ->dispatch( [ 'brief_id' => 42 ] );
```

```php
namespace App\Workflows;

use Queuety\Contracts\RepeatCondition;

final class ReviewApprovedCondition implements RepeatCondition {
    public function matches( array $state ): bool {
        return 'approved' === ( $state['review']['outcome'] ?? null );
    }
}
```

Las clases de condición reciben el estado público del flujo de trabajo, por lo que el predicado se puede serializar e inspeccionar sin depender de cierres.

## `repeat_while()`

```php
Queuety::workflow( 'poll_remote_status' )
    ->max_transitions( 20 )
    ->then( PollRemoteStatusStep::class, 'poll' )
    ->delay( seconds: 30 )
    ->repeat_while( 'poll', 'should_poll_again', true, 'repeat_poll' )
    ->then( FinalizeImportStep::class )
    ->dispatch( [ 'import_id' => 99 ] );
```

Este patrón resulta útil cuando un paso escribe un valor booleano sencillo como `should_poll_again`, `has_more_pages` o `needs_revision`.

## Por qué las repeticiones están separadas de `_next_step`

Aún puedes crear ramas manualmente devolviendo `_next_step` desde un paso normal. Las funciones auxiliares de repetición están pensadas para el caso más concreto en que:

- el destino es un paso anterior
- la condición procede del estado público del flujo de trabajo
- quieres que la lógica de repetición siga visible en la definición del flujo de trabajo

Así resulta más fácil inspeccionar las repeticiones en las exportaciones, los registros de eventos y la documentación que si la conexión de retorno queda oculta en código arbitrario de un paso.

## Cómo limitar las repeticiones

Usa `max_iterations` cuando quieras establecer un límite estricto para una repetición concreta:

```php
Queuety::workflow( 'poll_remote_status' )
    ->then( PollRemoteStatusStep::class, 'poll' )
    ->repeat_while( 'poll', 'should_poll_again', true, max_iterations: 8 )
    ->dispatch();
```

Usa `max_transitions()` cuando necesites un mecanismo más amplio para detener todo el flujo de trabajo:

```php
Queuety::workflow( 'safe_revision_loop' )
    ->max_transitions( 10 )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->then( NormalizeReviewOutcomeStep::class )
    ->repeat_until( 'draft', 'review_approved', true )
    ->dispatch();
```

Si una repetición supera `max_iterations` o el flujo de trabajo supera `max_transitions()`, Queuety marca la ejecución como fallida en vez de dejarla en un bucle infinito.

## Documentación relacionada

- [Ramificación condicional](https://queuety.inline0.dev/es/docs/workflows/conditional)
- [Señales](https://queuety.inline0.dev/es/docs/workflows/signals)
- [Medidas de protección](https://queuety.inline0.dev/es/docs/workflows/guardrails)
- [PHP API](https://queuety.inline0.dev/es/docs/api)
