====== Apricot インターセプター ====== --- //[[http://www.y2sunlight.com|y2sunlight]] 2020-05-25// [[apricot:top|Apricot に戻る]] 関連記事 * [[apricot:configuration|Apricot プロジェクトの作成]] * [[apricot:public|Apricot 公開フォルダ]] * [[apricot:core:top|Apricot コア]] * [[apricot:app:top|Apricot アプリ]] * Apricot 拡張 * [[apricot:ext:middleware|Apricot ミドルウェア]] * [[apricot:ext:access-log|Apricot アクセスログ]] * [[apricot:ext:csrf|Apricot CSRF対策]] * [[apricot:ext:user-auth|Apricot ユーザ認証]] * [[apricot:ext:basic-auth|Apricot 基本認証]] * [[apricot:ext:session-auth|Apricot セッション認証]] * Apricot インターセプター * [[apricot:ext:di-container|Apricot DIコンテナー]] インターセプター とはアクションの前処理の事です。ミドルウェアと同じでリクエストを中断してレスポンスオブジェクトを生成することもできますが、アクションの後処理はできません。これを図示すると以下のようになります。 === インターセプター構造 === {{:apricot:ext:ext-fig02.svg?nolink&800}} 上図から分かるようにミドルウェアパイプラインから見ると、インターセプターはアクションに含まれます。ミドルウェアとの一番の違いは、ミドルウェアは基本的に全てのコントローラを対象としているのに対し、インターセプターは、各コントローラで独自に設定ができるという点です。 インターセプターの主な用途としては入力データの検証(バリデーション)、入力データのフィルタリングや変換です。インターセプターを作ることで、すっきりしたアクションを作ることができます。 \\ ===== ベースコントローラの変更 ===== インターセプターの仕組みを実装するために、コアのベースコントローラ( [[apricot:core:base-controller#BaseControllerクラス|BaseController]] )を以下のように変更します。 {{fa>folder-open-o}} ** /apricot/core/Foundation ** 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)'' メソッド * リクエストされているアクション(Application::getActionName())が対象となります。 * アクションにインターセプターを追加します。 * BaseControllerは、インターセプターを配列( $this->interceptors )で管理しています。 * ''invokeAction($actionName, $params)'' メソッド * [[apricot:ext:middleware#middlewarepipeline_クラス|ミドルウェアパイプライン]]からはこのメソッドが呼び出されます。 * インターセプターを登録順に実行し最後に実際のアクションを実行します。 * インターセプターがResponseインスタンスを返した場合、処理は中止され、そのインスタンスをアクションのレスポンスとして返します。 インターセプターの使い方は、[[#インターセプターの使用|次項]]をご覧ください。 \\ ===== インターセプターの使用 ===== インターセプターの登録は、コントローラーのコンストラクタで ''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 でないとアクセスできません。 \\ ==== 認証コントローラ ==== 認証コントローラのバリデーションをクロージャ型のインタセプターとして再実装します。[[apricot:ext:session-auth|セッション認証]]の章で作った[[apricot:ext:session-auth#authcontroller_クラス|AuthController クラス]]に以下の変更を行います。 - コンストラクタ( __construct() ) を作ります。 - コンストラクタ内で、login()アクションのインタセプターを登録します。 $this->intercept('login', function(Controller $controller) { // バリデーションのロジック }); - login() メソッド内からバリデーション( validate() ) の呼び出し部分を削除します。 - validate() を削除します。 以下に修正後の最終的な AuthController.php を示します。 {{fa>folder-open-o}} ** /apricot/app/Controllers ** 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")); } } \\ ==== ユーザコントローラ ==== ユーザコントローラのバリデーションをメソッド型のインタセプターとして再実装します。[[apricot:app:user-edit|ユーザ登録画面]]の章で作った[[apricot:app:user-edit#ユーザコントローラ|ユーザコントローラ( UserController )]]に以下の変更を行います。 >ユーザコントローラには、[[apricot:app:validation#ユーザコントローラ|バリデーション]]の章と[[apricot:app:transaction#ユーザコントローラ|トランザクション]]の章で変更を加えています。 - コンストラクタ内で、insert()アクションのインタセプターを登録します。 $this->intercept('insert', 'UserInterceptor@insert'); - 同様に、update()アクションのインタセプターを登録します。 $this->intercept('update', 'UserInterceptor@update'); - insert() と update()メソッド内からバリデーションの呼び出し部分を削除します。 以下に修正後の最終的な UserController.php を示します。 {{fa>folder-open-o}} ** /apricot/app/Controllers ** 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')); } } \\