skip to content

On a manually built Laravel validator, what do the after(), sometimes() and stopOnFirstFailure() methods add, and when does each take effect?

level: middleimportance: should knowfreq 32%

answer

  1. configure before calling fails()
  2. after(): callables run after all rules
  3. sometimes($field, $rules, fn (Fluent $input))
  4. sometimes closure runs when you call it
  5. stopOnFirstFailure(): stop at first failing field

basics

~20 s

after() registers callbacks that run once the rules finish, to add cross-field errors. sometimes() adds rules to fields only when a closure over the input returns true. stopOnFirstFailure() stops checking further fields after the first failure. All are set before fails() or validate().

solid answer

~30 s

All three are called on the instance returned by `Validator::make()` before evaluating it. `after($callback)` accepts a closure, an invokable object or an array of them; each receives the `Validator` after the rules have run — even if they failed — and can call `$validator->errors()->add()`. `sometimes($attribute, $rules, $callback)` adds `$rules` to one or more fields when the callback, given the data as an `Illuminate\Support\Fluent` (and for wildcard fields the current item), returns true; note it evaluates the callback immediately, against the data passed to `make()`. `stopOnFirstFailure()` makes the validator break out after the first field that fails; `bail` is the per-field version.

code

php · 22 lines
php
<?php

use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Fluent;

$validator = Validator::make($row, [
    'price' => ['required', 'numeric', 'min:0'],
    'sale_price' => ['nullable', 'numeric', 'min:0'],
    'discount' => ['nullable', 'integer', 'between:0,100'],
]);

$validator->sometimes('discount_reason', ['required', 'max:200'], function (Fluent $input) {
    return $input->discount > 30;
});

$validator->after(function ($v) use ($row) {
    if ($v->errors()->isEmpty() && ($row['sale_price'] ?? 0) > $row['price']) {
        $v->errors()->add('sale_price', 'Sale price cannot exceed price.');
    }
});

$bad = $validator->stopOnFirstFailure()->fails();

go deeper

for a junior

Know that after() adds extra checks and sometimes() adds rules under a condition.

for a middle

Explain the callback signatures, that after() runs even on failure, and stopOnFirstFailure() versus bail.

for a senior

Account for sometimes() evaluating at call time and after() needing guards, and choose between sometimes() and Rule::when().

for a principal

Keep conditional validation readable by choosing one place for conditions, so reviewers do not trace rules across construction steps.

## Three configuration methods `Validator::make()` returns an unevaluated `Illuminate\Validation\Validator`. Before you call `fails()`, `passes()` or `validate()`, three methods let you adjust how it will run. ## after(): checks that span fields or need a query ```php $validator->after(function (Validator $validator) use ($row) { if ($row['sale_price'] > $row['price']) { $validator->errors()->add('sale_price', 'Sale price cannot exceed price.'); } }); ``` - Accepts a closure, an invokable object, or an **array** of them (each receives the validator). - Callbacks run **after all rules**, whether or not the rules passed — so guard against missing or malformed values, for example by returning early when `$validator->errors()->isNotEmpty()`. - Adding an error in a callback makes `fails()` return `true`. ## sometimes(): conditional rules on a manual validator ```php $validator->sometimes('discount_reason', ['required', 'max:200'], function (Fluent $input) { return $input->discount > 30; }); ``` - First argument: one field or an array of fields. - Second: rules to add when the condition holds. - Third: a callback receiving the data as `Illuminate\Support\Fluent`. For a wildcard field like `lines.*.reason`, it also receives the **current item** as a second argument, so each element can be judged on its own values. Two things are easy to miss: 1. The callback runs **when you call `sometimes()`**, against the data given to `make()`. If you change the data later (with `setData()`), the decision is not re-evaluated. 2. The method and the `sometimes` **rule** share a name but not a meaning: the rule means "only validate when the key is present"; the method means "add these rules when this closure says so". ## stopOnFirstFailure(): stop at the first bad field ```php if ($validator->stopOnFirstFailure()->fails()) { /* ... */ } ``` - Before moving to the next field, the validator checks whether any error exists and stops if so. - `after()` callbacks still run afterwards. - It differs from the `bail` rule, which stops the remaining rules **of one field**. Use it when you only need to know that the data is bad — for example rejecting a CSV row quickly — and the full list of problems is not shown to anyone. ## Timing summary | Method | When its logic runs | Effect | |---|---|---| | `after($cb)` | during `passes()`, after all rules | callbacks may add errors | | `sometimes($f, $r, $cb)` | immediately, at the call | adds rules to the validator if `$cb` is true | | `stopOnFirstFailure()` | during `passes()` | stops iterating fields after the first error | ## Putting them together for an import row 1. `Validator::make($row, $baseRules)` 2. `->sometimes('discount_reason', 'required', fn ($in) => $in->discount > 30)` 3. `->after(fn ($v) => $this->checkCategory($v, $row))` 4. `->stopOnFirstFailure()` 5. `->fails()` — one boolean per row, with at most one field reported. ## Versus other places for the same logic - In a form request, the equivalents are its `after()` method and its stop-on-first-failure setting; that is where they belong when validating a request. - `Rule::when()` inside the rules array can often replace `sometimes()` and keeps the condition next to the field; `sometimes()` is handy when you add conditions after construction or across several fields at once. ## Pitfalls - Calling `sometimes()` after `fails()` has no effect on the result already computed; configure first. - Expecting `stopOnFirstFailure()` to skip `after()` callbacks — they still run. - Returning a boolean from an `after` callback expecting it to fail validation; only added errors count. ## How to explain the difference quickly `sometimes()` decides **which rules exist**, at the moment you call it. `after()` adds **extra checks** after the rules run. `stopOnFirstFailure()` decides **how far** the rule loop goes. Keeping those three verbs apart — which, extra, how far — is usually enough to answer the follow-ups.

  • Why might a sometimes() condition ignore data you set later with setData()?
    `sometimes()` evaluates its callback immediately against the validator's current data and adds the rules then. Changing the data afterwards does not re-run the callback, so call `sometimes()` after the data is final, or build a new validator.
  • Does stopOnFirstFailure() stop after() callbacks from running?
    No. It only breaks out of the loop over fields. `after()` callbacks still run once that loop ends, so a callback should check whether errors already exist if it depends on valid data.

A manual validator is like a building inspector's checklist: sometimes() adds extra checklist lines for certain buildings before the visit, stopOnFirstFailure() means skipping the remaining rooms once one fails, and after() is the final walk-through of how the parts fit together, which still happens either way.

saying these in an interview costs you the question

  • after() callbacks only run when every rule passed
  • The sometimes() method and the sometimes rule do the same thing
  • stopOnFirstFailure() stops the remaining rules of one field only
  • sometimes() callbacks are re-evaluated every time fails() is called