미들웨어
미들웨어는 일반적으로 요청이나 응답을 가로채기 위해 사용됩니다. 예를 들어, 컨트롤러를 실행하기 전에 사용자 신원을 일괄적으로 검증하거나, 사용자가 로그인하지 않은 경우 로그인 페이지로 리다이렉트하는 등의 작업이 포함됩니다. 예를 들어, 응답에 특정 헤더를 추가하거나 특정 URI 요청의 비율을 통계적으로 집계하는 등의 작업이 가능합니다.
미들웨어 양파 모델
┌──────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌──────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Request ───────────────────> Controller ── Response ───────────────────────────> Client
│ │ │ │ │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
미들웨어와 컨트롤러는 전형적인 양파 모델을 구성하며, 미들웨어는 여러 겹의 양파 껍질과 같고, 컨트롤러는 양파의 심과 같습니다. 요청은 화살처럼 미들웨어 1, 2, 3을 통과하여 컨트롤러에 도달하고, 컨트롤러는 응답을 반환하며 응답은 다시 3, 2, 1의 순서로 미들웨어를 지나 클라이언트에 반환됩니다. 즉, 각 미들웨어에서 요청과 응답을 모두 얻을 수 있습니다.
요청 가로채기
때로는 특정 요청이 컨트롤러 층에 도달하지 않도록 하고 싶습니다. 예를 들어, middleware2에서 현재 사용자가 로그인하지 않은 경우에는 요청을 직접 가로채고 로그인 응답을 반환할 수 있습니다. 이렇게 되면 이 작업의 흐름은 아래와 같습니다.
┌────────────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌────────────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Request ─────────┐ │ │ Controller │ │ │ │
│ │ Response │ │ │ │ │ │
<───────────────────┘ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
위 그림과 같이 요청이 middleware2에 도달한 후 로그인 응답을 생성하고, 응답은 middleware2에서 middleware1로 다시 이동한 후 클라이언트에 반환됩니다.
미들웨어 인터페이스
미들웨어는 반드시 Webman\MiddlewareInterface
인터페이스를 구현해야 합니다.
interface MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
*/
public function process(Request $request, callable $handler): Response;
}
즉, 반드시 process
메서드를 구현해야 하며, process
메서드는 반드시 support\Response
객체를 반환해야 합니다. 기본적으로 이 객체는 $handler($request)
에 의해 생성되며(요청은 양파 심으로 계속 전달됨), response()
, json()
, xml()
, redirect()
등의 헬퍼 함수에서 생성된 응답일 수도 있습니다(요청은 양파 심으로 더 이상 전달되지 않음).
미들웨어 내 요청 및 응답 얻기
미들웨어 내에서 요청을 얻을 수 있으며, 컨트롤러 실행 후의 응답도 얻을 수 있으므로 미들웨어 내부는 세 부분으로 나뉩니다.
- 요청 전달 단계, 즉 요청 처리 이전의 단계
- 컨트롤러 요청 처리 단계, 즉 요청 처리 단계
- 응답 전달 단계, 즉 요청 처리 이후의 단계
세 단계는 미들웨어에서 다음과 같이 나타납니다.
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Test implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
echo '여기는 요청 전달 단계, 즉 요청 처리 전입니다.';
$response = $handler($request); // 양파 심으로 계속 전달되며, 컨트롤러를 실행하여 응답을 받습니다.
echo '여기는 응답 전달 단계, 즉 요청 처리 후입니다.';
return $response;
}
}
예시: 인증 미들웨어
파일을 생성하세요 app/middleware/AuthCheckTest.php
(디렉토리가 존재하지 않으면 직접 생성하시기 바랍니다) 아래와 같이 작성합니다.
<?php
namespace app\middleware;
use ReflectionClass;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class AuthCheckTest implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
if (session('user')) {
// 이미 로그인했습니다. 요청은 양파 심으로 계속 전달됩니다.
return $handler($request);
}
// 리플렉션을 통해 컨트롤러의 어떤 메서드가 로그인이 필요하지 않은지 확인합니다.
$controller = new ReflectionClass($request->controller);
$noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
// 접근 방법이 로그인이 필요합니다.
if (!in_array($request->action, $noNeedLogin)) {
// 요청을 가로채고 리다이렉트 응답을 반환합니다. 요청은 양파 심으로 더 이상 전달되지 않습니다.
return redirect('/user/login');
}
// 로그인이 필요하지 않습니다. 요청은 양파 심으로 계속 전달됩니다.
return $handler($request);
}
}
컨트롤러를 새로 만드세요 app/controller/UserController.php
<?php
namespace app\controller;
use support\Request;
class UserController
{
/**
* 로그인이 필요하지 않은 메서드
*/
protected $noNeedLogin = ['login'];
public function login(Request $request)
{
$request->session()->set('user', ['id' => 10, 'name' => 'webman']);
return json(['code' => 0, 'msg' => 'login ok']);
}
public function info()
{
return json(['code' => 0, 'msg' => 'ok', 'data' => session('user')]);
}
}
주의
$noNeedLogin
은 현재 컨트롤러에서 로그인 없이 접근 가능한 메서드를 기록합니다.
config/middleware.php
에 전역 미들웨어를 다음과 같이 추가합니다.
return [
// 전역 미들웨어
'' => [
// ... 여기에는 다른 미들웨어가 생략됩니다.
app\middleware\AuthCheckTest::class,
]
];
인증 미들웨어가 있으면 컨트롤러 레이어에서 비즈니스 로직 작성을 집중할 수 있으며, 사용자의 로그인 여부에 대해 걱정할 필요가 없습니다.
예시: 교차 출처 요청 미들웨어
파일을 생성하세요 app/middleware/AccessControlTest.php
(디렉토리가 존재하지 않으면 직접 생성하시기 바랍니다) 아래와 같이 작성합니다.
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class AccessControlTest implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
// 요청이 options인 경우 빈 응답을 반환하고, 그렇지 않으면 양파 심으로 계속 전달되어 응답을 받습니다.
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// 응답에 교차 출처 관련 http 헤더 추가
$response->withHeaders([
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
]);
return $response;
}
}
提示
교차 출처 요청은 OPTIONS 요청을 생성할 수 있으며, 우리는 OPTIONS 요청이 컨트롤러로 들어가는 것을 원하지 않으므로 OPTIONS 요청에 대해 빈 응답(response('')
)을 직접 반환하여 요청을 가로챕니다.
만약 API가 라우팅을 설정해야 한다면Route::any(..)
또는Route::add(['POST', 'OPTIONS'], ..)
를 사용하여 설정하세요.
config/middleware.php
에 전역 미들웨어를 다음과 같이 추가합니다.
return [
// 전역 미들웨어
'' => [
// ... 여기에는 다른 미들웨어가 생략됩니다.
app\middleware\AccessControlTest::class,
]
];
주의
ajax 요청이 사용자 정의 헤더를 설정한 경우, 미들웨어의Access-Control-Allow-Headers
필드에 이 사용자 정의 헤더를 추가해야 합니다. 그렇지 않으면Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.
라는 오류가 발생합니다.
설명
- 미들웨어는 전역 미들웨어, 애플리케이션 미들웨어(애플리케이션 미들웨어는 다중 애플리케이션 모드에서만 유효합니다. 다중 애플리케이션 참조), 라우트 미들웨어로 나뉩니다.
- 미들웨어 구성 파일의 위치는
config/middleware.php
입니다. - 전역 미들웨어는 키
''
아래에 설정합니다. - 애플리케이션 미들웨어는 구체적 애플리케이션 이름 아래에 설정합니다. 예를 들어:
return [
// 전역 미들웨어
'' => [
app\middleware\AuthCheckTest::class,
app\middleware\AccessControlTest::class,
],
// api 애플리케이션 미들웨어(애플리케이션 미들웨어는 다중 애플리케이션 모드에서만 유효합니다.)
'api' => [
app\middleware\ApiOnly::class,
]
];
컨트롤러 미들웨어 및 메서드 미들웨어
주석을 활용하여 특정 컨트롤러나 컨트롤러의 특정 메서드에 미들웨어를 설정할 수 있습니다.
<?php
namespace app\controller;
use app\middleware\Controller1Middleware;
use app\middleware\Controller2Middleware;
use app\middleware\Method1Middleware;
use app\middleware\Method2Middleware;
use support\annotation\Middleware;
use support\Request;
#[Middleware(Controller1Middleware::class, Controller2Middleware::class)]
class IndexController
{
#[Middleware(Method1Middleware::class, Method2Middleware::class)]
public function index(Request $request): string
{
return 'hello';
}
}
라우트 미들웨어
특정 하거나 일련의 라우트에 미들웨어를 설정할 수 있습니다. 예를 들어 config/route.php
에 다음과 같은 구성을 추가합니다.
<?php
use support\Request;
use Webman\Route;
Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
app\middleware\MiddlewareA::class,
app\middleware\MiddlewareB::class,
]);
Route::group('/blog', function () {
Route::any('/create', function () {return response('create');});
Route::any('/edit', function () {return response('edit');});
Route::any('/view/{id}', function ($r, $id) {response("view $id");});
})->middleware([
app\middleware\MiddlewareA::class,
app\middleware\MiddlewareB::class,
]);
미들웨어 생성자 파라미터 전달
구성 파일은 미들웨어 또는 익명 함수를 직접 인스턴스화하는 것을 지원하며, 이를 통해 생성자를 통해 미들웨어에 파라미터를 전달할 수 있습니다.
예를 들어 config/middleware.php
에서도 다음과 같이 구성할 수 있습니다.
return [
// 전역 미들웨어
'' => [
new app\middleware\AuthCheckTest($param1, $param2, ...),
function(){
return new app\middleware\AccessControlTest($param1, $param2, ...);
},
],
// api 애플리케이션 미들웨어(애플리케이션 미들웨어는 다중 애플리케이션 모드에서만 유효합니다.)
'api' => [
app\middleware\ApiOnly::class,
]
];
동일하게 라우트 미들웨어도 생성자를 통해 미들웨어에 파라미터를 전달할 수 있습니다. 예를 들어 config/route.php
에 다음과 같이 작성합니다.
Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
new app\middleware\MiddlewareA($param1, $param2, ...),
function(){
return new app\middleware\MiddlewareB($param1, $param2, ...);
},
]);
미들웨어에서 파라미터를 사용하는 예시입니다.
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class MiddlewareA implements MiddlewareInterface
{
protected $param1;
protected $param2;
public function __construct($param1, $param2)
{
$this->param1 = $param1;
$this->param2 = $param2;
}
public function process(Request $request, callable $handler) : Response
{
var_dump($this->param1, $this->param2);
return $handler($request);
}
}
미들웨어 실행 순서
- 미들웨어 실행 순서는
전역 미들웨어
->애플리케이션 미들웨어
->컨트롤러 미들웨어
->라우트 미들웨어
->메서드 미들웨어
입니다. - 동일한 레벨에 여러 미들웨어가 있는 경우, 동일한 레벨의 미들웨어를 실제로 설정한 순서대로 실행합니다.
- 404 요청은 기본적으로 어떤 미들웨어도 트리거하지 않습니다(단, 여전히
Route::fallback(function(){})->middleware()
를 통해 미들웨어를 추가할 수 있습니다).
라우트에서 미들웨어에 파라미터 전달 (route->setParams)
라우트 구성 config/route.php
<?php
use support\Request;
use Webman\Route;
Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' =>'some value']);
미들웨어(가정: 전역 미들웨어)
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Hello implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
// 기본 라우트의 경우 $request->route는 null이므로 $request->route가 비어 있는지 확인해야 합니다.
if ($route = $request->route) {
$value = $route->param('some_key');
var_export($value);
}
return $handler($request);
}
}
미들웨어에서 컨트롤러로 파라미터 전달
때때로 컨트롤러가 미들웨어에서 생성된 데이터를 사용해야 할 때가 있습니다. 이때 $request
객체에 속성을 추가하여 컨트롤러에 파라미터를 전달할 수 있습니다. 예를 들어:
미들웨어
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Hello implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$request->data = 'some value';
return $handler($request);
}
}
컨트롤러:
<?php
namespace app\controller;
use support\Request;
class FooController
{
public function index(Request $request)
{
return response($request->data);
}
}
미들웨어에서 현재 요청 라우트 정보 얻기
$request->route
를 사용하여 라우트 객체를 가져오고, 해당 메서드를 호출하여 필요한 정보를 얻을 수 있습니다.
라우트 구성
<?php
use support\Request;
use Webman\Route;
Route::any('/user/{uid}', [app\controller\UserController::class, 'view']);
미들웨어
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Hello implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$route = $request->route;
// 요청이 어떤 라우트와도 매칭되지 않으면(기본 라우트를 제외하고) $request->route는 null입니다.
// 예를 들어, 브라우저에서 주소 /user/111을 접근하면 아래와 같은 정보가 출력됩니다.
if ($route) {
var_export($route->getPath()); // /user/{uid}
var_export($route->getMethods()); // ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD','OPTIONS']
var_export($route->getName()); // user_view
var_export($route->getMiddleware()); // []
var_export($route->getCallback()); // ['app\\controller\\UserController', 'view']
var_export($route->param()); // ['uid'=>111]
var_export($route->param('uid')); // 111
}
return $handler($request);
}
}
주의
미들웨어에서 예외 얻기
비즈니스 처리 과정 중에 예외가 발생할 수 있으며, 미들웨어에서 $response->exception()
을 사용하여 예외를 얻을 수 있습니다.
라우트 구성
<?php
use support\Request;
use Webman\Route;
Route::any('/user/{uid}', function (Request $request, $uid) {
throw new \Exception('exception test');
});
미들웨어:
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Hello implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$response = $handler($request);
$exception = $response->exception();
if ($exception) {
echo $exception->getMessage();
}
return $response;
}
}
초전역 미들웨어
주 프로젝트의 전역 미들웨어는 주 프로젝트에만 영향을 미치며, 애플리케이션 플러그인에는 영향을 미치지 않습니다. 때로는 모든 플러그인을 포함한 전역 영향을 미치는 미들웨어를 추가하고 싶을 수 있습니다. 이럴 때는 초전역 미들웨어를 사용할 수 있습니다.
config/middleware.php
에 다음과 같이 구성합니다.
return [
'@' => [ // 주 프로젝트 및 모든 플러그인에 전역 미들웨어 추가
app\middleware\MiddlewareGlobl::class,
],
'' => [], // 주 프로젝트에만 전역 미들웨어 추가
];
提示
@
초전역 미들웨어는 주 프로젝트에 설정할 수 있을 뿐만 아니라, 특정 플러그인에서도 구성할 수 있습니다. 예를 들어plugin/ai/config/middleware.php
에 설정한@
초전역 미들웨어는 주 프로젝트 및 모든 플러그인에 영향을 미칩니다.
특정 플러그인에 미들웨어 추가
때때로 특정 애플리케이션 플러그인에 미들웨어를 추가하고 싶지만 플러그인의 코드를 수정하고 싶지 않을 수 있습니다(업그레이드 시 덮어씌워질 수 있음). 이럴 때는 주 프로젝트에서 해당 플러그인에 미들웨어를 추가할 수 있습니다.
config/middleware.php
에 다음과 같이 구성합니다.
return [
'plugin.ai' => [], // ai 플러그인에 미들웨어 추가
'plugin.ai.admin' => [], // ai 플러그인의 admin 모듈(plugin\ai\app\admin 디렉토리)에 미들웨어 추가
];
提示
물론 특정 플러그인에 유사한 구성을 추가하여 다른 플러그인에 영향을 줄 수도 있습니다. 예를 들어plugin/foo/config/middleware.php
에 위와 같은 구성을 추가하면 ai 플러그인에 영향을 미칠 것입니다.