ガイド

リレーション

モデルのリレーションをAPI経由で公開する方法を学びます。

コントローラーのセットアップ

モデルリレーションのコントローラーの定義は、モデルコントローラーの定義とよく似ています。

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Http\Controllers\RelationController;

class PostCommentsController extends RelationController
{
    /**
     * モデルの完全修飾クラス名
     */
    protected $model = Post::class; // または "App\Models\Post"

    /**
     * Post モデルで定義されているリレーション名
     */
    protected $relation = 'comments';
}

この時点では、リレーションの種類の違いを気にする必要はありません。コントローラーはすべての種類のリレーションで同じ方法で定義します。

Fillableなピボットフィールドとキャスト

belongsToManyまたはmorphToManyリレーション型のコントローラーを定義していて、ピボットテーブルに追加のフィールドがある場合は、protected $pivotFillableprotected $pivotJsonという2つの追加プロパティに注意が必要です。

$pivotFillableプロパティには、attachsynctoggleupdatePivotエンドポイント経由で更新できるピボットテーブルのフィールドの一覧を指定します。

$pivotJsonプロパティには、配列との相互変換を自動的にキャストしたいピボットテーブル上のjsonフィールドの一覧を指定します。関連するPivotモデル$castsプロパティを定義済みの場合は、この指定を省略できます。

要点
  • モデルリレーションのコントローラーは常にOrion\Http\Controllers\RelationControllerを継承します
  • $modelプロパティには完全修飾のモデルクラス名を設定します
  • $relationプロパティにはモデル上で定義されているとおりのリレーション名を設定します

ルートのセットアップ

コントローラーとは異なり、ルートはリレーションの種類ごとに異なる方法で定義します。

<?php

use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;
use App\Http\Controllers\ProfileImageController;
...

Route::group(['as' => 'api.'], function() {
    ...
    Orion::hasOneResource('profiles', 'image', ProfileImageController::class);
    Orion::hasManyResource('users', 'posts', UserPostsController::class);
    Orion::belongsToResource('posts', 'user', PostUserController::class);
    Orion::belongsToManyResource('users', 'roles', UserRolesController::class);
    Orion::hasOneThroughResource('posts', 'meta', PostMetaController::class);
    Orion::hasManyThroughResource('users', 'comments', UserCommentsController::class);
    Orion::morphOneResource('posts', 'image', PostImageController::class);
    Orion::morphManyResource('posts', 'comments', PostCommentsController::class);
    Orion::morphToResource('images', 'post', ImagePostController::class);
    Orion::morphToManyResource('posts', 'tags', PostTagsController::class);
    Orion::morphedByManyResource('tags', 'posts', TagsPostsController::class);
    ...
});

ソフトデリート

リレーションのモデルがSoftDeletesトレイトを使用していて、同じ機能をAPI経由でも公開したい場合は、リソース登録時にwithSoftDeletesメソッドを呼び出します。

<?php

use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;
use App\Http\Controllers\UserPostsController;

Route::group(['as' => 'api.'], function() {
    ...
    Orion::hasManyResource('users', 'posts', UserPostsController::class)->withSoftDeletes();
    ...
});

これによりrestorebatchRestoreエンドポイントが追加されます。API経由でリソースを完全削除(force delete)する方法については、関連するクエリパラメータのセクションを参照してください。

+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
| Domain | Method    | URI                                             | Name                                   | Action                                                                    | Middleware                                      |
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
...
|        | POST      | api/users/{user}/posts/{post}/restore           | api.users.relation.posts.restore       | App\Http\Controllers\Api\UserPostsController@restore                      | api                                             |
|        | POST      | api/users/{user}/posts/batch/restore            | api.users.relation.posts.batchRestore  | App\Http\Controllers\Api\UserPostsController@batchRestore                 | api                                             |

1対1

次のリレーションは1対1のリレーションとして扱われます。

  • hasOne
  • hasOneThrough
  • morphOne
  • belongsTo(hasManyリレーションの逆)
  • morphTo(morphManyリレーションの逆)

1対1のリレーションに対して、Orionは4つのエンドポイント(基本的にはCRUD操作のエンドポイント)を提供します: storeshowupdatedestroy

belongsToおよびmorphToリレーションにはstoreエンドポイントは提供されません。

ルート登録の例

Orion::hasOneResource('profiles', 'image' , ProfileImageController::class);

利用可能なエンドポイントの例

