Introduction
Fields describe one value across forms, tables, detail pages, filters, exports and model mutation.
Add fields to a resource in the same order a developer expects to read the form. Keep validation, display values and mutation rules on the field that owns the value.
TextField::make('title', __('qore::common.title'))
->setRules(fn () => ['required', 'string', 'max:255'])
->setWidth(widthOnForm: FieldWidth::MD, widthOnColumn: 240)
Identity
make(string $name, ?string $label = null): static: create the field.setColumn(string $column): static: use another database column than the field name.setIndexLabel(?string $label): static: use another label for index table columns.fieldComponent(): string: frontend field component name. Override this in custom fields.
Use setIndexLabel() when the normal form/detail label is too long for tables or when the table needs a shorter column heading:
TextField::make('invoice_reference', __('common.invoice_reference'))
->setIndexLabel(__('common.reference'));
The index label only affects index table field metadata. Forms, detail pages, relation table tabs, validation labels and exports continue to use the field's normal label.
Validation
setRules(Closure(?Model): array $callback): static: Laravel rules for the field itself.setExtraRules(Closure(?Model): array $callback): static: extra rules, useful when one field validates multiple payload keys.setIsRequired(): static: add therequiredrule.setIsUnique(string $table): static: add a unique rule for the field's database column and ignore the current model on edit.
Use the shorthand methods for common required and unique fields:
EmailField::make('email')
->setIsRequired()
->setIsUnique('users');
setIsUnique() uses the column configured through setColumn(), rather than the field name. Its unique rule automatically ignores the current model when validating an edit form.
Use setRules() for additional or conditional rules. Its callback receives the current model on edit when the validation needs it:
EmailField::make('email')
->setIsRequired()
->setRules(fn () => ['email:rfc,dns']);
Values
setDefaultValue(Closure(FormFieldContext): mixed $callback): static: initial value on create forms.getDefaultValue(FormFieldContext $context): mixed: resolve the configured default or autofill value.setAutofillValue(?Closure(Generator, FormFieldContext): mixed $callback): static: generated initial value when field autofill is enabled.getAutofillValue(Generator $faker, FormFieldContext $context): mixed: resolve a field's autofill value.setDisplayValue(?Closure(Model): mixed $detailDisplay = null, ?Closure(Model): mixed $indexDisplay = null): static: value for detail/index.setEditValue(Closure(FormFieldContext): mixed $callback): static: value used when editing.getEditValue(FormFieldContext $context): mixed: resolve the value used when editing.setExportValue(Closure(Model): mixed $callback): static: export value.getExportColumns(): array: override when one field exports multiple columns.
Autofill
Field autofill is a development helper for create forms. Enable it in your global Qore configuration:
qore()->fields()->setIsAutofillEnabled(app()->isLocal());
When enabled, Qore asks every field for an autofill value while it builds the initial form defaults. Built-in fields provide useful sample values, and custom fields can opt in with setAutofillValue(). A FormNode defaultFieldValuesCallback bypasses per-field defaults and autofill because it supplies the full default payload itself.
setDefaultValue() always wins. If a field has a default-value callback, Qore uses that callback and does not fall back to autofill, even when the callback returns null. Use setDefaultValue() for real business defaults and autofill only for generated development data.
The autofill callback receives Laravel's Faker generator and the current FormFieldContext:
use Faker\Generator;
use Qore\Next\System\Field\Context\FormFieldContext;
use Qore\Next\System\Fields\TextField;
TextField::make('reference')
->setAutofillValue(
fn (Generator $faker, FormFieldContext $context) => 'REF-'.$faker->unique()->numberBetween(1000, 9999)
);
Pass null to disable autofill for a field while global autofill remains enabled:
TextField::make('external_reference')
->setAutofillValue(null);
Mutation
setMutator(Closure(array, Model): void $mutateCallback, ?Closure(array, Model): void $mutateLazyCallback = null): static: write payload to model.isMutatable(bool $isMutatable = true): static: disable mutation for display-only fields.setMutationLogger(Closure(Model, Model): void $callback): static: customize or disable logbook entries. The first model is the new model, the second is the original model.
Use mutateLazy when the model must exist first, for example file uploads or pivot syncs.
Most fields write to the model column with the same name. Use setColumn() only when the field name and database column intentionally differ.
Forms
setWidth(?FieldWidth $widthOnForm = null, ?int $widthOnColumn = null): staticsetFormLabelClass(?string $class): staticsetInputSize(SizeType $size): staticsetPlaceHolder(string $placeholder): staticsetHint(string $hint): staticsetTooltip(string $tooltip): staticsetIsClearable(bool $isClearable): staticsetLabelVisibleOnForm(bool $visible = true): staticsetDebounceValue(int $ms): static
Global form defaults can be configured through qore()->fields(). A field-level value still wins when it is set directly on that field.
use Qore\Next\System\Node\SizeType;
qore()->fields()
->setDefaultSize(SizeType::LARGE)
->setDefaultFormLabelClass('font-semibold');
TextField::make('reference')
->setFormLabelClass('text-xs uppercase');
Dynamic Forms
onUpdate(Closure(FormFieldContext, mixed $newValue, mixed $previousValue): void $callback, bool $triggerImmediately = false): static: run when this field changes.setDependency(string $fieldName, Closure(FormFieldContext): void $callback): static: run when another field changes.onFormRequest(Closure(FormFieldContext): void $callback): static: register a callback that runs during the form request lifecycle before serialization. Multiple callbacks run in registration order.onFormResponse(Closure(FormFieldContext): void $callback): static: run during the form response lifecycle.setFieldIsHidden(?Closure(FieldContext): bool $callback, array $dependencies = []): staticsetFieldIsDisabled(Closure(FormFieldContext): bool $callback, array $dependencies = []): staticsetFieldIsReadOnly(Closure(FormFieldContext): bool $callback, array $dependencies = []): staticsetFieldIsMarkedAsRequired(Closure(FormFieldContext): bool $callback, array $dependencies = []): static
The setFieldIsHidden() callback receives a FieldContext, because it can run for index, detail, and form contexts. Form-only callbacks receive a FormFieldContext directly. Every context exposes its field through getField(). Index callbacks use an IndexFieldContext, detail callbacks a DetailFieldContext with getModel(), and form callbacks a FormFieldContext with getForm() and a nullable getModel(). Use getModelOrFail() in form callbacks that require a model.
FormFieldContext and DetailFieldContext extend ValueFieldContext, which provides getValue(), getValueAsString(), and getValueAsEnum(). On a form, getValue() reads the current raw value from the live form state; on a detail page it returns the field's transformed detail display value. Narrow a general FieldContext with instanceof FormFieldContext or instanceof DetailFieldContext before reading a value. IndexFieldContext deliberately has no value contract.
The concrete context type describes where a callback is currently running. It is independent of Field::isForIndex(), Field::isForDetail(), and Field::isForForm(), which describe how the mutable field instance has already been prepared for rendering. A form context without a model is not necessarily a resource create page; it can also represent a custom form.
The form request lifecycle runs before the node is serialized. It has already loaded default field values and handled field updates, so it is the right place to inspect current values, hide or disable fields, and mutate values before the frontend receives the response.
The form response lifecycle runs after the request lifecycle and just before FormNode::onFormReady(). Use it when a field needs to add response-only data after request handling is complete, such as lazy options or calculated UI metadata.
SelectField::make('type')
->setOptions([...]);
TextField::make('vat_number')
->setDependency('type', function (FormFieldContext $context) {
// Recalculate when type changes.
})
->setFieldIsHidden(
fn (FieldContext $context) => $context instanceof FormFieldContext
&& $context->getForm()->getFieldValue('type') !== 'company',
dependencies: ['type']
);
Field Lifecycle Examples
Use onUpdate() on the field that changed when you want to mutate another field immediately:
SelectField::make('customer_id')
->setOptions(fn (SelectMetadata $metadata) => $this->customerOptions($metadata))
->onUpdate(function (FormFieldContext $context, mixed $customerId) {
$form = $context->getForm();
if (! $customerId) {
$form->setFieldValue('invoice_email', null);
return;
}
$customer = Customer::query()->find($customerId);
$form->setFieldValue('invoice_email', $customer?->invoice_email);
});
Pass triggerImmediately: true when the field should fill dependent values as soon as the form first opens:
SelectField::make('customer_id')
->setDefaultValue(fn () => request()->integer('customer_id') ?: null)
->onUpdate(function (FormFieldContext $context, mixed $customerId) {
$form = $context->getForm();
$customer = Customer::query()->find($customerId);
$form->setFieldValue('invoice_email', $customer?->invoice_email);
}, triggerImmediately: true);
Use setDependency() on the field that depends on another field. This is useful when the dependent field owns the recalculation:
TextField::make('invoice_email')
->setDependency('customer_id', function (FormFieldContext $context) {
$form = $context->getForm();
$customer = Customer::query()->find($form->getFieldValue('customer_id'));
$form->setFieldValue('invoice_email', $customer?->invoice_email);
});
Use onFormRequest() when the field needs to inspect or mutate form state on every request:
TextField::make('reference')
->onFormRequest(function (FormFieldContext $context) {
$form = $context->getForm();
if (! $form->isInitialRequest()) {
return;
}
if ($form->getFieldValue('reference')) {
return;
}
$form->setFieldValue('reference', ReferenceService::makeNextReference());
});
Use onFormResponse() when the field needs response-time data after request handling:
SelectField::make('contact_id')
->setShouldLoadOptionsImmediately(false)
->onFormResponse(function (FormFieldContext $context) {
$form = $context->getForm();
$customerId = $form->getFieldValue('customer_id');
if (! $customerId) {
return;
}
$form->setFieldValue('contact_hint', "Showing contacts for customer #{$customerId}");
});
Use setFieldIsHidden(), setFieldIsDisabled() and setFieldIsReadOnly() for context-aware field behavior. Pass dependencies so Qore knows which field changes should refresh this behavior:
TextField::make('vat_number')
->setFieldIsHidden(
fn (FieldContext $context) => $context instanceof FormFieldContext
&& $context->getForm()->getFieldValue('type') !== 'company',
dependencies: ['type'],
);
TextField::make('invoice_email')
->setFieldIsReadOnly(
fn (FormFieldContext $context) => $context->getModel()?->invoice_sent_at !== null,
dependencies: ['customer_id'],
);
setFieldIsHidden() is also checked while resource fields are selected for pages. Its concrete FieldContext distinguishes form, detail, and index execution; a form context does not independently distinguish create from edit:
TextField::make('internal_notes')
->setFieldIsHidden(fn (FieldContext $context) => match (true) {
$context instanceof FormFieldContext => ! user()->can('update', $context->getModel() ?? Invoice::class),
$context instanceof DetailFieldContext => ! user()->can('viewInternalNotes', $context->getModel()),
default => ! user()->can('view-any-internal-notes', Invoice::class),
});
Resource pages call getAuthorizedFields() before applying page-specific visibility. This removes relational fields when the current user cannot view-any the related resource, while regular setFieldIsHidden() callbacks remain responsible for context-aware visibility.
Fields are automatically marked as required when their rules contain required. Use setFieldIsMarkedAsRequired() when the visual marker depends on form state or a complex rule such as required_if:
TextField::make('vat_number')
->setRules(fn () => ['required_if:type,company', 'string'])
->setFieldIsMarkedAsRequired(
fn (FormFieldContext $context) => $context->getForm()->getFieldValue('type') === 'company',
dependencies: ['type'],
);
Inside lifecycle callbacks, the Form object gives access to the form values, model, resource, parent relation context and node manager:
$form->getFieldValue('customer_id');
$form->setFieldValue('invoice_email', 'finance@example.test');
$form->getModel();
$form->getResource();
$form->getParentModel();
$form->getParentFieldValue('customer_id');
$form->getNodeManager();
Resource Visibility
setIsShownOnCreate(Closure(): bool $callback): staticsetIsShownOnEdit(Closure(Model): bool $callback): staticsetIsShownOnDetail(Closure(Model): bool $callback): staticsetIsShownOnIndex(Closure(): bool $callback): staticsetIsShownOnForms(Closure(): bool $callback): staticsetIsOnlyShownOnCreate(): staticsetIsOnlyShownOnEdit(): staticsetIsOnlyShownOnDetail(): staticsetIsOnlyShownOnIndex(): staticsetIsOnlyShownOnForms(): staticsetIsVisibleInTablesByDefault(bool $value): static
Tables
setIsFilterable(bool $value = true): staticsetFilter(Closure(Builder, mixed): void $callback): staticsetIsSortable(bool $value = true): staticsetSorter(Closure(Builder, TableSortOrder): void $callback): staticsetWidthOnColumn(?int $widthOnColumn): staticsetMinWidthOnColumn(?int $minWidthOnColumn): static
Use custom filters/sorters when the field value is not a plain column.