Relation fields
Introduction
Qore has fields for most relationships that exist in Laravel.
If you're unsure on how to set up the Migration, Model relationship or Field take a look at Relationship cheat sheet.
Before using relational fields, make sure you add the corresponding relationship to your model first.
If you do not do this, Qore will throw an error describing what you need to do.
Be also sure that you use the Laravel namespace for model relationships, and the Qore namespaces for Fields.
Fields
Most relationship fields expect another resource as the third argument. Currently the following relationship fields are supported:
BelongsTo::make('Medewerker', 'employee', Employee::class)
BelongsToMany::make('Medewerkers', 'employees', Employee::class)
HasOne::make('Hoofd medewerker', 'mainEmployee', Employee::class)
HasMany::make('Medewerkers', 'mainEmployees', Employee::class)
HasOneThrough::make('Owner','ticketMessageOwner', TicketMessageOwnerResource::class)
HasManyThrough::make('Owners','ticketMessageOwners', TicketMessageOwnerResource::class)
MorphOne::make('Address', 'address', Address::class)
MorphMany::make('Addressen', 'addresses', Address::class)
MorphTo::make('Something', 'morph')->resources(EmployeeResource::class, AddressResource::class)
MorphToMany::make('Tags', 'tags', TagResource::class)
BelongsToUser::make(__('User'), 'user') // Wrapper for BelongsTo
BelongsToManyUsers::make(__('Users'), 'users') // Wrapper for BelongsToMany
The name of your field should be the same as the name of your defined relationship method on your Model.
Alternatively, you can also chain ->onRelation($nameOfRelationMethod)
on your field.
Scoping options
By default, every related resource model is appended as a selectable option.
However, in a lot of cases you only want certain options to show.
You can achieve this with scopeUsing
:
BelongsTo::make('Primary contact', 'primaryContact', Contact::class)
->scopeUsing(function ($query, $model) {
if (!$model) {
return $query->where('is_private', false)
->whereNull('organization_id');
}
return $query->where('organization_id', $model->id);
})
Depending on other field values
In some cases you may want to change your scope based on the value of another field.
You can achieve this by using dependsOn
:
->dependsOn('employee', function (Builder $query, $employeeId) {
if (!$employeeId) {
return $query;
}
return $query->whereIn('addresses.id', [1, 5]);
})
See more information here Field dependency.
Front-end behaviour
Links
If you want the link(s) to open in a new tab:
BelongsTo::make(...)
->displayAsHref()
->openInNewTab()
If you want to show pills instead of <a>
links everywhere, you can publish the qore
config file and edit:
'preferences' => [
// ...
'display_as_href' => [
'default' => false
]
]
As tab / table
You can display BelongsToMany
, HasMany
and Morphmany
as tables on the detail page. Both inside a tab, and as a
card:
BelongsToMany::make('Medewerkers', 'employees', Employee::class)->asTab()
MorphMany::make('Addressen', 'addresses', Address::class)->asCard()
Attaching and detaching
When these relations are shown as a card or a tab, they will also contain actions to easily attach or detach certain records. You can however disable this:
BelongsToMany::make('Medewerkers', 'employees', Employee::class)->asTab()
->asCard()
->withoutDetaching()
->withoutAttaching()
In most cases when using HasMany
in combination with attaching, you want
to auto-fill the related field in the form that will be opened. You can achieve this with the attachOnField
method:
HasMany::make('Tijdregistraties', 'timeRegistrations', TimeRegistration::class)
->attachOnField('employee')
->asTab()
->asCard()
By default, the name of the field will be checked against the relation defined on the model. You can change this by setting the relation name:
MorphTo::make('Something', 'morph')
->onRelation('addressable')
->resources(EmployeeResource::class, AddressResource::class)
If your database column name does not match your field name_id
(e.g.: morph_id
), you will need to set this:
MorphTo::make('Morph test', 'morph')
->onIdColumn('some_morph_id')
->resources(EmployeeResource::class, HobbyResource::class)
Attach buttons
You can also append a button on the resource detail page to quickly add a relation:
BelongsToMany::make('Medewerkers', 'employees', Employee::class)
->withAttachButtonInHeader() // Added in header
->withAttachButton() // Added as a block on the detail page
// You can also change the look (disable stacked and make it outlined)
->withAttachButton(false, true)
If you want to open the form in a new browser tab, you can add:
BelongsToMany::make('Medewerkers', 'employees', Employee::class)
->attachOnNewPage()
Sorting relations
By default, relation fields will sort on the foreign key that is defined in the relation. You can however supply your own sorting column on the following fields:
BelongsTo::make('Ticket', 'ticket', TicketResource::class)
->sortColumn('tickets.name')
HasOne::make('Ticket message', 'ticketMessage', TicketMessageResource::class)
->sortColumn('ticket_messages.description')
HasMany::make('Ticket messages', 'ticketMessages', TicketMessageResource::class)
->sortColumn('ticket_messages.description')
HasOneThrough::make('Ticket owner', 'ticketMessageOwner', TicketMessageOwnerResource::class)
->sortColumn('ticket_message_owners.name')
HasManyThrough::make('Ticket owners', 'ticketMessageOwners', TicketMessageOwnerResource::class)
->sortColumn('ticket_message_owners.name')
MorphOne::make(__('Mail footer'), 'mailFooter', MailFooterResource::class)
->sortColumn('mail_footers.name')
The following fields do not support sorting: BelongsToMany
, MorphMany
, MorphToMany
Relational
Relational state
Most relation fields will append a "+" button to the input field. This will allow the user to create the resource without leaving the page.
Sometimes you may need to set the default state for the relational form. You can set the state:
BelongsToMany::make('Projecten (BTM)', 'projects', Project::class)
->withRelationalMeta(function(FormState $state) {
$state->hideFields('name');
$state->setState('description', 'abc');
})
->rules('nullable', 'exists:projects,id')
You can also get the related model if needed:
->withRelationalMeta(function(FormState $state) {
$relatedModel = $state->getRelatedModel();
// ..
})
You can also get the state
and meta
from the parent form using the following methods:
->withRelationalMeta(function(ManagesForm $form) {
$form->getParentState();
$form->getParentState('first_name');
$form->getParentMeta();
$form->getParentFieldMeta('field_name', 'key');
})
Relational table
When using the ->asCard
or asTab
method on a relation field, you might want to control which columns are shown by
default in the table.
You can deselect fields by default:
BelongsToMany::make('Projecten (BTM)', 'projects', Project::class)
->withDeselectedTableColumns('description', 'template')
You can also apply the opposite, by choosing which fields should be shown by default:
BelongsToMany::make('Projecten (BTM)', 'projects', Project::class)
->withSelectedTableColumns('name', 'description')
If the user has already defined his/her own table columns, this method will not work for that specific user until the user resets the table settings
The query used for the relation table may be overriden if neccessary:
BelongsToMany::make(..)
->withTableQueryUsing(function (Model $model) {
/** @var BelongsToMany|MorphToMany $relation */
$relation = $model->{$this->relationName}();
$relatedQuery = $relation->getQuery()
->select('*', $relation->getQualifiedRelatedPivotKeyName() . ' as id');
$relatedQuery->with($this->otherResource->indexQuery()->getEagerLoads());
return $relatedQuery;
});
If further customization is necessary on the columns, you can also modify them:
return HasMany::make(__('crm::crm.Contacts'), 'contacts', resource('contacts')->resourceClass())
// In this example, we only add the contact organization as a
// filter option in the dropdown instead of all organizations
->withTableColumnsUsing(function(ColumnCollection $collection, Model $relatedModel) {
$organizationColumn = $collection->findByName('organization');
// Filter options also accept a closure
$organizationColumn->filterOptions([
[
'label' => $relatedModel->name,
'value' => $relatedModel->id
]
]);
})
Relational table actions
When hovering over rows in the relation table, a small menu will be shown to detach your relation record. You can also add your own actions here, for example to update pivot table columns:
BelongsToMany::make('Labels (belongsToMany', 'labels', LabelResource::class)
->pivotFields(function (Model $model) {
return new FieldCollection(
Checkbox::make('Is active', 'is_active')
->tooltip($model->name)
);
})
->withTableActions(function (Model $parent) {
return new ActionCollection(
(new SetIsActiveAction())
);
})
You can also pass in an ActionCollection
directly if you don't need the related (parent) model in a closure.
Now the SetIsActiveAction
could look something like this:
class SetIsActiveAction extends Action
{
public function title(): string
{
return 'Set is active';
}
public function description(): string
{
return 'Set pivot column to active';
}
public function icon(): string
{
return 'rocket';
}
public function fields(): FieldCollection
{
return new FieldCollection(
Checkbox::make('Set active', 'active')
->default($this->relatedModel->labels()->find($this->model->id)->pivot->is_active)
);
}
public function run(Model $model, array $arguments): void
{
$this->relatedModel
->labels()
->updateExistingPivot(
$model, [
'is_active' => boolval($arguments['active'])
]
);
}
}
As you can see, you have access to the following properties in your action: $this->relatedModel
, $this->relatedResource
and $this->relatedResource
.
Mutating filter options
You can change the filter options on a field based on the table records:
BelongsTo::make(
__('crm::crm.Organization'),
'organization',
resource('organizations')->resourceClass()
)
->postProcessFilterOptionsUsing(function (Collection $options, QoreResource $myResource, array $tableRecordIds) {
return $options
->whereIn('value', ContactModel::whereIn('id', $tableRecordIds)
->pluck('organization_id'))
->values();
})
The $tableRecordIds
contains all the ids that are relevant for the table. Based on these ids, you might want to filter out any filter options.
Pivot
When working with a pivot table (https://laravel.com/docs/10.x/eloquent-relationships#retrieving-intermediate-table-columns), you might also have columns on the pivot table you need to see in the relation table.
Pivot fields
With the pivotFields
call you can supply a FieldCollection
or a Closure
that expects a Model
instance. These fields will be appended to the relation table.
Because these fields exists on another table (the pivot table), Qore will automatically replace the index query for the relation table to support this.
If you do not want this behaviour you can supply false to the second argument of the pivotFields method.
BelongsToMany::make('Labels', 'labels', LabelResource::class)
->pivotFields(function(Model $model) {
return new FieldCollection(
Checkbox::make('Is active', 'is_active')
->tooltip($model->name)
);
})
MorphToMany::make('Tags', 'tags', TagResource::class)
->pivotFields(new FieldCollection(
Checkbox::make('Is active', 'is_active')
), false)
Because Qore adds filters & sorting to every field by default, you might have to double check if they are working for your pivot fields as well (because the values exists on separate tables).
Eventually you could disable them or add ->filterUsing
and ->sortUsing
yourself.
Pivot table columns
You can optionally set the values of each pivot column when your resource gets updated/created. For example, assuming we have the following tables:
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Schema::create('model_tags', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Tag::class)->constrained()->cascadeOnDelete();
$table->morphs('model');
$table->string('comment');
$table->timestamps();
});
Then you can set this comment
column manually with the fillPivotUsing
closure:
MorphToMany::make('Tags', 'tags', TagResource::class)
->pivotFields(new FieldCollection(
Text::make('Comment', 'comment')
))
->fillPivotUsing(function(array $syncIds, ?Model $model): array {
$sync = [];
foreach ($syncIds as $id) {
$sync[$id] = ['comment' => 'Model => ' . $model->id];
}
return $sync;
})
->asCard()
->asTab(),
Events
When a model gets attached or detached from a resource (via relation fields like HasMany
, BelongsToMany
, HasOne
etc.), the following events will be dispatched:
ResourceRelationAttached::class
ResourceRelationDetached::class
On both events these public properties can be found:
model
, detachedModel
/ attachedModel
, resource
and detachedResource
/ attachedResource
.
When the field BelongsTo
gets updated, an event will not be fired. This is because the
column for the relation exists on the table of the model and it is not really considered attaching/detaching.
In the future this might be changed based on the opinion of developers.
Relationship cheat sheet
To make defining migrations and model relationships a bit easier, you can find the definitions per field below.
Each definition is based around a Ticket
model where the relation field is defined in the TicketResource
.
BelongsTo
A Ticket
can belong to an Organization
:
// Migration:
Schema::table('tickets', function(Blueprint $table) {
$table->foreignIdFor(Organization::class)->nullable()->constrained()->nullOnDelete();
});
// Ticket Model:
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class);
}
// TicketResource Field:
BelongsTo::make('Organization', 'organization', OrganizationResource::class)
BelongsToMany
A Ticket
can belong to many Organization
:
// Migration:
Schema::create('organization_ticket', function(Blueprint $table) {
$table->foreignIdFor(Ticket::class)->constrained()->cascadeOnDelete();
$table->foreignIdFor(Organization::class)->constrained()->cascadeOnDelete();
});
// Ticket Model:
public function organizations(): BelongsToMany
{
return $this->belongsToMany(Organization::class, 'organization_ticket');
}
// TicketResource Field:
BelongsToMany::make('Organizations', 'organizations', OrganizationResource::class)
HasOne
A Ticket
can have one Tag
:
// Migration:
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Ticket::class)->nullable()->constrained()->nullOnDelete();
$table->timestamps();
});
// Ticket Model:
public function tag(): HasOne
{
return $this->hasOne(Tag::class, 'ticket_id');
}
// TicketResource Field:
HasOne::make('Tag', 'tag', TagResource::class)
HasMany
A Ticket
can have many TicketMessage
:
// Migration:
Schema::create('ticket_messages', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Ticket::class)->nullable()->constrained()->nullOnDelete();
$table->timestamps();
});
// Ticket Model:
public function ticketMessages(): HasMany
{
return $this->hasMany(TicketMessage::class);
}
// TicketResource Field:
HasMany::make('Ticket messages', 'ticketMessages', TicketMessageResource::class)
HasOneThrough
A Ticket
can have one Contact
through TicketMessage
:
// Migration:
Schema::create('ticket_messages', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Ticket::class)->nullable()->constrained()->nullOnDelete();
$table->timestamps();
});
Schema::table('contacts', function(Blueprint $table) {
$table->foreignIdFor(TicketMessage::class)->nullable()->constrained()->nullOnDelete();
});
// Ticket Model:
public function contact(): HasOneThrough
{
return $this->hasOneThrough(Contact::class, TicketMessage::class);
}
// TicketResource Field:
// NOTE: This field not shown on forms
HasOneThrough::make('Contact', 'contact', ContactResource::class)
HasManyThrough
A Ticket
can have many Contact
through TicketMessage
:
// Migration:
Schema::create('ticket_messages', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Ticket::class)->nullable()->constrained()->nullOnDelete();
$table->timestamps();
});
Schema::table('contacts', function(Blueprint $table) {
$table->foreignIdFor(TicketMessage::class)->nullable()->constrained()->nullOnDelete();
});
// Ticket Model:
public function contacts(): HasManyThrough
{
return $this->hasManyThrough(Contact::class, TicketMessage::class);
}
// TicketResource Field:
// NOTE: This field not shown on forms,
// however attaching is possible via `->asCard()` and/or `->asTab()`
HasManyThrough::make('Contacts', 'contacts', ContactResource::class)
->asCard()
->asTab()
MorphOne
A Ticket
can morph one Contact
:
// Migration:
Schema::table('contacts', function(Blueprint $table) {
$table->morphs('entity');
});
// Ticket Model:
public function contact(): MorphOne
{
return $this->morphOne(Contact::class, 'entity');
}
// TicketResource Field:
MorphOne::make('Contact', 'contact', ContactResource::class)
MorphMany
A Ticket
can morph many Contact
:
// Migration:
Schema::table('contacts', function(Blueprint $table) {
$table->morphs('entity');
});
// Ticket Model:
public function contacts(): MorphMany
{
return $this->morphMany(Contact::class, 'entity');
}
// TicketResource Field:
MorphMany::make('Contacts', 'contacts', ContactResource::class)
MorphTo
A Ticket
can morph to an entity
:
// Migration:
Schema::table('tickets', function(Blueprint $table) {
$table->nullableMorphs('entity');
});
// Ticket Model:
public function entity(): MorphTo
{
return $this->morphTo('entity');
}
// TicketResource Field:
MorphTo::make('Entity', 'entity')
->resources(OrganizationResource::class, ContactResource::class)
MorphToMany
A Ticket
can morph to many Tag
:
// Migration:
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Schema::create('model_tags', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Tag::class)->constrained()->cascadeOnDelete();
$table->morphs('model');
$table->timestamps();
});
// Ticket Model:
public function tags(): MorphToMany
{
return $this->morphToMany(
Tag::class,
'model',
'model_tags'
);
}
// TicketResource Field:
MorphToMany::make('Tags', 'tags', TagResource::class)
BelongsToUser
A Ticket
can belong to User
:
Make sure you link it to your local User
model, because this will add an Avatar in the front-end.
// Migration:
use Illuminate\Database\Query\Expression;
Schema::table('tickets', function(Blueprint $table) {
$centralDb = tenant()->getConnection()->getDatabaseName();
$table->foreignIdFor(User::class)
->nullable()
->references('id')
->on(new Expression('`' . $centralDb . '`.users'))
->nullOnDelete();
});
// Ticket Model:
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
// TicketResource Field:
BelongsToUser::make('User', 'user')
BelongsToManyUsers
A Ticket
can belong to many User
:
Make sure you link it to your local User
model, because this will add an Avatar in the front-end.
// Migration:
use Illuminate\Database\Query\Expression;
Schema::create('ticket_user', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Ticket::class)->nullable()->constrained()->nullOnDelete();
$centralDb = tenant()->getConnection()->getDatabaseName();
$table->foreignIdFor(User::class)
->nullable()
->references('id')
->on(new Expression('`' . $centralDb . '`.users'))
->nullOnDelete();
$table->timestamps();
});
// Ticket Model:
public function users(): BelongsToMany
{
return $this->belongsToMany(
User::class,
get_tenant_database_name() . '.ticket_user'
);
}
// TicketResource Field:
BelongsToManyUsers::make('Users', 'users')