Resources

Relationships

Declare the Eloquent relations of a resource, their targets, pivot fields, constraints, link abilities and the operations a mutation accepts on them.

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 in relations of mutate.
  • The related resource applies its own fields, its own perimeterQuery() and its own policies to the related rows. See Perimeter.

Relation types

Relation classEloquent relationOperations in mutate
BelongsTobelongsTocreate, update, attach, detach
HasOnehasOnecreate, update, attach, detach, and delete with deletable()
HasOneOfManyhasOne()->ofMany()same as HasOne
HasManyhasManysame as HasOne
HasOneThroughhasOneThroughnone
HasManyThroughhasManyThroughnone
BelongsToManybelongsToManycreate, update, attach, detach, sync, toggle
MorphOnemorphOnesame as HasOne
MorphOneOfManymorphOne()->ofMany()same as HasOne
MorphManymorphManysame as HasOne
MorphTomorphTocreate, update, attach, detach
MorphToManymorphToManysame as BelongsToMany
MorphedByManymorphedByManysame 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:

app/Models/Label.php
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 MorphTo include returns starts with its type, under the @type key, which morph.type_key renames in config/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 null for the include.
  • An include of a MorphTo with several targets sets its search for each type under types. A MorphTo include takes no limit and no sorts, and an aggregate cannot go through it. See Includes and aggregates.
  • A MorphTo entry in mutate sends type, the morph type of its target: {"operation": "attach", "type": "shelf", "key": 2}. It is required with several targets.
Adding a morph map to an existing application needs the stored types migrated, because a stored class name no longer matches an alias: 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:

app/Models/Post.php
public function tags(): BelongsToMany
{
    return $this->belongsToMany(Tag::class)
        ->withPivot('weight', 'added_by', 'note')
        ->withTimestamps();
}
  • create and attach accept the creatable pivot fields with their creationRules(). update, sync and toggle accept the updatable pivot fields with their updateRules(). detach refuses pivot.
  • The pivot of 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 with fillUsing() or resolveUsing(), throws InvalidFieldException.
  • The pivot fields are bound to the pivot class of the relation, the class given to using() or Laravel's Pivot. An Enum pivot 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(),
ConstraintError 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.

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'),
app/Policies/PostPolicy.php
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.