Skip to main content

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): static
  • setModelClass(string $modelClass): static
  • setRelationshipName(string $name): static
  • setEagerLoader(Closure(Builder): void $callback): static
  • setRelationType(string $type): static
  • setIsDisplayedAsTable(bool $value = true): static for fields with relation tables.
  • setTableNode(Closure(Model): Node $callback): static for custom relation tables.

Most relation configuration is guessed by Qore by default. Configure it explicitly when the field name, relationship name or related resource is not obvious.

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 options from indexQuery() and uses modelTitle() as the label.

Override modelTitle() when the default {label} #{id} label is not useful:

public function modelTitle(Model $model): string
{
return $model->name;
}

Override getOptionsCallback() when the relation field needs searching, limits or a custom label:

use Closure;
use Illuminate\Database\Eloquent\Model;
use Qore\Next\System\Field\Select\SelectMetadata;
use Qore\Next\System\Field\Select\SelectOption;

/**
* @return Closure(SelectMetadata): list<SelectOption>
*/
public function getOptionsCallback(): Closure
{
return function (SelectMetadata $metadata) {
return $this->indexQuery()
->where('name', 'like', "%{$metadata->getSearch()}%")
->limit(20)
->get()
->map(fn (Model $model) => new SelectOption(
value: $model->getKey(),
label: $this->modelTitle($model),
))
->all();
};
}

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.