모델
컨트롤러 설정
모델을 API로 노출하려면 먼저 해당 모델을 위한 컨트롤러를 생성해야 합니다. 시작하기 - 간단한 CRUD 섹션에서 보았듯이, 모델 컨트롤러를 정의하는 방법은 매우 간단합니다.
<?php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Orion\Http\Controllers\Controller;
class PostsController extends Controller
{
/**
* 모델의 완전한 클래스 이름
*/
protected $model = Post::class; // 또는 "App\Models\Post"
}
페이지네이션 비활성화
기본적으로 index 엔드포인트가 반환하는 리소스에는 페이지네이션이 적용됩니다. 페이지네이션을 비활성화하고 모든 리소스를 반환하려면 DisablePagination 트레이트를 사용할 수 있습니다.
<?php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Orion\Http\Controllers\Controller;
use Orion\Concerns\DisablePagination;
class PostsController extends Controller
{
use DisablePagination;
/**
* 모델의 완전한 클래스 이름
*/
protected $model = Post::class; // 또는 "App\Models\Post"
}
- 모델 컨트롤러는 항상
Orion\Http\Controllers\Controller를 상속합니다 $model속성에는 전체 네임스페이스를 포함한 모델 클래스명을 지정합니다DisablePagination트레이트를 사용하면 모델 리소스와 연관관계 리소스 모두에서 페이지네이션을 비활성화할 수 있습니다
라우트 설정
컨트롤러를 생성했다면 이제 라우트를 등록할 차례입니다.
<?php
use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;
use App\Http\Controllers\PostsController;
Route::group(['as' => 'api.'], function() {
...
Orion::resource('posts', PostsController::class);
...
});
기본적으로 Orion::resource 메서드는 Laravel의 기본 Route::apiResource와 동일하게 동작하며, 리소스에 대한 다양한 액션을 처리하는 여러 라우트를 생성합니다.
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
...
| | GET|HEAD | api/posts | api.posts.index | App\Http\Controllers\Api\PostsController@index | api |
| | POST | api/posts/search | api.posts.search | App\Http\Controllers\Api\PostsController@index | api |
| | POST | api/posts | api.posts.store | App\Http\Controllers\Api\PostsController@store | api |
| | GET|HEAD | api/posts/{post} | api.posts.show | App\Http\Controllers\Api\PostsController@show | api |
| | PUT|PATCH | api/posts/{post} | api.posts.update | App\Http\Controllers\Api\PostsController@update | api |
| | DELETE | api/posts/{post} | api.posts.destroy | App\Http\Controllers\Api\PostsController@destroy | api |
| | POST | api/posts/batch | api.posts.batchStore | App\Http\Controllers\Api\PostsController@batchStore | api |
| | PATCH | api/posts/batch | api.posts.batchUpdate | App\Http\Controllers\Api\PostsController@batchUpdate | api |
| | DELETE | api/posts/batch | api.posts.batchDestroy | App\Http\Controllers\Api\PostsController@batchDestroy | api |
소프트 삭제
모델이 SoftDeletes 트레이트를 사용하고 있고 동일한 기능을 API로도 노출하고 싶다면, 리소스를 등록할 때 withSoftDeletes 메서드를 호출하세요.
<?php
use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;
Route::group(['as' => 'api.'], function() {
...
Orion::resource('posts', 'Api\PostsController')->withSoftDeletes();
...
});
이렇게 하면 restore 및 batchRestore 엔드포인트가 추가됩니다. API를 통해 리소스를 영구적으로 삭제하는 방법(강제 삭제)은 관련 쿼리 파라미터 섹션을 참고하세요.
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+-------------------------------------------------+----------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------+
...
| | POST | api/posts/{post}/restore | api.posts.restore | App\Http\Controllers\Api\PostsController@restore | api |
| | POST | api/posts/batch/restore | api.posts.batchRestore | App\Http\Controllers\Api\PostsController@batchRestore | api |
키 커스터마이징
기본적으로 엔드포인트는 기본 키(보통 id)를 사용하여 데이터베이스에서 모델을 조회합니다. 그러나 경우에 따라 기본 키는 그대로 유지하면서 다른 필드로 모델을 조회하고 싶을 수 있습니다. 이를 위해서는 컨트롤러에서 keyName 메서드를 오버라이드하세요.
<?php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Orion\Http\Controllers\Controller;
class PostsController extends Controller
{
/**
* 모델의 완전한 클래스 이름
*/
protected $model = Post::class; // 또는 "App\Models\Post"
/**
* 데이터베이스에서 리소스를 조회하는 데 사용되는 필드 이름.
*
* @return string
*/
protected function keyName(): string
{
return 'slug';
}
}
쿼리 커스터마이징
Orion은 매우 유연하여 각 엔드포인트에서 Eloquent 쿼리를 빌드하고 실행하는 방식을 재정의할 수 있습니다.
개별 엔드포인트
쿼리 빌드
index 엔드포인트가 게시된 블로그 글만 반환하도록 하고 싶다고 가정해 보겠습니다. 이를 위해서는 컨트롤러에서 buildIndexFetchQuery 메서드를 오버라이드하세요.
<?php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Orion\Http\Controllers\Controller;
class PostsController extends Controller
{
/**
* 모델의 완전한 클래스 이름
*/
protected $model = Post::class; // 또는 "App\Models\Post"
/**
* index 메서드에서 엔티티를 조회하기 위한 Eloquent 쿼리를 구성합니다.
*
* @param Request $request
* @param array $requestedRelations
* @return Builder
*/
protected function buildIndexFetchQuery(Request $request, array $requestedRelations): Builder
{
$query = parent::buildIndexFetchQuery($request, $requestedRelations);
$query->whereNotNull('published_at');
return $query;
}
}
쿼리 실행
모델 목록을 조회할 때 특정 컬럼만 선택하는 것이 대표적인 예입니다. 이를 위해서는 runIndexFetchQuery 메서드를 오버라이드하세요.
<?php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Orion\Http\Controllers\Controller;
class PostsController extends Controller
{
/**
* 모델의 완전한 클래스 이름
*/
protected $model = Post::class; // 또는 "App\Models\Post"
...
/**
* index 메서드에서 엔티티를 조회하기 위해 지정된 쿼리를 실행합니다.
*
* @param Request $request
* @param Builder $query
* @param int $paginationLimit
* @return LengthAwarePaginator
*/
protected function runIndexFetchQuery(Request $request, Builder $query, int $paginationLimit): LengthAwarePaginator
{
return $query->paginate($paginationLimit, ['id', 'title', 'published_at']);
}
}
작업 수행
index나 show 같은 엔드포인트의 주된 목적은 데이터베이스를 변경하는 것이 아니라 데이터를 조회하는 것입니다. 반면 store, update 등의 엔드포인트는 데이터베이스를 변경합니다. 모델 저장과 같은 특정 작업이 수행되는 방식 역시 커스터마이징할 수 있습니다.
아래 예시에서는 현재 인증된 사용자가 관리자인 경우 게시글의 속성을 강제로 채웁니다(여기서 역할(role) 구현은 가상의 예시입니다).
<?php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Orion\Http\Controllers\Controller;
class PostsController extends Controller
{
/**
* 모델의 완전한 클래스 이름
*/
protected $model = Post::class; // 또는 "App\Models\Post"
...
/**
* 지정된 엔티티에 속성을 채우고 데이터베이스에 저장합니다.
*
* @param Request $request
* @param Model $entity
* @param array $attributes
*/
protected function performStore(Request $request, Model $entity, array $attributes): void
{
if ($this->resolveUser()->hasRole('admin')) {
$entity->forceFill($attributes);
} else {
$entity->fill($attributes);
}
$entity->save();
}
}
performFill 메서드를 오버라이드하여 모델에 속성이 채워지는 방식도 커스터마이징할 수 있습니다.엔드포인트 그룹
이제 index 엔드포인트는 게시된 글만 반환합니다. 그런데 동일한 제약을 show 엔드포인트에도 적용하고 싶다면 어떻게 해야 할까요? 물론 코드를 복제하여 buildShowFetchQuery 메서드를 오버라이드할 수도 있지만, 더 나은 방법이 있습니다.
쿼리 빌드
buildIndexFetchQuery 메서드의 구현을 살펴보면 내부적으로 buildFetchQuery 메서드를 사용한다는 것을 알 수 있습니다. 실제로 이 메서드는 index, show, update, destroy, restore 엔드포인트에서 모델 조회 쿼리를 빌드하는 데 사용되며, 이 메서드 역시 오버라이드할 수 있습니다.
동일한 제약을 index와 show 엔드포인트에 (그리고 update, destroy, restore에도) 한 번에 적용하려면 다음과 같이 작성합니다.
<?php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Orion\Http\Controllers\Controller;
class PostsController extends Controller
{
/**
* 모델의 완전한 클래스 이름
*/
protected $model = Post::class; // 또는 "App\Models\Post"
/**
* 엔티티를 조회하기 위한 Eloquent 쿼리를 구성합니다.
*
* @param Request $request
* @param array $requestedRelations
* @return Builder
*/
protected function buildFetchQuery(Request $request, array $requestedRelations): Builder
{
$query = parent::buildFetchQuery($request, $requestedRelations);
$query->whereNotNull('published_at');
return $query;
}
...
}
쿼리 실행
쿼리 실행 측면에서 index 엔드포인트는 다소 예외적이지만(단일 모델이 아닌 모델 목록을 조회하기 때문입니다), show, update, destroy, restore 엔드포인트는 동일한 로직을 공유하며 내부적으로 runFetchQuery를 사용합니다.
약간의 리팩터링을 거치면, 게시된 블로그 글만 조회하고 데이터베이스에서 id, title, published_at 컬럼(속성)만 가져오는 컨트롤러가 완성됩니다.
<?php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Orion\Http\Controllers\Controller;
class PostsController extends Controller
{
/**
* 모델의 완전한 클래스 이름
*/
protected $model = Post::class; // 또는 "App\Models\Post"
/**
* 데이터베이스에서 선택할 속성 목록
*/
protected $attributes = ['id', 'title', 'published_at'];
/**
* 엔티티를 조회하기 위한 Eloquent 쿼리를 구성합니다.
*
* @param Request $request
* @param array $requestedRelations
* @return Builder
*/
protected function buildFetchQuery(Request $request, array $requestedRelations): Builder
{
$query = parent::buildFetchQuery($request, $requestedRelations);
$query->whereNotNull('published_at');
return $query;
}
/**
* 엔티티를 조회하기 위해 지정된 쿼리를 실행합니다.
*
* @param Request $request
* @param Builder $query
* @param int|string $key
* @return Model
*/
protected function runFetchQuery(Request $request, Builder $query, $key): Model
{
return $query->select($this->attributes)->findOrFail($key);
}
/**
* index 메서드에서 엔티티를 조회하기 위해 지정된 쿼리를 실행합니다.
*
* @param Request $request
* @param Builder $query
* @param int $paginationLimit
* @return LengthAwarePaginator
*/
protected function runIndexFetchQuery(Request $request, Builder $query, int $paginationLimit): LengthAwarePaginator
{
return $query->paginate($paginationLimit, $this->attributes);
}
/**
* 지정된 엔티티에 속성을 채우고 데이터베이스에 저장합니다.
*
* @param Request $request
* @param Model $post
* @param array $attributes
*/
protected function performStore(Request $request, Model $post, array $attributes): void
{
if ($this->resolveUser()->hasRole('admin')) {
$post->forceFill($attributes);
} else {
$post->fill($attributes);
}
$post->save();
}
}
표준 작업 메서드
| 메서드 | Build | Run | Perform |
|---|---|---|---|
| index | buildIndexFetchQuery | runIndexFetchQuery | - |
| store | buildStoreFetchQuery | runStoreFetchQuery | performStore |
| show | buildShowFetchQuery | runShowFetchQuery | - |
| update | buildUpdateFetchQuery | runUpdateFetchQuery | performUpdate |
| destroy | buildDestroyFetchQuery | runDestroyFetchQuery | performDestroy |
| restore | buildRestoreFetchQuery | runRestoreFetchQuery | performRestore |