目次

Apricot インターセプター

y2sunlight 2020-05-25

Apricot に戻る

関連記事

インターセプター とはアクションの前処理の事です。ミドルウェアと同じでリクエストを中断してレスポンスオブジェクトを生成することもできますが、アクションの後処理はできません。これを図示すると以下のようになります。

インターセプター構造

上図から分かるようにミドルウェアパイプラインから見ると、インターセプターはアクションに含まれます。ミドルウェアとの一番の違いは、ミドルウェアは基本的に全てのコントローラを対象としているのに対し、インターセプターは、各コントローラで独自に設定ができるという点です。

インターセプターの主な用途としては入力データの検証(バリデーション)、入力データのフィルタリングや変換です。インターセプターを作ることで、すっきりしたアクションを作ることができます。


ベースコントローラの変更

インターセプターの仕組みを実装するために、コアのベースコントローラ( BaseController )を以下のように変更します。

/apricot/core/Foundation

BaseController.php
<?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() メソッドを使って行います。インターセプターにはクロージャー型とメソッド型の両方が使用できます。簡単なバリデーション処理ならクロジャー型で以下のように書きます。

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 クラスに以下の変更を行います。

  1. コンストラクタ( __construct() ) を作ります。
  2. コンストラクタ内で、login()アクションのインタセプターを登録します。
    $this->intercept('login', function(Controller $controller)
    {
      // バリデーションのロジック
    });
  3. login() メソッド内からバリデーション( validate() ) の呼び出し部分を削除します。
  4. validate() を削除します。

以下に修正後の最終的な AuthController.php を示します。

/apricot/app/Controllers

AuthController.php
<?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 )に以下の変更を行います。

ユーザコントローラには、バリデーションの章とトランザクションの章で変更を加えています。
  1. コンストラクタ内で、insert()アクションのインタセプターを登録します。
    $this->intercept('insert', 'UserInterceptor@insert');
  2. 同様に、update()アクションのインタセプターを登録します。
    $this->intercept('update', 'UserInterceptor@update');
  3. insert() と update()メソッド内からバリデーションの呼び出し部分を削除します。

以下に修正後の最終的な UserController.php を示します。

/apricot/app/Controllers

UserController.php
<?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'));
    }
}