Conditional flow
Conditional flow
The action chain is a small query builder for the page. Besides the linear actions, you can branch and loop, and the condition is evaluated by Puppeteer against the live page at runtime.
PHP is not inside the browser, so you do not write the check as a closure over the DOM. You describe what to check with the Condition helper, and Node does the checking.
when
Runs a branch only if a condition holds. The closure receives a sub-builder you chain actions on, exactly like Laravel's $query->when():
use EduLazaro\Larascraper\Support\Condition; protected function handle(string $url): ScraperResponse { return $this->scrape($url) ->when( Condition::selectorExists('#cookie-banner'), fn ($b) => $b->click('#accept-cookies'), ) ->crawl(ProductCrawler::class) ->run(); }
The else branch is optional, and rarely needed, since usually you just continue the main chain afterwards:
->when( Condition::textContains('No results', '.notice'), fn ($b) => $b->click('#clear-filters'), // then fn ($b) => $b->waitForSelector('.product'), // else )
Guarding an action with when() is what keeps an optional step from breaking a run. A click('#accept-cookies') on a page that has no banner is a failed action and therefore a failed fetch.
repeatUntil
Repeats a branch until a condition holds, for "retry until it works" flows such as solving a captcha or paginating.
It is always bounded. max defaults to 5 and is clamped to at least 1; there is no unbounded mode. delay throttles the time between iterations so you do not hammer a server:
->repeatUntil( Condition::selectorMissing('#captcha-img'), // stop once the captcha is gone fn ($b) => $b ->solveCaptcha('#captcha-img', '#captcha-input') ->clickAndWait('#verify'), max: 6, delay: 1500, )
A failed attempt is not a failed run
If the body throws mid-iteration, a transient page where an expected element is missing, or a gotoAttr() that finds no PDF this pass, that counts as one failed attempt. The loop re-checks the condition and retries on the next pass, bounded by max, instead of aborting the whole fetch. Only when every attempt is exhausted and the condition still never holds is the last error surfaced.
That is what makes loops resilient when each pass navigates to freshly generated state: a new captcha image, a re-issued session, a token that expires between attempts.
The conditions
| True when | |
|---|---|
Condition::selectorExists($selector) |
an element matching the selector exists |
Condition::selectorMissing($selector) |
no element matching the selector exists |
Condition::textContains($text, $selector = null) |
the text is found, in $selector or in the whole page |
Condition::urlContains($text) |
the current URL contains the substring |
Condition::captured() |
a file or binary has been captured, to pair with capture() in a loop |
Choosing the right condition
Write the condition as the outcome you want, not as the step you just performed. selectorMissing('#captcha-img') says "the captcha is gone", which is true exactly when the attempt worked. Something like "the submit button was clicked" is true whether or not anything came of it, and the loop exits on the first pass having achieved nothing.
The same applies to captured(): it becomes true the moment a file is grabbed, so a download loop stops on success and gives up after max attempts, with no bookkeeping of your own.