+-----------+-------------------------------------------------+------------------------------------------+---------------------------------------------------------------------------+
| Method    | URI                                             | Name                                     | Action                                                                    |
+-----------+-------------------------------------------------+------------------------------------------+---------------------------------------------------------------------------+
| POST      | api/profiles/{profile}/image                    | api.profiles.relation.image.store        | App\Http\Controllers\Api\ProfileImageController@store                     |
| GET|HEAD  | api/profiles/{profile}/image/{image?}           | api.profiles.relation.image.show         | App\Http\Controllers\Api\ProfileImageController@show                      |
| PATCH|PUT | api/profiles/{profile}/image/{image?}           | api.profiles.relation.image.update       | App\Http\Controllers\Api\ProfileImageController@update                    |
| DELETE    | api/profiles/{profile}/image/{image?}           | api.profiles.relation.image.destroy      | App\Http\Controllers\Api\ProfileImageController@destroy                   |
最後のパラメータが省略可能になっている点に注目してください。1対1のリレーションであるため、関連キーを指定しなくてもエンドポイントにアクセスできます。Orionが自動的に処理します。

hasOnehasOneThroughmorphOnebelongsTomorphToリレーションでは、関連リソースのキーを指定する必要はありません。

1対多

次のリレーションは1対多のリレーションとして扱われます。

  • hasMany
  • hasManyThrough
  • morphMany

1対多のリレーションに対して、Orionは11のエンドポイント(CRUD操作、検索、関連付け、関連付け解除のエンドポイント)を提供します: indexsearchstoreshowupdatedestroyassociatedissociatebatchStorebatchUpdatebatchDestroy

ルート登録の例

Orion::hasManyResource('users', 'posts' , UserPostsController::class);

利用可能なエンドポイントの例

+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+
| Method    | URI                                             | Name                                   | Action                                                                    |
+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+
| GET|HEAD  | api/users/{user}/posts                          | api.users.relation.posts.index         | App\Http\Controllers\Api\UserPostsController@index                        |
| POST      | api/users/{user}/posts/search                   | api.users.relation.posts.search        | App\Http\Controllers\Api\UserPostsController@index                        |
| POST      | api/users/{user}/posts                          | api.users.relation.posts.store         | App\Http\Controllers\Api\UserPostsController@store                        |
| GET|HEAD  | api/users/{user}/posts/{post}                   | api.users.relation.posts.show          | App\Http\Controllers\Api\UserPostsController@show                         |
| PATCH     | api/users/{user}/posts/{post}                   | api.users.relation.posts.update        | App\Http\Controllers\Api\UserPostsController@update                       |
| PUT       | api/users/{user}/posts/{post}                   | api.users.relation.posts.update        | App\Http\Controllers\Api\UserPostsController@update                       |
| DELETE    | api/users/{user}/posts/{post}                   | api.users.relation.posts.destroy       | App\Http\Controllers\Api\UserPostsController@destroy                      |
| POST      | api/users/{user}/posts/associate                | api.users.relation.posts.associate     | App\Http\Controllers\Api\UserPostsController@associate                    |
| DELETE    | api/users/{user}/posts/{post}/dissociate        | api.users.relation.posts.dissociate    | App\Http\Controllers\Api\UserPostsController@dissociate                   |
| POST      | api/users/{user}/posts/batch                    | api.users.relation.posts.batchStore    | App\Http\Controllers\Api\UserPostsController@batchStore                   |
| PATCH     | api/users/{user}/posts/batch                    | api.users.relation.posts.batchUpdate   | App\Http\Controllers\Api\UserPostsController@batchUpdate                  |
| DELETE    | api/users/{user}/posts/batch                    | api.users.relation.posts.batchDestroy  | App\Http\Controllers\Api\UserPostsController@batchDestroy                 |

関連付け(associate)

1対多のリレーションリソースは、リレーションのモデルを親モデルに関連付けるためのassociateエンドポイントを提供します。

このエンドポイントへのリクエストペイロードにはrelated_keyという1つのフィールドだけがあります。この例では、related_keyはユーザーに関連付ける投稿のIDになります。

リクエストの例:

// (POST) https://myapp.com/api/users/{user}/posts/associate
{
    "related_key" : 5
}

関連付けの解除(dissociate)

1対多のリレーションリソースは、リレーションのモデルを親モデルから切り離すためのdissociateエンドポイントも提供します。

