Relation Fields
Relation fields extend SelectField, so they support select options, lazy option loading, tags, add buttons, filters and form dependencies.
They also implement relation behaviour:
setRelatedResource(QoreResource $resource): staticsetModelClass(string $modelClass): staticsetRelationshipName(string $name): staticgetRelatedModelKeys(Model $model): arraysetEagerLoader(Closure(Builder): void $callback): staticsetRelationType(string $type): staticsetIsDisplayedAsTable(bool $value = true): staticfor fields with relation tables.setTableNode(Closure(DetailFieldContext): Node $callback): staticfor custom relation tables.setCanAttach(bool $value = true): staticfor relation tables that support attaching existing records.setCanDetach(bool $value = true): staticfor relation tables that support detaching selected records.
Most relation configuration is guessed by Qore by default. Configure it explicitly when the field name, relationship name or related resource is not obvious.
getRelatedModelKeys() resolves the current relation to a flat list of raw model keys. It returns one key for a singular relation, all keys for a collection relation, and an empty list for an empty relation. Relation attach and detach flows use this contract so they never merge serialized display values into mutation payloads.
Use setEagerLoader() when the display value needs the relation loaded on index/detail/export:
BelongsToField::make('customer')
->setRelatedResource(qore()->getResourceOrFail('customers'))
->setEagerLoader(fn (Builder $builder) => $builder->with('customer'));
Options from a Resource
When a relation field has a related resource, its select options come from that resource by default. QoreResource::getOptionsCallback() returns models from optionsQuery() by default and may return any iterable of supported option values. Models that implement SelectOptionable are converted through toSelectOption(); other models are mapped through their registered resource's getSelectOption() method. Call getOptions(SelectMetadata $metadata) when you need the normalized SelectOption values directly.
optionsQuery() defaults to indexQuery(), so relation options use the same base query as the resource index unless the resource overrides it. The same options query is used when Qore resolves selected values for lazy-loaded relation fields, builds attach-existing forms and resolves the submitted attach-existing model.
Override modelTitle() when the default {label} #{id} label is not useful:
public function modelTitle(Model $model): string
{
return $model->name;
}
Override modelDescription() when relation options need a short second piece of context:
public function modelDescription(Model $model): ?string
{
return $model->email;
}
Override optionsQuery() when relation options need different scoping, ordering or eager loading than the index table:
use Illuminate\Database\Eloquent\Builder;
public function optionsQuery(): Builder
{
return parent::optionsQuery()
->where('is_active', true)
->orderBy('name');
}
Override getSelectOption() when relation options need a custom label or description shape. Keep the option value as the model key, because relation fields and attach-existing forms resolve submitted values by key:
use Illuminate\Database\Eloquent\Model;
use Qore\Next\System\Field\Select\SelectOption;
public function getSelectOption(Model $model): SelectOption
{
return new SelectOption(
value: $model->getKey(),
label: "{$model->code} - {$model->name}",
description: $model->email,
);
}
Override getOptionsCallback() when the relation field needs request-aware searching, limits or a custom set of options:
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Qore\Next\System\Field\Select\SelectMetadata;
/**
* @return Closure(SelectMetadata): Collection<int, Model>
*/
public function getOptionsCallback(): Closure
{
return function (SelectMetadata $metadata) {
return $this->optionsQuery()
->where('name', 'like', "%{$metadata->getSearch()}%")
->limit(20)
->get();
};
}
For example:
BelongsToField::make('customer')
->setRelatedResource(qore()->getResourceOrFail('customers'))
->setShouldLoadOptionsImmediately(false)
BelongsToField
For belongsTo. Stores a foreign key on the current table. Default column is snake-case {name}_id.
BelongsToField::make('customer')
->setRelatedResource(qore()->getResourceOrFail('customers'));
Migration:
$table->foreignId('customer_id')
->nullable()
->constrained()
->nullOnDelete();
Model:
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
BelongsToManyField
For belongsToMany. Syncs the pivot after the model has been saved.
BelongsToManyField::make('roles')
->setRelatedResource(qore()->getResourceOrFail('roles'))
->setIsDisplayedAsTable();
Migration:
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->primary(['role_id', 'user_id']);
});
Model:
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
HasOneField
For hasOne. Hidden on forms by default. Usually shown on detail pages.
Migration on the related table:
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
Model:
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
HasManyField
For hasMany. Hidden on forms by default and displayed as a relation table by default.
HasManyField::make('orders')
->setRelatedResource(qore()->getResourceOrFail('orders'))
->setIsDisplayedAsTable();
Migration on the related table:
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
Model:
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
HasOneThroughField
For hasOneThrough. Hidden on forms by default.
public function owner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
Migrations follow Laravel's through relation rules: the intermediate table points to the parent, and the final table points to the intermediate model.
HasManyThroughField
For hasManyThrough. Hidden on forms by default and displayed as a relation table.
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
Migrations follow Laravel's through relation rules.
MorphToField
For morphTo. Stores both {name}_type and {name}_id.
MorphToField::make('entity')
->setRelatedResources([
qore()->getResourceOrFail('customers'),
qore()->getResourceOrFail('orders'),
]);
Migration:
$table->nullableMorphs('entity');
Model:
public function entity(): MorphTo
{
return $this->morphTo();
}
MorphOneField
For morphOne. Hidden on forms by default.
Migration on related table:
$table->morphs('entity');
Model:
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'entity');
}
MorphManyField
For morphMany. Hidden on forms by default and displayed as a relation table.
Migration on related table:
$table->morphs('entity');
Model:
public function notes(): MorphMany
{
return $this->morphMany(Note::class, 'entity');
}
MorphToManyField
For polymorphic many-to-many relations.
Migration:
Schema::create('taggables', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable');
});
Model:
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
User Relation Fields
BelongsToUserField and BelongsToManyUsersField use qore()->getUserDisplayCallback() for display values. Configure that callback in AppServiceProvider when the default user title/avatar/detail URL is not correct.