Skip to main content

Flex Field Groups

← Back to Table of Contents

Why these migrations exist

Filament Flex Fields ships three optional database tables as part of the M8 schema product. They are not required for normal field usage, AudioField, or HasFlexFields with PHP/config schemas. The package registers migrations via loadMigrationsFrom, so php artisan migrate on a host app creates the tables even when the admin resource stays disabled. That is intentional: empty tables are harmless; enabling the feature later does not require a separate migration step. You can also publish migrations explicitly:

The three tables

flex_field_groups

Stores editable field group definitions for the optional Filament resource. Model: Bjanczak\FilamentFlexFields\Models\FlexFieldGroup Unique constraint: (tenant_id, slug) — same slug may exist for different tenants; global groups use empty string '' (not SQL NULL).

flex_field_schema_versions

Append-only version history for SchemaRegistry (enterprise control plane), not for individual Eloquent records. Model: Bjanczak\FilamentFlexFields\Models\FlexFieldSchemaVersion Generic SchemaRegistry::publish() calls may omit flex_field_group_id; group publishes always set it. Use $group->schemaVersions() for the relationship.

flex_field_schema_heads

Stores the last issued and current live version for each schema ID. A locked head serializes publication and prevents competing editors from issuing the same version. It also retains issued version numbers when history is explicitly purged. Install its migration together with flex_field_schema_versions when using persistent snapshot history.

Architecture (what talks to what)

SchemaRegistry (enterprise versioning) is separate from FlexFieldSchemaRegistry (runtime form builder), but Field groups sync into the runtime registry automatically when schema.sync_on_save / schema.sync_from_database are enabled (default).

Do I need to run migrations?

If you never enable the resource and never call SchemaRegistry::publish(), the tables stay empty and the package behaves like a config-only install.

Enable the Filament resource

  1. Run migrations (see above).
  2. Turn on the resource:
Or in config:
  1. Keep FilamentFlexFieldsPlugin on your panel — it registers FlexFieldGroupResource when the flag is true.
Navigation: Field groups (/flex-field-groups slug under your panel path).

Flex Field Studio (Beta)

Flex Field Studio is in beta and is not ready for production use. Its interface and workflows are incomplete and may not work reliably. Keep the Studio page disabled in production. This beta notice applies to the Studio management page, not the FlexFieldStudio PHP integration API described below.
When schema.management_page_enabled is true, Flex Field Studio (Beta) lists groups per discovered entity. Each card shows the latest SchemaRegistry version and approval state (draft, review, or live) when the group has been published; otherwise it reads Not published. The Studio flag currently defaults to true, but the page is registered only when schema.resource_enabled is also enabled. To use the Field groups resource with Studio disabled, set:
Or set schema.management_page_enabled to false in config/filament-flex-fields.php. After changing a cached configuration, run php artisan config:clear.

SchemaRegistry persistence

When schema.registry_persistence is true (default) and flex_field_schema_versions exists, publish/rollback writes to the database. Otherwise SchemaRegistry uses in-memory storage (fine for unit tests).
States: draft, review, live. Rows published via $group->publishToRegistry() store flex_field_group_id and are available through $group->schemaVersions().

Runtime sync (automatic)

When enabled (default), groups stay aligned with FlexFieldSchemaRegistry: Key collision: DB groups registered after config schemas use the same string key — a DB group with slug crm overwrites a config schema keyed crm. Use distinct slugs or disable sync_from_database.
Disable either flag if you prefer manual registration in your service provider.

One-line Filament integration (FlexFieldStudio)

After a model uses HasFlexFields and field groups are synced to the runtime registry, wire forms, tables, and infolists without hand-building each component:

Sections in runtime forms

When a field group defines sections in the admin UI, use flexFieldFormLayout() (instance or static) to render one Filament Section per schema section, plus an optional catch-all section for ungrouped fields:
flexFieldFormSection() remains available for backward compatibility: it returns a single section wrapping the layout when multiple sections exist. Import, export, publish, and rollback all preserve sections and section_id on fields. Tenant-aware resolution uses FlexFieldSchemaResolver (global schemas + tenantId:slug scoped groups). Field types can be limited per tenant via TenantFieldPacks::registerPack() and per user via FieldRbacMatrix.

Filament admin workflow

On Create / Edit field group, header actions include: The repeater supports slug, label, type, sort, required flag, help text, placeholder, config JSON, and JSON condition rules (visible_when, required_when, disabled_when). Saves run through FlexFieldGroupValidator (duplicate slug detection, type checks, FlexFieldDefinition parsing). Programmatic API remains on the model:

Tenant resolver & admin scoping

Register how the active tenant is resolved for schema filtering and optional admin list scoping:
When scope_resource_by_tenant is true, the Field groups list shows global groups (tenant_id = '') plus groups for the resolved tenant.

FlexFieldGroup helpers

toRegistrySchema() returns a payload compatible with SchemaImportExport / blueprint packs. Set target_type on each group (or FLEX_FIELDS_SCHEMA_DEFAULT_TARGET) so HasFlexFields resolves the correct Eloquent model.

Security & RBAC

Default policy requires Gate ability manageFlexFieldSchemas (configurable):
Custom policy:
Publish and rollback actions authorize publish / rollback on the policy. Enable scope_resource_by_tenant or add a custom policy / global scope for stricter tenant isolation.

Slug uniqueness (multi-tenant)

  • Database: unique index on (tenant_id, slug) with tenant_id = '' for global groups (no MySQL NULL duplicate slugs).
  • Filament form: unique rule scoped to the same tenant_id value.