このエンドポイントのリクエストにペイロードはありません。ただし、上記のルート例にある{post}ルートパラメータに注目してください。これがユーザーから切り離す投稿のIDになります。

多対多

次のリレーションは多対多のリレーションとして扱われます。

  • belongsToMany
  • morphToMany

多対多のリレーションに対して、Orionは14のエンドポイント(CRUD操作、検索、アタッチ、デタッチ、同期、トグル、ピボット更新のエンドポイント)を提供します: indexsearchstoreshowupdatedestroyattachdetachsynctogglepivotbatchStorebatchUpdatebatchDestroy

ルート登録の例

Orion::belongsToManyResource('users', 'roles' , UserRolesController::class);

利用可能なエンドポイントの例

+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+
| Method    | URI                                             | Name                                   | Action                                                                    |
+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+
| GET|HEAD  | api/users/{user}/roles                          | api.users.relation.roles.index         | App\Http\Controllers\Api\UserRolesController@index                        |
| POST      | api/users/{user}/roles/search                   | api.users.relation.roles.search        | App\Http\Controllers\Api\UserRolesController@index                        |
| POST      | api/users/{user}/roles                          | api.users.relation.roles.store         | App\Http\Controllers\Api\UserRolesController@store                        |
| GET|HEAD  | api/users/{user}/roles/{role}                   | api.users.relation.roles.show          | App\Http\Controllers\Api\UserRolesController@show                         |
| PATCH     | api/users/{user}/roles/{role}                   | api.users.relation.roles.update        | App\Http\Controllers\Api\UserRolesController@update                       |
| PUT       | api/users/{user}/roles/{role}                   | api.users.relation.roles.update        | App\Http\Controllers\Api\UserRolesController@update                       |
| DELETE    | api/users/{user}/roles/{role}                   | api.users.relation.roles.destroy       | App\Http\Controllers\Api\UserRolesController@destroy                      |
| POST      | api/users/{user}/roles/attach                   | api.users.relation.roles.attach        | App\Http\Controllers\Api\UserRolesController@attach                       |
| DELETE    | api/users/{user}/roles/detach                   | api.users.relation.roles.detach        | App\Http\Controllers\Api\UserRolesController@detach                       |
| PATCH     | api/users/{user}/roles/sync                     | api.users.relation.roles.sync          | App\Http\Controllers\Api\UserRolesController@sync                         |
| PATCH     | api/users/{user}/roles/toggle                   | api.users.relation.roles.toggle        | App\Http\Controllers\Api\UserRolesController@toggle                       |
| PATCH     | api/users/{user}/roles/{role}/pivot             | api.users.relation.roles.pivot         | App\Http\Controllers\Api\UserRolesController@updatePivot                  |
| POST      | api/users/{user}/roles/batch                    | api.users.relation.roles.batchStore    | App\Http\Controllers\Api\UserRolesController@batchStore                   |
| PATCH     | api/users/{user}/roles/batch                    | api.users.relation.roles.batchUpdate   | App\Http\Controllers\Api\UserRolesController@batchUpdate                  |
| DELETE    | api/users/{user}/roles/batch                    | api.users.relation.roles.batchDestroy  | App\Http\Controllers\Api\UserRolesController@batchDestroy                 |
ピボットテーブルに追加のフィールドがある場合は、Fillableなピボットフィールドとキャストセクションで説明したとおりにfillableなピボットフィールドを必ず定義してください。定義しない場合、attachsynctoggleupdatePivotエンドポイントでこれらのフィールドが期待どおりに動作しないことがあります。

アタッチ

多対多のリレーションリソースは、1つまたは複数の関連モデルを別のモデルにアタッチするためのattachエンドポイントを提供します。Laravelにおける関連モデルのアタッチ/デタッチの動作の詳細については、LaravelドキュメントのAttaching / Detachingセクションを参照してください。

リクエストペイロードは、必須のresourcesフィールドと任意のduplicatesフィールドで構成されます。duplicatesフィールドはクエリパラメータとしても指定できます。

resourcesフィールドには配列またはオブジェクトを指定できます。配列の場合、配列の各要素はアタッチしたい関連モデルのIDになります。オブジェクトの場合、キーは関連モデルのIDになり、値は関連モデルのアタッチ時に設定するピボットテーブルのフィールドを含むオブジェクトになります。

デフォルトでは、duplicatesパラメータはfalseです。trueに設定すると、同じ関連モデルを複数回アタッチした場合にピボットテーブルにエントリが重複して作成されます。

