Custom Fields Logo
3.x
Essentials

Builder Scoping

Scope custom-field resolution to specific sections, and hook into code-uniqueness resolution

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:

  • $entityType identifies the entity that owns the field or section.
  • $type is 'field' or 'section', so each code type can use a different scope.
  • $sectionId identifies the section containing the generated code, or it is null when 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.

Copyright © 2026