Builder Scoping
onlySections()
By default, every builder (CustomFields::form(), ::infolist(), ::table(),
::exporter(), ::importer()) resolves fields for the whole entity type. onlySections()
constrains that resolution to a specific set of custom_field_sections rows:
use Relaticle\CustomFields\Facades\CustomFields;
CustomFields::form()
->forModel($model)
->onlySections([$sectionA->id, $sectionB->id])
->build();
Passing [] means "no scope." Existing call sites that never call onlySections() are
unaffected.
This supports consumers that version their form definitions. For example, they may clone
a section and its fields per form version. Field codes are normally unique per entity
type, so two versions of "the same field" would collide on code unless resolution is
scoped by section. onlySections() lets each version's builder only see its own
section(s), so the same code can live in more than one section without one bleeding into
the other's schema.
onlySections() is inherited by every builder from BaseBuilder, including through
FormBuilder::build() and InfolistBuilder::build(). Containers preserve the scope, so
it applies whether you call ->build() or ->values().
Combining scopes and filters
onlySections(), only(), and except() constrain the metadata query first. Section
filters then select sections, and field filters select fields from the surviving
sections. Empty sections are removed.
Table, Infolist, and Exporter builders expose the selected metadata through
getSections() and getFields(). These methods run filters before component factories
and record-specific conditional visibility.
Calling onlySections() again replaces the previous section scope. Calling it with an
empty array clears that scope. Reusing only(), except(), or forModel() also reloads
metadata with the new constraint.
The persistence contract
onlySections() narrows resolution (which fields get loaded onto a form, infolist, or
table). It does not change how values are saved. UsesCustomFields::saveCustomFields()
iterates the model's custom-field-values relationship and writes each submitted value by
field code. If two sections share a code and that relationship isn't scoped to match
onlySections(), saveCustomFields() will silently write the same value to both
field rows. That corrupts data even when resolution scoping works correctly.
If you use onlySections(), scope your model's custom-field-values relationship
(customFields() by default, or your override) to the same section(s). It is
entity-scoped by default and overridable per model.
CodeGenerator::resolveUniquenessScopeUsing()
Auto-generated codes (FIELD_CODE_AUTO_GENERATE) are checked for collisions before use.
Register a callback to narrow that check the same way onlySections() narrows
resolution:
use Illuminate\Database\Eloquent\Builder;
use Relaticle\CustomFields\Support\CodeGenerator;
CodeGenerator::resolveUniquenessScopeUsing(
fn (string $entityType, string $type, int|string|null $sectionId): ?Closure => $sectionId !== null
? fn (Builder $query): Builder => $query->where('custom_field_section_id', $sectionId)
: null
);
The callback receives:
$entityTypeidentifies the entity that owns the field or section.$typeis'field'or'section', so each code type can use a different scope.$sectionIdidentifies the section containing the generated code, or it isnullwhen there isn't one (for example, the sectionless field-management table).
Return null to leave the uniqueness check global. This is the default behavior.
Return a closure to narrow it. The closure receives the in-progress Builder and must
return the query to apply. where() mutation works because it returns the same
instance. Returning another instance, such as $query->clone()->where(...), also works.
Register the callback once, typically in a service provider's boot() method.
See Extending for filterFieldsUsing() / filterSectionsUsing(),
which narrow resolution by an arbitrary predicate rather than by section identity.