Relationships
Declaring relations
relations() returns the relations that clients may include, filter on, sort on and write. Each relation names the Eloquent relation method of the model and the resource of the related model:
use Lomkit\Rest\Relations\BelongsTo;
use Lomkit\Rest\Relations\BelongsToMany;
use Lomkit\Rest\Relations\HasMany;
public function relations(): array
{
return [
BelongsTo::make('category', CategoryResource::class),
HasMany::make('comments', CommentResource::class),
BelongsToMany::make('tags', TagResource::class),
];
}
- The first argument is the name of the relation method on the model. The second is a resource class, never a model class.
- Use the relation classes of
Lomkit\Rest\Relations, not the Eloquent ones. - A relation that the resource does not declare is refused everywhere: in
includes, in filters, in sorts, in aggregates and inrelationsofmutate. - The related resource applies its own fields, its own
perimeterQuery()and its own policies to the related rows. See Perimeter.
Relation types
| Relation class | Eloquent relation | Operations in mutate |
|---|---|---|
BelongsTo | belongsTo | create, update, attach, detach |
HasOne | hasOne | create, update, attach, detach, and delete with deletable() |
HasOneOfMany | hasOne()->ofMany() | same as HasOne |
HasMany | hasMany | same as HasOne |
HasOneThrough | hasOneThrough | none |
HasManyThrough | hasManyThrough | none |
BelongsToMany | belongsToMany | create, update, attach, detach, sync, toggle |
MorphOne | morphOne | same as HasOne |
MorphOneOfMany | morphOne()->ofMany() | same as HasOne |
MorphMany | morphMany | same as HasOne |
MorphTo | morphTo | create, update, attach, detach |
MorphToMany | morphToMany | same as BelongsToMany |
MorphedByMany | morphedByMany | same as BelongsToMany |
The details list the operations of each relation under operations, with ?with=relations.operations. See Writing relations for each operation.
Belongs to
A Product belongs to a Category:
use Lomkit\Rest\Relations\BelongsTo;
BelongsTo::make('category', CategoryResource::class),
A BelongsTo include loads the one related row of each parent row: it takes no limit and no sorts. A search can sort on a field of a to-one relation, such as category.name.
Has one and has many
A User has one Address and many Post models:
use Lomkit\Rest\Relations\HasMany;
use Lomkit\Rest\Relations\HasOne;
use Lomkit\Rest\Relations\HasOneOfMany;
HasOne::make('address', AddressResource::class),
HasOneOfMany::make('latestOrder', OrderResource::class),
HasMany::make('posts', PostResource::class),
A to-many include returns at most its limit rows per parent row, or the $includeLimit of the related resource. See Includes and aggregates.
Through relations
A through relation reaches distant rows through an intermediate model, such as the replies of the tickets of a team:
use Lomkit\Rest\Relations\HasManyThrough;
use Lomkit\Rest\Relations\HasOneThrough;
HasOneThrough::make('car', CarResource::class),
HasManyThrough::make('replies', ReplyResource::class),
HasOneThrough and HasManyThrough can be included, filtered and aggregated, but not written. They accept no operation, and mutate refuses any entry under them as a whole: The mutate.0.relations.replies field is prohibited. Write the distant rows through the intermediate relations instead.
Many to many
A Post belongs to many Tag models, through a pivot table:
use Lomkit\Rest\Relations\BelongsToMany;
BelongsToMany::make('tags', TagResource::class),
MorphToMany and MorphedByMany declare the polymorphic forms, and its inverse:
use Lomkit\Rest\Relations\MorphedByMany;
use Lomkit\Rest\Relations\MorphToMany;
MorphToMany::make('tags', TagResource::class), // on PostResource
MorphedByMany::make('posts', PostResource::class), // on TagResource
Morph one and morph many
use Lomkit\Rest\Relations\MorphMany;
use Lomkit\Rest\Relations\MorphOne;
use Lomkit\Rest\Relations\MorphOneOfMany;
MorphOne::make('image', ImageResource::class),
MorphOneOfMany::make('latestImage', ImageResource::class),
MorphMany::make('comments', CommentResource::class),
These relations write the morph type with getMorphClass(): with a morph map, they write the alias.
Morph to
A MorphTo declares every resource that it can load, its targets:
use Lomkit\Rest\Relations\MorphTo;
MorphTo::make('labelable', [BookResource::class, ShelfResource::class]),
The model keeps one morphTo() method:
public function labelable(): MorphTo
{
return $this->morphTo();
}
- Each target is keyed by its morph type, the
getMorphClass()of its model: the morph-map alias, or the class name without a map. A morph map is recommended. - A declaration with one resource is the one-target case:
MorphTo::make('commentable', PostResource::class). - Each row that a
MorphToinclude returns starts with its type, under the@typekey, whichmorph.type_keyrenames inconfig/rest.php.
{"id": 1, "text": "classic", "labelable": {"@type": "book", "id": 1, "title": "Dune"}}
- A row whose stored type is not a target of the relation returns
nullfor the include. - An include of a
MorphTowith several targets sets its search for each type undertypes. AMorphToinclude takes nolimitand nosorts, and an aggregate cannot go through it. See Includes and aggregates. - A
MorphToentry inmutatesendstype, the morph type of its target:{"operation": "attach", "type": "shelf", "key": 2}. It is required with several targets.
UPDATE labels SET labelable_type = 'book' WHERE labelable_type = 'App\\Models\\Book'.Pivot fields
BelongsToMany, MorphToMany and MorphedByMany declare the columns of their pivot table with typed fields. Each pivot field decides which operations write its column, validates the value, and decides whether the value is returned:
use Lomkit\Rest\Fields\DateTime;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Fields\Text;
use Lomkit\Rest\Relations\BelongsToMany;
BelongsToMany::make('tags', TagResource::class)->pivotFields([
Integer::make('weight')
->nullable()
->rules('min:0'),
Text::make('added_by')->updatable(false), // written on create and attach only
Text::make('note')->writeOnly(), // written, never returned
DateTime::make('created_at')->readonly(), // returned, never written
]),
Declare the same columns on the Eloquent relation, so that Eloquent reads them:
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)
->withPivot('weight', 'added_by', 'note')
->withTimestamps();
}
createandattachaccept the creatable pivot fields with theircreationRules().update,syncandtoggleaccept the updatable pivot fields with theirupdateRules().detachrefusespivot.- The
pivotof an included row holds the selectable pivot fields. - A filter reaches a pivot field through its relation:
tags.pivot.weight, with the operators of its type. - A pivot field is a column of the pivot table: a
virtual()pivot field, or one withfillUsing()orresolveUsing(), throwsInvalidFieldException. - The pivot fields are bound to the pivot class of the relation, the class given to
using()or Laravel'sPivot. AnEnumpivot field reads its enum class from the casts of that class.
Constraints
A relation can be required or refused in a mutation, on creation or on update:
use Lomkit\Rest\Relations\BelongsTo;
BelongsTo::make('category', CategoryResource::class)
->requiredOnCreation()
->prohibitedOnUpdate(),
| Constraint | Error when it fails |
|---|---|
requiredOnCreation(), requiredOnUpdate() | The mutate.0.relations.category field is required. |
prohibitedOnCreation(), prohibitedOnUpdate() | The mutate.0.relations.category field is prohibited. |
Each constraint takes a boolean or a callback. The callback receives the request and the related resource, null on a MorphTo with several targets:
use Lomkit\Rest\Http\Requests\RestRequest;
use Lomkit\Rest\Http\Resource;
BelongsTo::make('category', CategoryResource::class)
->requiredOnCreation(fn (RestRequest $request, ?Resource $resource): bool => ! $request->user()?->is_staff),
A relation that is required on update is required in every update entry, and an empty list does not satisfy it.
The details list the constraints of each relation under constraints, with ?with=relations.constraints.
Link abilities
A mutation that links two rows checks the link on the policy of the parent row. By default, it checks update on the parent. Declare the abilities of the relation to check others:
BelongsToMany::make('tags', TagResource::class)
->abilities(attach: 'attachTag', detach: 'detachTag'),
public function attachTag(User $user, Post $post, Tag $tag): bool
{
return $user->team_id === $post->team_id;
}
attach covers the create, update and attach entries and the rows that sync and toggle attach. detach covers the detach entries and the rows that sync and toggle detach. A declared ability that no policy method answers is denied. See Policies.
Deleting child rows
A child relation can delete its rows in the same mutation as their parent once it declares deletable(). This applies to HasOne, HasOneOfMany, HasMany, MorphOne, MorphOneOfMany and MorphMany:
HasMany::make('lines', OrderLineResource::class)->deletable(),
{
"mutate": [
{
"operation": "update",
"key": 7,
"relations": {
"lines": [{"operation": "delete", "key": [102, 103]}]
}
}
]
}
Each key must be a child of the parent row. Each row is checked with the delete policy of the related resource and with the detach link check, then deleted through the performDelete() of the related resource. The hooks of the destroy endpoint do not run. The details list delete last in the operations of a deletable() relation. See Writing relations.
Custom relations
A relation class of your application extends one of the package classes. mutationOperations() lists the operations that its entries accept, as cases of Lomkit\Rest\Fields\MutationOperation. A relation that returns [] takes no entry in mutate:
use Lomkit\Rest\Fields\MutationOperation;
use Lomkit\Rest\Relations\HasMany;
class AppendOnlyHasMany extends HasMany
{
public function mutationOperations(): array
{
return [MutationOperation::Create];
}
}
The package writes a relation by the class of its Eloquent relation. A relation has no write hook: change a write in the mutating() and mutated() hooks of the resource. A custom relation with pivot fields implements Lomkit\Rest\Contracts\PivotRelation, which the Lomkit\Rest\Concerns\Relations\HasPivotFields trait implements.