The same slug may exist for different tenants; not twice for the same tenant (including global).

Configuration reference


Tests

tests/Schema/FlexFieldStudioTest.php, tests/Schema/FlexFieldGroupResourceTest.php, tests/Schema/FlexFieldGroupRegistrySyncTest.php, tests/Schema/FlexFieldGroupEnterpriseTest.php, tests/Unit/EnterpriseControlPlaneTest.php — resolver/tenant/RBAC, FlexFieldStudio form+table builders, validation, CRUD, registry sync, publish/rollback/import/blueprint Livewire actions, composite slug uniqueness.
  • JSON schemas in config: config/filament-flex-fields.php → schemas
  • Runtime builder: HasFlexFields, FlexFieldFormBuilder, FlexFieldSchemaRegistry, FlexFieldStudio
  • Blueprint packs: SchemaBlueprintPacks (CRM / HR / Booking / …)
  • Import / export: SchemaImportExport
  • JSON conditions: Schema conditions
  • Tenant access policies: TenantFieldPacks, FieldRbacMatrix
  • Component monitoring: Laravel application events

Detect simultaneous schema edits

For an editor or API that publishes snapshots, remember the schema’s version when opening it and pass that version when saving. Zero means no snapshot has been published yet. Scope the schema ID to your tenant in application code.
The version comparison occurs within the publication transaction, while the schema head is locked. A conflict leaves history and the version counter unchanged. This also covers the first publication. Purging history does not reuse previously issued version numbers. Guard rollback in the same way:
Existing calls without expectedVersion retain their publication behavior. Merely reading currentVersion() does not activate conflict detection. This guard applies to schema snapshots, not automatically to ordinary record edits, the field-group editor or every application transaction. Persistent versioning requires both the schema versions and schema heads migrations. Request-local in-memory history is intended for isolated uses; it is not durable shared editor storage.

Validate exchanged schemas before saving

Import, export, dry-run validation and publication share the built-in payload contract. Field-group saving also checks that contract after normalizing its form settings. Unknown versions, unexpected schema/field metadata, invalid literal paths, overlapping field paths and invalid flags are rejected. The known legacy required flag remains readable; explicit is_required or isRequired takes precedence. Version-zero migration accepts the documented old field, name, default and required aliases. It preserves an explicit canonical null or false and removes redundant legacy aliases. Invalid records and future versions are rejected instead of being replaced by empty fields. Authorize importing/configuring a schema separately. Verify that its target matches an application-approved target before applying it. The target is data; import does not instantiate that class or grant access to its records. Built-in configuration shape checks do not certify every component’s business rules. For external families, use their typed family schema codec and configured adapter. Boolean switches must use true or false, including encryption settings. Numeric limits must be finite, steps positive, and minimums cannot exceed maximums. Numeric strings accepted by the field configurators remain supported. Length limits must be nonnegative integers or integer strings. Optional empty widths use the normal full-width layout; other widths must be supported enum values. These checks also run when a field group is saved, before it is stored or published.

Apply validated values to a record

Use applySubmittedFlexFieldValues() on a model using HasFlexFields when accepting values from an API or another interface outside a Filament form. Define the rules and authorization policies in your application, rather than accepting them in the request. This method applies a patch: omitted fields keep their existing values, while an explicitly submitted null clears a field whose rules allow it.
The same method works on a new model; use your create policy as the record policy and assign its tenant in trusted application code. Validation and authorization complete before any values are changed. A rejected patch leaves the model values unchanged. The method does not save automatically; call save() inside your transaction when the patch is accepted. Nested rules use relative paths such as *.title or *.tags.*. Validate the parent container too: array:title,tags rejects unexpected map keys, list requires a consecutive list, and max:20 limits its length. Use present when an empty array is allowed. Expansion is limited to 4,096 validation paths and 65,536 rules for one submission. Overlapping expanded patterns are rejected. Encrypted storage definitions automatically enable encryption on contracts for those exact field paths. Text resembling encrypted data is treated as incoming text and encrypted again as a new value. Declare an encrypted field as its own contract; do not patch through its parent or child path. Existing stored ciphertext in omitted fields remains unchanged. This API keeps record permission, tenant selection, schema selection and transaction control in your application. The trusted setters setFlexFieldValue() and setFlexFieldValues() still prepare storage values; they do not perform these request-level permission checks. See value submission and data limits for saving values outside a form. Laravel documents the underlying transaction and row-locking behavior.

Configure approved application references

Saved field configuration accepts the settings declared for that field type. Unknown names, invalid values and conflicting aliases are rejected before a group is saved or a form is built. Before updating an existing installation, review old schemas and remove or migrate settings that are no longer used. The package does not silently delete your saved settings. If a dynamic schema selects an application model, custom option view or Livewire method, approve that reference in your application configuration. For example:
models covers UserSelect’s option_model (and its model alias) and Slug’s spatie_model. views covers option_view and option_trigger_view. methods covers VerificationCode’s auto_submit_method and TodoList’s remote_loader. Each category accepts up to 128 explicit references for that field type. References are denied by default; adding one to an imported file does not approve it. These lists apply to dynamic schema configuration. You can still configure a component directly through its PHP methods. Approving a reference does not grant access to records: keep your configure/view/edit policies, tenant restrictions and model query scopes in place. To find obsolete configuration names before deploying an update:
This check returns the unknown names and leaves your data unchanged. Normal schema validation also checks the values, nested records, reference approvals and formula dependencies. A formula stored in config.formula or config.calculated receives the same expression and dependency checks as a formula in the field metadata.
Last modified on October 16, 2026