Relations
Introduction
Eloquent provides a large variety of relationships. You can read about them here.
Restify handles all relationships and gives you an expressive way to list resource relationships.
Definition
The list of relationships should be defined into a repository method called related
:
public static function related(): array
{
return [];
}
Eager fields
The related
method will return an array that should be a key-value pair, where the key is the related name that the API will request, and the value could be an instance of Binaryk\LaravelRestify\Fields\EagerField
or a relationship name defined in your model.
Each EagerField
declaration is similar to the Field
one. The first argument is the model
relationship name. The second argument is a repository that represents the related entity.
Let's say we have a User that has a list of posts. We will define it this way:
HasMany::make('posts', PostRepository::class),
or:
HasMany::make('posts'),
Related Declaration
Let's see how can we inform a repository about its relationships:
// CompanyRepository
public static function related(): array
{
return [
'usersRelationship' => HasMany::make('users', UserRepository::class),
HasMany::make('posts'),
'extraData' => fn() => ['location' => 'Romania'],
'extraMeta' => new Invokable()
'country',
];
}
Above we can see a few types of relationships declarations that Restify provides. Let's explain them.
Long definition
'usersRelationship' => HasMany::make('users', UserRepository::class),
This means that there is a relationship of the hasMany
type declared in the Company model. The Eloquent relationship name is users
(see the first argument of the HasMany field):
// app/Models/Company.php
public function users(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(User::class);
}
The key usersRelationship
represents the query param the API exposes to load the list of users:
GET: api/companies?related=usersRelationship
The UserRepository
represents the repository class that serializes the users list.
Short definition
HasMany::make('posts'),
Usually the key (query param) and the actual Eloquent relationship names are the same, so Restify provides a shorter version of defining the relationship.
In this case the name of the query param will be the same as the relationship name - posts
. The name of the repository PostRepository
will be resolved based on the same key and $uriKey of the repository.
The request will look like this:
GET: api/companies?related=posts
Callables
'extraData' => fn() => ['location' => 'Romania'],
'extraMeta' => new Invokable()
Restify allows you to resolve specific data using callable functions or invokable (classes with a single public __invoke method). You can return any kind of data from these callables. It'll be serialized accordingly. The query param in this case should match the key:
GET: api/companies?related=extraData,extraMeta
Forwarding
'country',
If you simply define a key in the related
, Restify will forward your request to the associated model. Your model could return anything, as it might be an Eloquent relationship or any primary data.
Let's take a look over all the relationships that Restify provides:
Frontend request
In order to get the related resources, you need to send a GET
request to:
GET `/api/restify/users?include=posts`
Sometimes, you might want to load specific columns from the database into the response. For example, if you have a Post
model with an id
, title
, and a description
column, you might want to load only the title
and the description
column in the response.
In order to do this, you can use the following request:
GET /users/1?include=posts[title|description]
Nested relationships
Let's assume you have the CompanyRepository
:
// CompanyRepository
public static function related(): array
{
return [
HasMany::make('users),
];
}
In the UserRepository you have a relationship to a list of user posts and roles:
// UserRepository
public static function related(): array
{
return [
HasMany::make('posts'),
MorphToMany::make('roles'),
];
}
In PostRepository
you might have a list of comments for each post:
// PostRepository
public static function related(): array
{
return [
HasMany::make('comments'),
];
}
In order to get the company's users with their posts and roles, you can follow the laravel syntax for eager loading into the request query:
GET: /api/restify/companies?include=users.posts,users.roles
This request will return a list like this:
{
"data": {
"id": "91c2bdd0-bf6f-4717-b1c4-a6131843ba56",
"type": "companies",
"attributes": {
"name": "Binar Code"
},
"relationships": {
"users": [{
"id": "3",
"type": "users",
"attributes": {
"name": "Eduard"
},
"relationships": {
"posts": [{
"id": "1",
"type": "posts",
"attributes": {
"title": "Post title"
}
}],
"roles": [{
"id": "1",
"type": "roles",
"attributes": {
"name": "admin"
}
}]
}
}]
}
}
}
You can also specify and load the comments
of the posts
:
GET: /api/restify/companies?include=users.posts.comments,users.roles
Or specify the exact columns that you want to load for each nested layer:
GET: /api/restify/companies?include=users[name].posts[id|title].comments[comment],users.roles[name]
Meta information
Starting with Restify 7+, meta information for related (in index requests) will not be displayed. For more details read the repository meta.
BelongsTo & MorphOne
The BelongsTo
and MorphOne
eager fields work in a similar way, so let's take the BelongsTo
as an example.
Let's assume each Post
belongsTo a User
. To return the post's owner, we will have it defined just like this:
// PostRepository
public static function related(): array
{
return [
'owner' => \Binaryk\LaravelRestify\Fields\BelongsTo::make('user', UserRepository::class),
];
}
The model should define the relationship user
:
// Post.php
public function user()
{
return $this->belongsTo(User::class);
}
Now the frontend can list post or posts including the following relationship:
GET: api/restify/posts/1?include=owner
{
"data": {
"id": "91c2bdd0-bf6f-4717-b1c4-a6131843ba56",
"type": "posts",
"attributes": {
"title": "Culpa qui accusamus eaque sint.",
"description": "Id illo et quidem nobis reiciendis molestiae."
},
"relationships": {
"owner": {
"id": "3",
"type": "users",
"attributes": {
"name": "Laborum vel esse dolorem amet consequatur.",
"email": "[email protected]"
},
"meta": {
"authorizedToShow": true,
"authorizedToStore": true,
"authorizedToUpdate": false,
"authorizedToDelete": false
}
}
},
"meta": {
"authorizedToShow": true,
"authorizedToStore": true,
"authorizedToUpdate": true,
"authorizedToDelete": true
}
}
}
Searchable belongs to
The BelongsTo
field allows you to use the search endpoint to search over a column from the belongsTo
relationship by simply using the searchables
call:
BelongsTo::make('user')->searchable('name')
The searchable
method accepts a list of database attributes from the related entity (users
in our case).
Therefore, if we get the following search request, it'll also search into the related user's name:
GET: api/restify/companies?related=user&search="John"
HasOne
The HasOne
field corresponds to a hasOne
Eloquent relationship.
For example, let's assume a User
model hasOne
Phone
model. We may add the relationship to our UserRepository
like so:
// UserRepository
public static function related(): array
{
return [
\Binaryk\LaravelRestify\Fields\HasOne::make('phone', PhoneRepository::class),
];
}
The json response structure will be the same as previously:
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"name": "Et maxime voluptatem cumque accusamus sit."
},
"relationships": {
"phone": {
"id": "2",
"type": "phones",
"attributes": {
"phone": "+40 766 444 22"
},
"meta": {
"authorizedToShow": false,
"authorizedToStore": true,
"authorizedToUpdate": false,
"authorizedToDelete": false
}
},
...
HasMany & MorphMany
The HasMany
and MorphMany
fields correspond to a hasMany
and morphMany
Eloquent relationship. For example, let's assume a User
model hasMany
Post
models. We may add the relationship to our UserRepository
as shown:
// UserRepository
public static function related(): array
{
return [
\Binaryk\LaravelRestify\Fields\HasMany::make('posts', PostRepository::class),
];
}
In addition, you will get back the posts
relationship:
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"name": "Et maxime voluptatem cumque accusamus sit."
},
"relationships": {
"posts": [
{
"id": "91c2bdd0-ccf6-49ec-9ae9-8bae1d39c100",
"type": "posts",
"attributes": {
"title": "Rem suscipit tempora ullam accusantium in rerum.",
"description": "Vero nostrum quasi velit molestiae animi neque."
},
"meta": {
"authorizedToShow": true,
"authorizedToStore": true,
"authorizedToUpdate": true,
"authorizedToDelete": true
}
}
]
},
"meta": {
"authorizedToShow": true,
"authorizedToStore": true,
"authorizedToUpdate": false,
"authorizedToDelete": false
}
}
}
Paginate
HasMany
field returns 15 entries in the relationships
. This could be customizable from the repository (the
repository being in this case the class of the related resource) class by using:
public static int $defaultRelatablePerPage = 100;
Relatable per page
You can also use the query ?relatablePerPage=100
.
GET: api/restify/users?related=posts&relatablePerPage=100
When using relatablePerPage
query param, it will paginate all the relatable entities with that size.
BelongsToMany & MorphToMany
The BelongsToMany
and MorphToMany
field corresponds to a belongsToMany
or morphToMany
Eloquent relationship. For example, let's assume a User
model belongsToMany
Role models. We may add the relationship to our UserRepository in such wise:
// CompanyRepository
public static function related(): array
{
return [
\Binaryk\LaravelRestify\Fields\BelongsToMany::make('users', UserRepository::class),
];
}
Pivot fields
If your belongsToMany
relationship interacts with additional "pivot" attributes that are stored on the intermediate
table of the many-to-many
relationship, you may also attach those to your BelongsToMany
Restify Field. Once these
fields are attached to the relationship field and the relationship has been defined on both sides, they will be
displayed on the request.
For example, let's assume our User
model belongsToMany
Role models. On our user_role
intermediate table, let's
imagine we have a policy
field that contains a simple text about the relationship. We can attach this pivot field
to the BelongsToMany
field by using the fields method:
BelongsToMany::make('users', RoleRepository::class)->withPivot(
field('is_admin')
),
You'll might as well have to define this in the User
model:
public function users()
{
return $this->belongsToMany(User::class, 'user_company')->withPivot('is_admin');
}
Now, let's try to get the list of companies with users:
GET: /api/restify/company/1?include=users
{
"data": {
"id": "1",
"type": "companies",
"attributes": {
"name": "ut"
},
"relationships": {
"users": [
{
"id": "1",
"type": "users",
"attributes": {
"name": "Linnea Rowe Sr.",
"email": "[email protected]",
},
"meta": {
"authorizedToShow": true,
"authorizedToStore": true,
"authorizedToUpdate": true,
"authorizedToDelete": true
},
"pivots": {
"is_admin": true
}
}
]
},
"meta": {
"authorizedToShow": true,
"authorizedToStore": true,
"authorizedToUpdate": true,
"authorizedToDelete": true
}
}
}
Attach related
Once you have defined the BelongsToMany
field, you can now attach User to a Company just like this:
POST: api/restify/companies/1/attach/users
Payload:
{
"users": [1, 2],
"is_admin": true
}
Authorize attach
You have a few options to authorize the attach
endpoint.
First, you can define the policy method attachUsers
. The name should start with attach
and suffix with the CamelCase
name of the model's relationship name:
// CompanyPolicy.php
public function attachUsers(User $authenticatedUser, Company $company, User $userToBeAttached): bool
{
return $authenticatedUser->isAdmin();
}
The policy attachUsers
method will be called for each individual userToBeAttached
. However, if you attach - [1, 3] ids, this method will be called twice.
Another way to authorize this is by using the canAttach
method to the Eager field directly. This method accepts an invokable class instance or a closure:
'users' => BelongsToMany::make('users', UserRepository::class)
->canAttach(function ($request, $pivot) {
return $request->user()->isAdmin();
}),
Override attach
You are free to intercept the attach operation entirely and override it by using a closure or an invokable:
'users' => BelongsToMany::make('users', UserRepository::class)
->attachCallback(function ($request, $repository, $company) {
$company->users()->attach($request->input('users'));
}),
Or using an invokable :
'users' => BelongsToMany::make('users', UserRepository::class)
->attachCallback(new AttachCompanyUsers),
and then define the class:
use Illuminate\Http\Request;
class AttachCompanyUsers
{
public function __invoke(Request $request, CompanyRepository $repository, Company $company): void
{
$company->users()->attach($request->input('users'));
}
}
Sync related
You can also sync
your BelongsToMany
field. Say you have to sync permissions to a role. You can do it like this:
POST: api/restify/roles/1/sync/permissions
Payload:
{
"permissions": [1, 2]
}
Under the hood this will call the sync
method on the BelongsToMany
relationship:
// $role of the id 1
$role->permissions()->sync($request->input('permissions'));
Authorize sync
You can define a policy method syncPermissions
. The name should start with sync
and suffix with the plural CamelCase
name of the model's relationship name:
// RolePolicy.php
public function syncPermissions(User $authenticatedUser, Company $company, Collection $keys): bool
{
// $keys are the primary keys of the related model (permissions in our case) Restify is trying to `sync`
}
Detach related
As soon we declared the BelongsToMany
relationship, Restify automatically registers the detach
endpoint:
POST: api/restify/companies/1/detach/users
Using the payload:
{
"users": [1]
}
Authorize detach
You have a few options to authorize the detach
endpoint.
Primarily, you can define the policy method detachUsers
, as the name should start with detach
and suffix with the CamelCase
name of the model relationship name:
// CompanyPolicy.php
public function detachUsers(User $authenticatedUser, Company $company, User $userToBeDetached): bool
{
return $authenticatedUser->isAdmin();
}
The policy detachUsers
method will be called for each individual userToBeDetached
. If you detach - [1, 3] ids, this method will be called twice.
Another way to authorize this is by using the canDetach
method to the Eager field directly. This method accepts an invokable
class instance or a closure
:
'users' => BelongsToMany::make('users', UserRepository::class)
->canDetach(
fn($request, $pivot) => $request->user()->can('detach', $pivot)
),
Override detach
You are free to intercept the detach method entirely and override it by using a closure or an invokable:
'users' => BelongsToMany::make('users', UserRepository::class)
->detachCallback(function ($request, $repository, $company) {
$company->users()->detach($request->input('users'));
}),
Or using an invokable :
'users' => BelongsToMany::make('users', UserRepository::class)
->detachCallback(new DetachCompanyUsers),
and then define the class:
use Illuminate\Http\Request;
class DetachCompanyUsers
{
public function __invoke(Request $request, CompanyRepository $repository, Company $company): void
{
$company->users()->detach($request->input('users'));
}
}