컨트롤러 (Controllers)
소개
모든 요청 처리 로직을 라우트 파일에서 클로저로 정의하는 대신, "컨트롤러" 클래스를 사용해 이러한 동작을 체계적으로 관리할 수 있습니다. 컨트롤러는 관련된 요청 처리 로직을 하나의 클래스에 모아둘 수 있습니다. 예를 들어, UserController 클래스가 사용자의 조회, 생성, 수정, 삭제 등과 같이 사용자와 관련된 모든 요청을 처리하도록 할 수 있습니다. 기본적으로 컨트롤러는 app/Http/Controllers 디렉토리에 저장됩니다.
컨트롤러 작성
기본 컨트롤러
기본적인 컨트롤러 예제를 살펴보겠습니다. 이 컨트롤러는 라라벨에 내장된 기본 컨트롤러 클래스인 App\Http\Controllers\Controller를 확장합니다.
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
class UserController extends Controller
{
/**
* 주어진 사용자의 프로필을 보여줍니다.
*
* @param int $id
* @return \Illuminate\View\View
*/
public function show($id)
{
return view('user.profile', [
'user' => User::findOrFail($id)
]);
}
}
아래와 같이 이 컨트롤러 메서드에 대한 라우트를 정의할 수 있습니다.
use App\Http\Controllers\UserController;
Route::get('/user/{id}', [UserController::class, 'show']);
들어오는 요청이 지정한 라우트 URI와 일치하면, App\Http\Controllers\UserController 클래스의 show 메서드가 호출되고, 라우트 파라미터가 해당 메서드에 전달됩니다.
컨트롤러는 반드시 기본 클래스를 상속할 필요는 없습니다. 그러나 기본 클래스를 상속하지 않으면
middleware,authorize와 같은 편리한 기능을 사용할 수 없습니다.
단일 액션 컨트롤러
컨트롤러에서 처리하는 액션이 아주 복잡하다면, 해당 액션만을 위한 전용 컨트롤러 클래스를 만드는 것이 편할 수 있습니다. 이럴 때는 컨트롤러 안에 __invoke 메서드만 하나 정의하면 됩니다.
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
class ProvisionServer extends Controller
{
/**
* 새 웹 서버를 셋업합니다.
*
* @return \Illuminate\Http\Response
*/
public function __invoke()
{
// ...
}
}
단일 액션 컨트롤러에 라우트를 등록할 때는, 컨트롤러 메서드명을 따로 지정하지 않고, 컨트롤러 이름만 라우터에 넘기면 됩니다.
use App\Http\Controllers\ProvisionServer;
Route::post('/server', ProvisionServer::class);
make:controller Artisan 명령어의 --invokable 옵션을 사용해 인보커블(단일 액션) 컨트롤러를 생성할 수 있습니다.
php artisan make:controller ProvisionServer --invokable
컨트롤러 스텁은 스텁 게시를 통해 커스터마이즈 할 수 있습니다.
컨트롤러 미들웨어
미들웨어는 라우트 파일 내 컨트롤러 라우트에 지정할 수 있습니다.
Route::get('profile', [UserController::class, 'show'])->middleware('auth');
혹은 컨트롤러의 생성자에서 미들웨어를 지정하는 것이 더 편리할 수도 있습니다. 컨트롤러의 생성자 안에서 middleware 메서드를 사용하면, 컨트롤러의 특정 액션에 미들웨어를 할당할 수 있습니다.
class UserController extends Controller
{
/**
* 새 컨트롤러 인스턴스를 생성합니다.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('log')->only('index');
$this->middleware('subscribed')->except('store');
}
}
컨트롤러에서는 미들웨어를 클로저로도 등록할 수 있습니다. 즉, 하나의 컨트롤러에서만 사용할 인라인 미들웨어를 전체 미들웨어 클래스를 따로 정의하지 않고 쉽게 만들 수 있습니다.
$this->middleware(function ($request, $next) {
return $next($request);
});
리소스 컨트롤러
애플리케이션의 각 Eloquent 모델을 "리소스"로 생각해 보면, 보통 각 리소스별로 비슷한 작업(생성, 조회, 수정, 삭제 등)을 반복하게 됩니다. 예를 들어, 애플리케이션에 Photo 모델과 Movie 모델이 있다면, 사용자들은 이 리소스들을 생성, 조회, 수정, 삭제할 가능성이 높습니다.
이처럼 자주 반복되는 경우를 위해, 라라벨의 리소스 라우팅은 한 줄의 코드로 전형적인 생성, 조회, 수정, 삭제("CRUD") 라우트를 컨트롤러에 할당해 줍니다. 먼저, make:controller Artisan 명령어에 --resource 옵션을 사용해 이러한 동작을 처리할 컨트롤러를 쉽게 생성할 수 있습니다.
php artisan make:controller PhotoController --resource
위 명령어는 app/Http/Controllers/PhotoController.php 경로에 컨트롤러를 생성하며, 리소스별 작업을 위한 메서드가 포함되어 있습니다. 다음으로, 생성한 컨트롤러로 리소스 라우트를 등록합니다.
use App\Http\Controllers\PhotoController;
Route::resource('photos', PhotoController::class);
이 한 줄의 라우트 선언으로 해당 리소스에 다양한 작업을 처리하는 여러 라우트가 즉시 생성됩니다. 만들어진 컨트롤러는 각각의 액션에 대한 스텁 메서드를 이미 포함하고 있습니다. 애플리케이션의 전체 라우트를 빠르게 확인하고 싶을 때는 route:list Artisan 명령어를 실행하면 됩니다.
아래와 같이 resources 메서드에 배열을 넘기면 여러 리소스 컨트롤러를 한 번에 등록할 수도 있습니다.
Route::resources([
'photos' => PhotoController::class,
'posts' => PostController::class,
]);
리소스 컨트롤러가 처리하는 액션
| Verb | URI | 액션 | 라우트 이름 |
|---|---|---|---|
| GET | /photos | index | photos.index |
| GET | /photos/create | create | photos.create |
| POST | /photos | store | photos.store |
| GET | /photos/{photo} | show | photos.show |
| GET | /photos/{photo}/edit | edit | photos.edit |
| PUT/PATCH | /photos/{photo} | update | photos.update |
| DELETE | /photos/{photo} | destroy | photos.destroy |
미존재 모델 처리 동작 커스터마이즈
일반적으로, 암묵적으로 바인딩된 리소스 모델을 찾지 못하면 404 HTTP 응답이 반환됩니다. 하지만, missing 메서드를 이용해 리소스 라우트에 대한 이 동작을 커스터마이즈할 수 있습니다. missing 메서드는 암묵적으로 바인딩된 모델을 찾을 수 없을 때 호출되는 클로저를 인자로 받습니다.
use App\Http\Controllers\PhotoController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
Route::resource('photos', PhotoController::class)
->missing(function (Request $request) {
return Redirect::route('photos.index');
});
리소스 모델 지정
라우트 모델 바인딩을 활용하며, 리소스 컨트롤러의 메서드에서 모델 인스턴스를 타입힌트로 지정하고 싶다면, 컨트롤러를 생성할 때 --model 옵션을 사용할 수 있습니다.
php artisan make:controller PhotoController --model=Photo --resource
폼 리퀘스트 클래스 자동 생성
리소스 컨트롤러를 생성할 때 --requests 옵션도 함께 주면, 컨트롤러의 저장 및 수정 메서드에서 사용할 폼 리퀘스트 클래스가 Artisan에 의해 자동 생성됩니다.
php artisan make:controller PhotoController --model=Photo --resource --requests
부분 리소스 라우트
리소스 라우트를 선언할 때, 컨트롤러가 전체 기본 액션 대신 일부 액션만 처리하도록 지정할 수 있습니다.
use App\Http\Controllers\PhotoController;
Route::resource('photos', PhotoController::class)->only([
'index', 'show'
]);
Route::resource('photos', PhotoController::class)->except([
'create', 'store', 'update', 'destroy'
]);
API 리소스 라우트
API에서 사용될 리소스 라우트를 선언할 때는, create와 edit처럼 HTML 템플릿을 제공하는 라우트를 보통 제외하게 됩니다. 이런 경우를 위해 apiResource 메서드를 사용하면 두 라우트가 자동으로 빠집니다.
use App\Http\Controllers\PhotoController;
Route::apiResource('photos', PhotoController::class);
아래와 같이 여러 API 리소스 컨트롤러도 한 번에 등록할 수 있습니다.
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\PostController;
Route::apiResources([
'photos' => PhotoController::class,
'posts' => PostController::class,
]);
make:controller 명령어 실행 시 --api 옵션을 사용하면, create나 edit 메서드 없이 빠르게 API 리소스 컨트롤러를 생성할 수 있습니다.
php artisan make:controller PhotoController --api
중첩 리소스
경우에 따라 중첩된 리소스에 대한 라우트가 필요할 수 있습니다. 예를 들어, 포토 리소스에는 여러 개의 댓글이 달릴 수 있습니다. 이런 중첩 리소스 컨트롤러를 라우트 선언에서 "점" 표기법으로 정의할 수 있습니다.
use App\Http\Controllers\PhotoCommentController;
Route::resource('photos.comments', PhotoCommentController::class);
이 라우트는 다음과 같은 URI로 중첩 리소스를 접근할 수 있게 됩니다.
/photos/{photo}/comments/{comment}
중첩 리소스 스코핑
라라벨의 암묵적 모델 바인딩 기능을 이용하면, 고유적으로 스코프된 중첩 바인딩이 가능해집니다. 즉, 자식 모델이 반드시 부모 모델에 속해 있는지 확인하는 방식입니다. 중첩 리소스를 정의할 때 scoped 메서드를 사용하면 자동 스코핑을 활성화할 수 있고, 자식 리소스를 어떤 필드로 조회할지도 지정할 수 있습니다. 자세한 내용은 리소스 라우트 스코핑 문서를 참고하세요.
얕은 중첩(Shallow Nesting)
일반적으로, 자식 리소스의 ID가 이미 고유한 경우 URI에 부모와 자식 ID를 모두 포함할 필요가 없습니다. 예를 들어, 오토 인크리먼트된 기본 키 등 고유 식별자를 URI 세그먼트로 사용하는 경우, "얕은(Shallow) 중첩"을 선택할 수 있습니다.
use App\Http\Controllers\CommentController;
Route::resource('photos.comments', CommentController::class)->shallow();
해당 라우트 정의는 아래와 같은 라우트를 만듭니다.
| Verb | URI | 액션 | 라우트 이름 |
|---|---|---|---|
| GET | /photos/{photo}/comments | index | photos.comments.index |
| GET | /photos/{photo}/comments/create | create | photos.comments.create |
| POST | /photos/{photo}/comments | store | photos.comments.store |
| GET | /comments/{comment} | show | comments.show |
| GET | /comments/{comment}/edit | edit | comments.edit |
| PUT/PATCH | /comments/{comment} | update | comments.update |
| DELETE | /comments/{comment} | destroy | comments.destroy |
리소스 라우트 이름 지정
기본적으로 리소스 컨트롤러의 모든 액션에는 라우트 이름이 자동으로 지정되지만, names 배열을 넘겨 원하는 이름으로 직접 지정할 수도 있습니다.
use App\Http\Controllers\PhotoController;
Route::resource('photos', PhotoController::class)->names([
'create' => 'photos.build'
]);
리소스 라우트 파라미터 이름 지정
기본적으로 Route::resource는 "단수화된" 리소스 이름을 기준으로 라우트 파라미터를 생성합니다. 이 파라미터 명칭을 리소스별로 변경하려면 parameters 메서드를 사용할 수 있습니다. 이 메서드에 넘기는 배열은 리소스명과 파라미터명을 매칭하는 연관 배열이어야 합니다.
use App\Http\Controllers\AdminUserController;
Route::resource('users', AdminUserController::class)->parameters([
'users' => 'admin_user'
]);
위 예제는 리소스의 show 라우트에 대해 다음과 같은 URI를 생성합니다.
/users/{admin_user}
리소스 라우트 스코핑
라라벨의 스코프 암묵적 모델 바인딩는 중첩된 라우트에서 자식 모델이 반드시 부모 모델에 속하는지 자동으로 확인해 줍니다. 중첩 리소스를 정의할 때 scoped 메서드로 자동 스코핑을 활성화할 수 있으며, 자식 리소스를 어떤 필드로 조회할지도 간편하게 지정 가능합니다.
use App\Http\Controllers\PhotoCommentController;
Route::resource('photos.comments', PhotoCommentController::class)->scoped([
'comment' => 'slug',
]);
이 라우트는 다음과 같이 스코프된 중첩 리소스를 사용할 수 있게 만듭니다.
/photos/{photo}/comments/{comment:slug}
커스텀 키를 사용하는 암묵적 바인딩을 중첩 라우트 파라미터로 쓰면, 라라벨은 부모 관계명을 추측해서 쿼리를 자동으로 스코프 처리합니다. 위 예시에서는 Photo 모델에 comments(파라미터 이름의 복수형)라는 관계가 있다고 간주하여 Comment 모델을 조회합니다.
리소스 URI 현지화
기본적으로 Route::resource는 리소스 URI에 영어 동사를 사용합니다. 만약 create와 edit 액션의 동사를 현지화(다른 언어로 변경)하려면, 애플리케이션의 App\Providers\RouteServiceProvider 클래스의 boot 메서드 초입에서 Route::resourceVerbs 메서드를 사용할 수 있습니다.
/**
* 라우트 모델 바인딩, 패턴 필터 등을 정의합니다.
*
* @return void
*/
public function boot()
{
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);
// ...
}
동사를 커스터마이징 한 뒤, 예를 들어 Route::resource('fotos', PhotoController::class)를 등록하면, 아래와 같은 URI가 생성됩니다.
/fotos/crear
/fotos/{foto}/editar
리소스 컨트롤러 보완
기본 리소스 라우트 외에 추가 라우트가 필요하다면, 반드시 Route::resource 호출 이전에 보조 라우트를 정의해야 합니다. 그렇지 않으면 resource 메서드가 생성하는 라우트가 직접 정의한 추가 라우트보다 우선 적용될 수 있습니다.
use App\Http\Controller\PhotoController;
Route::get('/photos/popular', [PhotoController::class, 'popular']);
Route::resource('photos', PhotoController::class);
컨트롤러의 책임을 명확히 하세요. 만약 리소스 기본 액션 외의 메서드가 자주 필요하다면, 컨트롤러를 더 작고 목적별로 분리하는 것이 좋습니다.
의존성 주입과 컨트롤러
생성자 인젝션
라라벨의 서비스 컨테이너는 모든 컨트롤러를 자동으로 resolve(해결)합니다. 따라서, 컨트롤러의 생성자에 필요한 의존성을 타입힌트로 지정하면 라라벨이 자동으로 주입해 줍니다. 아래 예제를 참고하세요.
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
class UserController extends Controller
{
/**
* UserRepository 인스턴스입니다.
*/
protected $users;
/**
* 새 컨트롤러 인스턴스를 생성합니다.
*
* @param \App\Repositories\UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
}
메서드 인젝션
생성자 인젝션 외에도, 컨트롤러의 메서드에 타입힌트로 의존성을 전달받을 수 있습니다. 가장 흔한 예시로, Illuminate\Http\Request 인스턴스를 컨트롤러 메서드에 주입할 수 있습니다.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* 새 유저를 저장합니다.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$name = $request->name;
//
}
}
컨트롤러 메서드가 라우트 파라미터도 함께 받는 경우, 의존성 뒤에 라우트 인수를 차례로 나열하면 됩니다. 예를 들어, 다음과 같은 라우트가 있다면
use App\Http\Controllers\UserController;
Route::put('/user/{id}', [UserController::class, 'update']);
컨트롤러 메서드를 아래와 같이 정의해 Illuminate\Http\Request를 타입힌트로 받고, 라우트 인수인 id를 뒤에 추가로 받을 수 있습니다.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* 주어진 유저 정보를 업데이트합니다.
*
* @param \Illuminate\Http\Request $request
* @param string $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
}