Conversation
isNested carries two meanings: "this Form widget is a child of another Form widget", which extension guards use to avoid injecting fields on the wrong instance, and "this form's data scope differs from the model's attributes", which core and Winter.Translate use. Both hold for repeater and nestedform; for fieldset only the first does, since it reuses the parent form's model and arrayName and merges its save data straight back up. Conflating them means Winter.Translate skips fieldset fields, so translatable attributes grouped in a fieldset never get their ml* widget, and the nested attribute-name prefixing in defineFormField() asks the model for '.field_name' instead of 'field_name', so required is never auto-detected from model rules. Keeping isNested true leaves every existing guard behaving as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Guards both halves of the change: the inner form must stay flagged as nested so that code extending form widgets can still tell it apart from the form it is nested in, and its fields must resolve required against the plain model attribute rather than one prefixed with the inherited arrayName. A regular nested form is asserted to keep prefixing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Suppressing the attribute-name prefixing outright was wrong: a fieldset inside a repeater item inherits that item's array name, and the prefix derived from it is exactly what isAttributeRequired() needs. Only the top level case is broken, where a bare parent array name leaves nothing to prefix with and produced ".field_name". Guard the empty prefix instead, which needs no flag at all. For the same reason the flag now answers whether the fields resolve on the model, which a fieldset only does when the form it is nested in does too. Renamed accordingly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
setFormValues() fills the model as if the form were about to be saved, so that the fields resolve against it. A repeater item form and a nested form are handed the parent form's model to render against, but their fields are keys of their own data scope, not attributes of that model: filling it from there writes foreign values onto the record whenever a field shares its name with an attribute. It goes unnoticed for a plain attribute - the model is not saved on a refresh - but a cast attribute rejects the value outright, so a repeater group holding a field named after an enum-cast attribute makes refreshing or switching the repeater throw. Guarded with the scope discriminator, so fieldsets, whose fields really are the model's attributes, keep filling it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. WalkthroughThe Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to Independent nested fields no longer populate matching parent-model attributes, while shared fieldsets retain their existing model behavior. No actionable merge risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Builds on #1529 — that PR adds the
Form::$sharesModelScopediscriminator this one guards with, so the diff below includes it. Happy to rebase once #1529 lands, or to fold this into it if you'd rather have one PR.The problem
Form::setFormValues()fills the model as if the form were about to be saved, so the fields resolve against it:A repeater item form and a nested form are handed the parent form's model to render against (
Repeater::makeItemFormWidget(),NestedForm::init()), but their fields are keys of their own data scope, not attributes of that model. Filling it from there writes foreign values onto the record whenever an item field shares its name with a model attribute — andtype,name,title,statusare exactly the names repeater groups tend to use.For a plain attribute it goes unnoticed, since nothing saves the model during a refresh. For a cast attribute it fails outright:
Refreshing the repeater (or switching/copying items, which is where we hit it) throws:
The value never belonged to the model — it was only ever the item's.
The fix
isNestedis set in exactly three places in core:Repeater,NestedFormandFieldSet. The first two bring a data scope of their own and must not touch the model; a fieldset groups fields visually and its fields really are the model's attributes, which is what$sharesModelScopealready flags. Top-level forms are unaffected, soRelationControllerand settings forms keep filling the model as before.Tests
modules/backend/tests/widgets/FormNestedModelScopeTest.phpputs a field namedstatuson the model, on a fieldset, on a nested form and in a repeater group, and asserts which of the four are allowed to reach the model's attribute. The two nested cases throw theValueErrorabove without the guard.🤖 Generated with Claude Code
Summary by CodeRabbit