— y2sunlight 2020-05-25
関連記事
インターセプター とはアクションの前処理の事です。ミドルウェアと同じでリクエストを中断してレスポンスオブジェクトを生成することもできますが、アクションの後処理はできません。これを図示すると以下のようになります。
上図から分かるようにミドルウェアパイプラインから見ると、インターセプターはアクションに含まれます。ミドルウェアとの一番の違いは、ミドルウェアは基本的に全てのコントローラを対象としているのに対し、インターセプターは、各コントローラで独自に設定ができるという点です。
インターセプターの主な用途としては入力データの検証(バリデーション)、入力データのフィルタリングや変換です。インターセプターを作ることで、すっきりしたアクションを作ることができます。
インターセプターの仕組みを実装するために、コアのベースコントローラ( BaseController )を以下のように変更します。
/apricot/core/Foundation
<?php namespace Core\Foundation; /** * Request Controller Class (Controller Base) */ class BaseController { /** * The interceptors registered on the controller. * @var array */ protected $interceptors = []; /** * Register interceptors on the controller. * * @param string $actionName * @param array|mixed $interceptors array or arguments list */ protected function intercept($actionName, $interceptors) { if ($actionName == \Core\Application::getInstance()->getActionName()) { $interceptor_arr = is_array($interceptors) ? $interceptors : array_slice(func_get_args(),1); $this->interceptors = array_merge($this->interceptors , $interceptor_arr); } } /** * Call real Action * @param string $actionName * @param array $params * @return \Core\Foundation\Response */ protected function callAction($actionName, $params) { return call_user_func_array(array($this, $actionName), $params); } /** * Invoke Action * @param string $actionName * @param array $params * @return \Core\Foundation\Response */ public function invokeAction($actionName, $params) { // Interceptor parameters $iparams = array_merge(array('_controller'=>$this), $params); // Invoke Interceptor $response = null; foreach($this->interceptors as $interceptor) { if (is_callable($interceptor)) { // Case of callable $response = call_user_func_array($interceptor, $iparams); } elseif(strpos($interceptor,'@')!==false) { // Case of Controller/Action list($class, $method) = explode('@', $interceptor); if (empty($class)) { $instance = $this; } else { $class = "\\App\\Controllers\\Interceptors\\{$class}"; $instance = new $class; } // Call interceptor $response = call_user_func_array(array($instance, $method), $iparams); } if ($response instanceof \Core\Foundation\Response) { return $response; } } // Call Action return $this->callAction($actionName, $params); } }
callAction()
メソッドに変更はありません。intercept()
メソッドを追加し、invokeAction()
メソッドを変更します。
intercept($actionName, $interceptors)
メソッド
invokeAction($actionName, $params)
メソッド
インターセプターの使い方は、次項をご覧ください。
インターセプターの登録は、コントローラーのコンストラクタで intercept()
メソッドを使って行います。インターセプターにはクロージャー型とメソッド型の両方が使用できます。簡単なバリデーション処理ならクロジャー型で以下のように書きます。
class FooController extends Controller { public function __construct() { // インターセプターの登録 $this->intercept('action', function(Controller $controller, int $id) { $inputs = Input::all(); ... }); } ... }
インターセプターに渡される引数は、第1引数に、コントローラのインスタンスが、その後にアクションと同じの引数が続きます。また、インターセプターはレスポンスオブジェクトを返して以降のアクションを中止することができます。
インターセプターにメソッド型を使用する場合は、以下のように 'クラス名@メソッド名' の形式で指定します。
class FooController extends Controller { public function __construct(User $user) { // インターセプター登録 $this->intercept('action1', 'FooInterceptor@action1'); $this->intercept('action2', 'FooInterceptor@action2'); ... } .... }
Apricotでは、インターセプターを配置する場所は以下に決めれられいます。
aprocot/app/Controllers/Interceptors
結果として、インターセプタークラスの名前空間は \\App\\Controllers\\Interceptors
になります。
自分自身( $this )のメソッドを指定する場合は、'@メソッド名'
のように指定します。但し、自分自身のメソッドでも public でないとアクセスできません。
認証コントローラのバリデーションをクロージャ型のインタセプターとして再実装します。セッション認証の章で作ったAuthController クラスに以下の変更を行います。
$this->intercept('login', function(Controller $controller) { // バリデーションのロジック });
以下に修正後の最終的な AuthController.php を示します。
/apricot/app/Controllers
<?php namespace App\Controllers; use Core\Input; use Core\Foundation\ErrorBag; use App\Foundation\Security\AuthUser; use App\Foundation\Controller; use App\Foundation\ValidatorErrorBag; /** * Authコントローラ */ class AuthController extends Controller { public function __construct() { // インターセプターの登録 $this->intercept('login', function(Controller $controller) { $inputs = Input::all(); // Validation $v =(new \Valitron\Validator($inputs)) ->rule('required', 'account') ->rule('alphaNum','account') ->rule('ascii','password') ->labels(inputLabels('auth.login')); if(!$v->validate()) { $errorBag = new ValidatorErrorBag($v->errors()); return redirect(back())->withInputs()->withErrors($errorBag); } }); } /** * ログインフォーム表示 * @return \Core\Foundation\Response */ public function showForm() { if (AuthUser::check()) { // 認証済ならトップ画面表示 return redirect(route('')); } if (AuthUser::remember()) { // 自動認証できたらトップ画面表示 return redirect(route('')); } return render('login'); } /** * ログイン(ユーザ認証) * @return \Core\Foundation\Response */ public function login() { $inputs = Input::all(); if (!AuthUser::authenticate($inputs['account'], $inputs['password'], !empty($inputs['remember']))) { // ユーザが見つからない $errorBag = new ErrorBag([__('auth.login.error.no_account')]); return redirect(back())->withInputs()->withErrors($errorBag); } // ログイン成功 return redirect(AuthUser::getPathAfterLogin()); } /** * ログアウト * @return \Core\Foundation\Response */ public function logout() { // セッションの破棄 AuthUser::forget(); // ログイン画面表示 return redirect(route("login")); } }
ユーザコントローラのバリデーションをメソッド型のインタセプターとして再実装します。ユーザ登録画面の章で作ったユーザコントローラ( UserController )に以下の変更を行います。
$this->intercept('insert', 'UserInterceptor@insert');
$this->intercept('update', 'UserInterceptor@update');
以下に修正後の最終的な UserController.php を示します。
/apricot/app/Controllers
<?php namespace App\Controllers; use App\Exceptions\ApplicationException; use App\Foundation\Controller; use App\Models\User; use Core\Input; /** * ユーザコントローラ */ class UserController extends Controller { /** * User * @var \App\Models\User */ private $user; /** * ユーザコントローラの生成 */ public function __construct() { // モデル $this->user = new User(); // インターセプター登録 $this->intercept('insert', 'UserInterceptor@insert'); $this->intercept('update', 'UserInterceptor@update'); // トランザクションアクション登録 $this->transactional('insert','update','delete'); } /** * ユーザ一覧 * @return \Core\Foundation\Response */ public function index() { // 全件検索 $users = $this->user->findAll(); return render("user.index", ["users"=>$users]); } /** * ユーザ新規登録 * @return \Core\Foundation\Response */ public function create() { // 新規作成 $user = $this->user->create(); return render("user.create", ["user"=>$user]); } /** * ユーザレコード挿入 * @return \Core\Foundation\Response */ public function insert() { $inputs = Input::all(); try { // ユーザレコード挿入 $user = $this->user->insert($inputs); } catch(\Exception $e) { throw new ApplicationException(__('messages.error.db.insert'),$e->getMessage(),0,$e); } // ユーザ一編集画面にリダイレクト return redirect(route("user/{$user->id}/edit"))->with('msg',__('messages.success.db.insert')); } /** * ユーザ編集 * @return \Core\Foundation\Response */ public function edit(int $id) { // 主キー検索 $user = $this->user->findOne($id); if ($user!==false) { return render("user.edit", ["user"=>$user]); } else { return redirect(route("users"))->withOldErrors(); } } /** * ユーザレコード更新 * @param int $id * @return \Core\Foundation\Response */ public function update(int $id) { $inputs = Input::all(); try { // レコード更新 $this->user->update($id, $inputs); } catch(ApplicationException $e) { throw $e; } catch(\Exception $e) { throw new ApplicationException(__('messages.error.db.update'),$e->getMessage(),0,$e); } // ユーザ一編集画面にリダイレクト return redirect(route("user/{$id}/edit"))->with('msg',__('messages.success.db.update')); } /** * ユーザレコード削除 * @param int $id * @return \Core\Foundation\Response */ public function delete(int $id) { try { // レコード削除 $this->user->delete($id); } catch(ApplicationException $e) { throw $e; } catch(\Exception $e) { throw new ApplicationException(__('messages.error.db.delete'),$e->getMessage(),0,$e); } // ユーザ一覧画面にリダイレクト return redirect(route("users"))->with('msg',__('messages.success.db.delete')); } }