リクエストの例(配列版):

// (POST) https://myapp.com/api/users/{user}/roles/attach
{
    "resources" : [3,4,7]
}

リクエストの例(オブジェクト版):

// (POST) https://myapp.com/api/users/{user}/roles/attach
{
    "resources" : {
        "3" : {
            "example_pivot_field" : "value A",
            ...
        },
        "4" : {
            "example_pivot_field" : "value B",
            ...
        },
        "7" : {
            "example_pivot_field" : "value C",
            ...
        }
    }
}

デタッチ

多対多のリレーションリソースは、1つまたは複数の関連モデルを、アタッチ先のモデルからデタッチするためのdetachエンドポイントを提供します。

リクエストペイロードはresourcesという1つのフィールドのみで構成されます。

attachエンドポイントと同様に、resourcesフィールドには配列またはオブジェクトを指定できます。このメソッドでresourcesフィールドのオブジェクト表現をサポートすることで、フロントエンド側は関連リソースのアタッチ/デタッチを統一された方法で行いやすくなります。また、これらのオブジェクトに追加のデータを任意で格納し、beforeDetachafterDetachフックで利用することもできます。

リクエストの例(配列版):

// (DELETE) https://myapp.com/api/users/{user}/roles/detach
{
    "resources" : [3,4,7]
}

リクエストの例(オブジェクト版):

// (DELETE) https://myapp.com/api/users/{user}/roles/detach
{
    "resources" : {
        "3" : {},
        "4" : {
            "some_field" : "some value",
            ...
        },
        "7" : {},
    }
}

同期(sync)

多対多のリレーションリソースは、1つまたは複数の関連モデルの関連付けを別のモデルと同期するためのsyncエンドポイントを提供します。Laravelにおける関連モデルの同期の動作の詳細については、LaravelドキュメントのSyncing Associationsセクションを参照してください。

リクエストペイロードは、必須のresourcesフィールドと任意のdetachingフィールドで構成されます。detachingフィールドはクエリパラメータとしても指定できます。

resourcesフィールドには配列またはオブジェクトを指定できます。配列の場合、配列の各要素は同期したい関連モデルのIDになります。オブジェクトの場合、キーは関連モデルのIDになり、値は関連モデルの同期時に設定するピボットテーブルのフィールドを含むオブジェクトになります。

デフォルトでは、detachingパラメータはtrueです。falseに設定すると、ペイロードには含まれないがピボットテーブルには存在する関連モデルがデタッチされなくなります。

リクエストの例(配列版):

// (PATCH) https://myapp.com/api/users/{user}/roles/sync
{
    "resources" : [3,4]
}

リクエストの例(オブジェクト版):

// (PATCH) https://myapp.com/api/users/{user}/roles/sync
{
    "resources" : {
        "3" : {
            "example_pivot_field" : "value A",
            ...
        },
        "4" : {
            "example_pivot_field" : "value B",
            ...
        },
    }
}

トグル

多対多のリレーションリソースは、1つまたは複数の関連モデルのアタッチ状態を「トグル」するためのtoggleエンドポイントを提供します。Laravelにおける関連モデルの「トグル」の動作の詳細については、LaravelドキュメントのToggling Associationsセクションを参照してください。

リクエストペイロードはresourcesという1つのフィールドのみで構成されます。syncエンドポイントと同様に、resourcesフィールドには配列またはオブジェクトを指定できます。

リクエストの例(配列版):

// (PATCH) https://myapp.com/api/users/{user}/roles/toggle
{
    "resources" : [3,4]
}

リクエストの例(オブジェクト版):

// (PATCH) https://myapp.com/api/users/{user}/roles/toggle
{
    "resources" : {
        "3" : {
            "example_pivot_field" : "value A",
            ...
        },
        "4" : {
            "example_pivot_field" : "value B",
            ...
        },
    }
}

ピボットの更新

多対多のリレーションリソースは、いずれかの関連モデルのピボット行を更新するためのpivotエンドポイントを提供します。ピボット行の更新方法の詳細については、LaravelドキュメントのUpdating A Record On A Pivot Tableセクションを参照してください。

リクエストペイロードはpivotという1つのフィールドのみで構成されます。そのプロパティは、関連モデルに対して更新されるピボットテーブルのフィールドです。

リクエストの例:

