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'),
Restify 7+ will guess the serialization repository using the key, so you don't necessarily have to specify it:

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]
Getting specific columns will make your requests more performant.

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
    }
  }
}

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'));
    }
}

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`
}

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'));
    }
}
Edit this page on GitHub Updated at Sun, Aug 13, 2023