livewire/livewire: Skip redundant form rule checks during validation (#10713)
Contributed by calebporzio
Part of the livewire v4.4.7 release
Category: Fix (3)
Pull request #10713 trims a slice of redundant work out of Livewire's validation pipeline. Until now, every call to validate() on a component with form objects forced Livewire to build each attached form object's rules — even when component-level or explicitly provided rules had already answered the question "are there any rules to run?" The fix is a single added condition in the validation handler, first flagged as wasted work in discussion #10331. If you're upgrading from v3.8.10 into the 4.x line, there are no migration steps and no change in validation results — but form-heavy components stop paying for a rules build they never used.
The background: a question answered twice
Livewire's validation handler, providedOrGlobalRulesMessagesAndAttributes(), assembles the rule set for a validation call from several sources: rules handed directly to validate([...]), the component's own rules (from #[Validate] attributes or a rules() method), and — for root components — the rules of any attached form objects. One job of that merged set is a simple existence check: does this component have anything to validate at all?
The problem was ordering. The merge of form-object rules ran unconditionally, whether or not the answer was already known. A component with its own rules — or a call like $this->validate(['username' => 'required']) — still made every form object build its full rules array, only for that result to confirm something Livewire already knew. On forms whose rules() merges arrays or resolves conditional rules, that's a real (if small) tax on every validation pass.
The change: one condition
The entire production diff is a guard on the form scan:
// src/Features/SupportValidation/HandlesValidation.php
-if ($this->isRootComponent()) {
+if (empty($rules) && $this->isRootComponent()) {
foreach ($this->getFormObjects() as $form) {
$allRules = array_merge($allRules, $form->getRules());
}
Form objects are now consulted for the missing-rules check only when the component/provided rules are empty — exactly the case where their rules are the only possible answer. Two things are deliberately preserved: the isRootComponent() guard, and actual form validation itself. When a validation call genuinely targets a form's fields, the form's rules are still built and applied as before; this change only removes the duplicate build used for the existence check.
What it looks like in practice
use Livewire\Component;
use Livewire\Form;
class PostForm extends Form
{
public string $title = '';
public string $content = '';
public function rules()
{
return [
'title' => 'required|min:3',
'content' => 'required',
];
}
}
class SavePost extends Component
{
public PostForm $form;
#[Validate('required')]
public string $username = '';
public function save()
{
$this->validate(); // validates $username *and* $form->title/$form->content
}
}
The PR measures the effect by counting calls to a form's rules() method:
$this->validate()on a component that also has its own rules: formrules()calls drop from three to two — and errors for both the component field (username) and the form field (form.content) are still returned, so component and form validation continue to run side by side.$this->validate(['username' => 'required']): the one unnecessary form-rule build drops to zero, and only the explicitly requested field is validated (form.contentcorrectly reports no errors).
Both scenarios are locked in by new regression assertions in SupportFormObjects/UnitTest.php, using a PostFormWithCountedRules stub that counts rules() invocations. Both assertions failed before the change and pass after.
Upgrade impact from v3.8.10
There is nothing to migrate: no API changes, no config changes, and no change to which fields are validated or which errors come back. The only observable difference is that a form's rules() method may now be called fewer times per validation call. If your form's rules() is a pure rule array — as it should be — you'll notice nothing. If it has side effects (logging, counters, state mutation), it will now run only when its rules are genuinely needed, which is almost certainly what you want anyway.
The PR is also honest about scope: no overall application speedup is claimed. This removes one redundant rules build per form from these validation calls — meaningful for forms with expensive rules() methods, invisible elsewhere. The form-object and validation suites are green after the change: 130 passed, 1 pre-existing skip, 596 assertions, with a clean git diff --check.
Takeaways
- Form-object rules are now scanned for the "any rules at all?" check only when component/provided rules are empty; real form validation is untouched.
- Validation results are identical — component and form errors still come back together from
$this->validate(). - Measured effect: form
rules()calls fall from 3 → 2 when component rules exist, and from 1 → 0 when rules are passed explicitly tovalidate(). - Upgrading from v3.8.10: zero migration steps; only side-effectful
rules()implementations will notice anything.