가이드

연관관계

모델 연관관계를 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입니다.

$pivotFillable 속성에는 attach, sync, toggle, updatePivot 엔드포인트를 통해 업데이트할 수 있는 피벗 테이블 필드 목록을 담아야 합니다.

$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를 통해 리소스를 영구 삭제(강제 삭제)하는 방법은 관련 쿼리 파라미터 섹션을 참고하세요.

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

일대일

다음 연관관계는 일대일 연관관계로 간주됩니다.

  • hasOne
  • hasOneThrough
  • morphOne
  • belongsTo (hasMany 연관관계의 역방향)
  • morphTo (morphMany 연관관계의 역방향)

일대일 연관관계에 대해 Orion은 4개의 엔드포인트(기본적으로 CRUD 작업을 위한 엔드포인트)를 제공합니다: store, show, update, destroy

belongsTomorphTo 연관관계에는 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                   |
마지막 파라미터가 선택 사항으로 표시된 것에 주목하세요. 일대일 연관관계이므로 관련 키를 제공하지 않고도 엔드포인트에 접근할 수 있습니다. 나머지는 Orion이 알아서 처리합니다 🙂

hasOne, hasOneThrough, morphOne, belongsTo, morphTo 연관관계는 관련 리소스 키를 요구하지 않습니다.

일대다

다음 연관관계는 일대다 연관관계로 간주됩니다.

  • hasMany
  • hasManyThrough
  • morphMany

일대다 연관관계에 대해 Orion은 11개의 엔드포인트(CRUD 작업, 검색, 연관 짓기, 연관 해제를 위한 엔드포인트)를 제공합니다: index, search, store, show, update, destroy, associate, dissociate, batchStore, batchUpdate, batchDestroy

라우트 등록 예시

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)

일대다 연관관계 리소스는 연관관계 모델을 부모 모델과 연관 짓기 위한 associate 엔드포인트를 제공합니다.

이 엔드포인트의 요청 페이로드에는 related_key라는 단 하나의 필드만 있습니다. 이 예시에서 related_key는 사용자와 연관 지을 게시글의 ID가 됩니다.

요청 예시:

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

연관 해제 (Dissociate)

일대다 연관관계 리소스는 연관관계 모델을 부모 모델에서 연관 해제하기 위한 dissociate 엔드포인트도 제공합니다.

이 엔드포인트의 요청에는 페이로드가 없습니다. 다만 위 예시 라우트의 {post} 라우트 파라미터에 주목하세요. 이 값이 사용자에게서 연관 해제할 게시글의 ID가 됩니다.

다대다

다음 연관관계는 다대다 연관관계로 간주됩니다.

  • belongsToMany
  • morphToMany

다대다 연관관계에 대해 Orion은 14개의 엔드포인트(CRUD 작업, 검색, 연결, 연결 해제, 동기화, 토글, 피벗 업데이트를 위한 엔드포인트)를 제공합니다: index, search, store, show, update, destroy, attach, detach, sync, toggle, pivot, batchStore, batchUpdate, batchDestroy

라우트 등록 예시

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 피벗 필드를 반드시 정의하세요. 그렇지 않으면 attach, sync, toggle, updatePivot 엔드포인트가 해당 필드에 대해 예상대로 동작하지 않을 수 있습니다.

연결 (Attach)

다대다 연관관계 리소스는 하나 또는 여러 개의 관련 모델을 다른 모델에 연결하기 위한 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",
            ...
        }
    }
}

연결 해제 (Detach)

다대다 연관관계 리소스는 하나 또는 여러 개의 관련 모델을 연결된 모델에서 연결 해제하기 위한 detach 엔드포인트를 제공합니다.

요청 페이로드는 resources라는 단 하나의 필드로 구성됩니다.

attach 엔드포인트와 마찬가지로 resources 필드는 배열 또는 객체일 수 있습니다. 이 메서드에서 resources 필드의 객체 표현을 지원하면 프런트엔드에서 관련 리소스를 표준화된 방식으로 연결/연결 해제하기가 더 쉬워집니다. 또한 이 객체에 추가 데이터를 선택적으로 담아 beforeDetach 또는 afterDetach 훅에서 활용할 수도 있습니다.

요청 예시 (배열 방식):

// (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)

다대다 연관관계 리소스는 하나 또는 여러 개의 관련 모델과 다른 모델 간의 연관을 동기화하기 위한 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",
            ...
        },
    }
}

토글 (Toggle)

다대다 연관관계 리소스는 하나 또는 여러 개의 관련 모델의 연결 상태를 "토글"하기 위한 toggle 엔드포인트를 제공합니다. Laravel에서 관련 모델의 "토글"이 동작하는 방식에 대한 자세한 내용은 Laravel 문서의 Toggling Associations 섹션을 참고하세요.

요청 페이로드는 resources라는 단 하나의 필드로 구성됩니다. 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이라는 단 하나의 필드로 구성됩니다. 이 필드의 속성은 관련 모델에 대해 업데이트될 피벗 테이블 필드입니다.

요청 예시:

// (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

일대다 작업 메서드

메서드Build (부모)Run (부모)BuildRunPerform
associatebuildAssociateParentFetchQueryrunAssociateParentFetchQuerybuildAssociateFetchQueryrunAssociateFetchQueryperformAssociate
dissociatebuildDissociateParentFetchQueryrunDissociateParentFetchQuerybuildDissociateFetchQueryrunDissociateFetchQueryperformDissociate

다대다 작업 메서드

메서드Build (부모)Run (부모)BuildRunPerform
attachbuildAttachParentFetchQueryrunAttachParentFetchQuery--performAttach
detachbuildDetachParentFetchQueryrunDetachParentFetchQuery--performDetach
syncbuildSyncParentFetchQueryrunSyncParentFetchQuery--performSync
togglebuildToggleParentFetchQueryrunToggleParentFetchQuery--performToggle
updatePivotbuildUpdatePivotParentFetchQueryrunUpdatePivotParentFetchQuery--performUpdatePivot
performFill 메서드를 오버라이드하면 모델의 속성이 채워지는 방식도 커스터마이징할 수 있습니다.