// (PATCH) https://myapp.com/api/users/{user}/roles/{role}/pivot
{
    "pivot" : { // プロパティはピボットテーブルのカラムに対応します
        "example_pivot_field" : "updated value",
        "another_pivot_field" : "new value"
        ...
    }
}

キーのカスタマイズ

モデルリソースと同様に、リレーションリソースは主キーを使用してデータベースからリソースを取得します。

リレーションリソースのキーのカスタマイズ

<?php

namespace App\Http\Controllers\Api;

use App\Models\Team;
use Orion\Http\Controllers\RelationController;

class TeamPostsController extends RelationController
{
    /**
     * モデルの完全修飾クラス名
     */
    protected $model = Team::class; // または "App\Models\Team"
    
    /**
     * Post モデルで定義されているリレーション名
     */
    protected $relation = 'posts';

    /**
     * データベースからリソースを取得するために使用されるフィールド名。
     *
     * @return string
     */
    protected function keyName(): string
    {
        return 'slug'; // ここでの "slug" は Post モデル(posts リレーション)のフィールドです
    }
}

親リソースのキーのカスタマイズ

親リソースとリレーションリソースの両方がカスタムキーを使用する場合は、parentKeyNameメソッドもオーバーライドする必要があります。

<?php

namespace App\Http\Controllers\Api;

use App\Models\Team;
use Orion\Http\Controllers\RelationController;

class TeamPostsController extends RelationController
{
    /**
     * モデルの完全修飾クラス名
     */
    protected $model = Team::class; // または "App\Models\Team"
    
    /**
     * Post モデルで定義されているリレーション名
     */
    protected $relation = 'posts';

     /**
     * データベースから親リソースを取得するために使用されるフィールド名。
     *
     * @return string
     */
    protected function parentKeyName(): string
    {
        return 'short_name'; // ここでの "short_name" は Team モデルのフィールドです
    }

    /**
     * データベースからリソースを取得するために使用されるフィールド名。
     *
     * @return string
     */
    protected function keyName(): string
    {
        return 'slug'; // ここでの "slug" は Post モデル(posts リレーション)のフィールドです
    }
}

クエリのカスタマイズ

モデルコントローラーと同様に、各エンドポイントのEloquentクエリを再定義できます。唯一の大きな違いは、リレーションコントローラーの各エンドポイントには、リレーションの親モデルを取得するための「build」および「run」メソッドも用意されている点です。

標準操作のメソッド

メソッドBuild(親)Run(親)BuildRunPerform
indexbuildIndexParentFetchQueryrunIndexParentFetchQuerybuildIndexFetchQueryrunIndexFetchQuery-
storebuildStoreParentFetchQueryrunStoreParentFetchQuerybuildStoreFetchQueryrunStoreFetchQueryperformStore
showbuildShowParentFetchQueryrunShowParentFetchQuerybuildShowFetchQueryrunShowFetchQuery-
updatebuildUpdateParentFetchQueryrunUpdateParentFetchQuerybuildUpdateFetchQueryrunUpdateFetchQueryperformUpdate
destroybuildDestroyParentFetchQueryrunDestroyParentFetchQuerybuildDestroyFetchQueryrunDestroyFetchQueryperformDestroy
restorebuildRestoreParentFetchQueryrunRestoreParentFetchQuerybuildRestoreFetchQueryrunRestoreFetchQueryperformRestore

1対多操作のメソッド

メソッドBuild(親)Run(親)BuildRunPerform
associatebuildAssociateParentFetchQueryrunAssociateParentFetchQuerybuildAssociateFetchQueryrunAssociateFetchQueryperformAssociate
dissociatebuildDissociateParentFetchQueryrunDissociateParentFetchQuerybuildDissociateFetchQueryrunDissociateFetchQueryperformDissociate

多対多操作のメソッド

メソッドBuild(親)Run(親)BuildRunPerform
attachbuildAttachParentFetchQueryrunAttachParentFetchQuery--performAttach
detachbuildDetachParentFetchQueryrunDetachParentFetchQuery--performDetach
syncbuildSyncParentFetchQueryrunSyncParentFetchQuery--performSync
togglebuildToggleParentFetchQueryrunToggleParentFetchQuery--performToggle
updatePivotbuildUpdatePivotParentFetchQueryrunUpdatePivotParentFetchQuery--performUpdatePivot
performFillメソッドをオーバーライドすることで、モデルへの属性の設定方法をカスタマイズすることもできます。