— y2sunlight 2020-07-29
目次
本章では主にApricotのスケルトンに含まれているユーザコントローラを例にとって説明します。ユーザコントローラは、以下に配置されています。
/your-project/app/Controllers/UserController.php
リクエストルータは、アプリケーションのエンドポイント(URI)がどの コントローラ@アクション を実行するのかを決定します。Apricotでは、リクエストルータに FastRouteを使用しています。ルーティングに関する詳細は FastRoute を参照して下さい。
ルーティングの設定は、/your-project/config/routes.php
ファイルを編集することで行います。以下はApricotのスケルトンとして提供されている routes.php
です。
/your-project/config
<?php /** * This file contains callback for route definitions. */ return function (FastRoute\RouteCollector $r) { /** @var string $base route base path */ $base = Apricot\Application::getInstance()->getRouteBase(); // Creates a route group with a common prefix. $r->addGroup($base, function (FastRoute\RouteCollector $r) use($base) { // An example $r->get('/home', 'HomeController@index'); }); };
$base
には Apricotの Application
クラスから取得したアプリケーションのべースパスが保存されています。このパスは、index.php が存在するURIパスに一致します。以下では、$base
を /your-project-path/
と呼びます。
$base
をルートに持つルーティンググループを RouteCollector
の addGroup()
メソッドを使って定義します。これは共通のプレフィックスを持つルートに対して有用な手段です。そして、アプリケーションのルーティングは、このメソッドの第2引数であるクロージャ―の中にコーディングします。
上の例では、getメソッドに対する特定のルート( /your-project-path/home
) とハンドラー( HomeController@index
)を関連付けています。ここでは、ハンドラーに コントローラ名@アクション名
を表す文字列を指定しています。コントローラはクラスで、アクションはメソッドになります。この文字列の解釈はアプリケーションに依存します。これらについては「コントローラとアクション」を参照して下さい。
ハンドラーには以下の例のように、クロージャ―を使用するできます。この例では、利用者が /your-project-path/
をアクセスすると、それは /home
にリダイレクトされます。
$r->get('[/]', function() use($base){ header("Location: " . $base.'/home'); });
以下は、ユーザ登録ページの CRUD操作(create, read, update, and delete)に対するルーティングの例です。
<?php $r->get ('/users', 'UserController@index'); $r->get ('/user/create', 'UserController@create'); $r->post('/user/insert', 'UserController@insert'); $r->get ('/user/{id:\d+}/edit', 'UserController@edit'); $r->post('/user/{id:\d+}/update', 'UserController@update'); $r->post('/user/{id:\d+}/delete', 'UserController@delete'); };
フォームから送信される場合はpostメソッドが、それ以外はgetメソッドのルートが定義されています。indexアクションはユーザリスト表示、createアクションは新規登録ページ、editアクションは編集ページを表示します。insert、update そして deleteのアクション はそれぞれCUD操作を実行します。
editアクションなどのルートは名前付きのプレースホルダーを {名前}:{パターン}
の形式で指定しています。パターンには正規表現が指定できます。この例では、id
と言う名前で1桁以上の数字(\d+
)を指定しています。使用できる正規表現についても FastRouteのREAMME を参照して下さい。
ルーティングによって設定されているエンドポイント(URI)がアクセスされると、Apricotはハンドラー(クロージャでない場合)で指定されている コントローラ@アクション
をインボークします。コントローラは App\Foundation\Controller
を継承する必要があります。そして、コントローラは以下の場所に配置する必要があります( 名前空間は\App\Controllers
です )。
/your-project/app/Controllers
以下は、Apricotのスケルトンに含まれているHomeコントローラです。このコントローラにはindexアクションだけが含まれています。
/your-project/app/Controllers
<?php namespace App\Controllers; use App\Foundation\Controller; use App\Foundation\Security\AuthUser; /** * Home Controller */ class HomeController extends Controller { /** * Index Page for this controller. * * @return \Apricot\Foundation\Response */ public function index() { $message = __('messages.home.msg_hello', [':account'=>AuthUser::getUser()->account]); return render('home',['message'=>$message]); } }
以下は、ユーザコントローラのコンストラクタです。
/** * User Controller */ class UserController extends Controller { /** * @var \App\Models\User */ private $user; /** * Creates a user controller. */ public function __construct(User $user) { // User Model $this->user = $user; // Registers interceptors. $this->intercept('insert', 'UserInterceptor@insert'); $this->intercept('update', 'UserInterceptor@update'); // Registers transactional actions. $this->transactional('insert','update','delete'); } // ... }
public function __construct(User $user)
コントローラのコンストラクタは、Auto Wiring をサポートします。これは、コンストラクター引数の型ヒントを調べることにより、オブジェクトとそのすべての依存関係を再帰的に自動的に解決する機能です。但し、注入できるのはオブジェクト型の変数だけです。Auto Wiring には外部ライブラリーの League/Container を使用しています。
Auto Wiring で使用する引数には、一般的にモデルクラスやサービスクラスを指定します。上の例では、ユーザモデルをコンストラクター引数に指定し、それをメンバ変数( $this→user
)に格納しています。
$this->intercept('insert', 'UserInterceptor@insert'); $this->intercept('update', 'UserInterceptor@update');
コンストラクタではアクションにインターセプターを登録できます。この例では、insertとupdateアクションにそれぞれインターセプターを登録しています。インターセプターについては次項を参照して下さい。
$this->transactional('insert','update','delete');
コンストラクタではアクションにトランザクションを適用できます。この例では、insert、updateそしてdeteleの各アクションにトランザクションを適用しています。トランザクションについては後続の項を参照して下さい。
以下はユーザコントローラのeditアクションの例です。
public function edit(int $id) { // Finds By the primary key $user = $this->user->findOne($id); if ($user!==false) { return render("user.edit", ["user"=>$user]); } else { return redirect(route("users"))->withOldErrors(); } }
config/routes.php
内のeditアクションのルーティングは次のようになっています。
get('/user/{id:\d+}/edit', 'UserController@edit');
例えば、利用者が /your-project-path/user/1/edit
にアクセスすると UserController の editアクションがインボークされます。この時、edit
のルートパターンである {id:\d+}
は アクションのパラメータ int $id
に引き渡され、結果的に $id
には 1
になります。パターン中の名前とパラメータの名前は同じでなければなりません。
インターセプター とはアクションの前処理の事です。ミドルウェアと同じでリクエストを中断してレスポンスオブジェクトを生成することもできますが、アクションの後処理はできません。これを図示すると以下のようになります。
上図から分かるようにミドルウェアパイプラインから見ると、インターセプターはアクションに含まれます。ミドルウェアとの一番の違いは、ミドルウェアは基本的に全てのコントローラを対象としているのに対し、インターセプターは、各コントローラで独自に設定ができるという点です。
インターセプターの主な用途としては以下のものがあります:
インターセプターを利用することで、簡潔なアクションを作ることができます。
インターセプターの登録は、コントローラーのコンストラクタの中でコントローラーの intercept()
メソッドを使って行います。intercept()
の第1引数はアクション名を文字列で、第2引数にはインターセプターを指定します。インターセプターにはクロージャー または 'クラス名@メソッド名' の形式の文字列を指定できます。簡単なバリデーション処理ならクロジャー型で以下のように書きます。
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'); ... } .... }
インターセプターとして自分自身( $this )のメソッドを指定する場合は、'@メソッド名'
のように指定します。但し、自分自身のメソッドでもインターセプターのアクセス権は public でないといません。
インターセプターは以下の場所に配置する必要があります( 名前空間は \App\Controllers\Interceptors
です )。
/your-project/app/Controllers/Interceptors
インターセプターメソッドのシグネチャには次の規則があります:
/** * Interceptor method * * @param Controller $controller インターセプターを呼び出したコントローラ * @param mixed $... 対応するアクションメソッドと同じ引数 * @return void|\Apricot\Foundation\Response 失敗の場合Responseオブジェクトを返す */ public function interceptorMethod( Controller $controller, [, mixed $... ] ):mixed
以下にユーザコントローラのinsertアクションに対するインターセプター( UserInterceptor@insert )の例を示します。
/your-project/app/Controllers/Interceptors
<?php namespace App\Controllers\Interceptors; use Apricot\Input; use App\Foundation\Controller; use App\Foundation\ValidatorErrorBag; /** * User Interceptor */ class UserInterceptor { /** * Interceptor for insert method. * * @return void|\Apricot\Foundation\Response return Response if failed */ public function insert(Controller $controller) { $inputs = Input::all(); // Validation $v =(new \Valitron\Validator($inputs)); ->rule('required', ['account','password']) ->rule('equals','password','password_confirmation') ->labels(inputLabels('messages.user.create')); if(!$v->validate()) { $errorBag = new ValidatorErrorBag($v->errors()); return redirect(back())->withInputs()->withErrors($errorBag); } // Removes unnecessary inputs Input::remove('password_confirmation'); } }
この例では、Input::all() で入力変数を取得した後に、Validator
のインスタンスを生成し、そのインスタンスの validate()
メソッドを使ってバリデーションを実行してます。バリデーションについては次章を参照して下さい。
バリデーションが失敗した時、withInputs() で入力変数を、withErrors() でバリデーションのエラーバッグをフラッシュ変数に保存します。そして、redirect()で前画面にリダイレクトするレスポンスオブジェクトを生成し、それを返します。Apricotはインターセプターがレスポンスオブジェクトを返した時、コントローラーアクションを呼び出さずに、そのレスポンスをクライアントに返します。
バリデーションが成功した時、不要になった入力変数 'password_confirmation' を Input::remove() で削除して処理を終了します。その後、Apricotはコントローラーアクションを呼び出します。
Apricotでは、コントローラのアクションをラップする形で、アクションにトランザクションを適用することができます。Apricotは、トランザクションの適用されたアクションを呼び出す前にトランザクションを開始し、アクションが正常に終了するとそのトランザクションをコミットします。アクションが ApplicationException
例外をスローした場合、Apricotは、それをキャッチしてエラーログを出力して、アクションの代わりに前画面に戻るリダイレクトレスポンスをクライアントに返します。これらの処理は全てApricotのフレームワーク内で行われるのでアクションのコーディングを最小限にすることができます。
アクションにトランザクションを適用するには、以下のように、コンストラクターの中で、コントローラの transactional()
メソッドを使用します。
class FooController extends Controller { public function __construct(User $user) { // トランザクションの適用 $this->transactional('insert','update','delete'); ... } .... }
この例では、insert、updateそしてdeteleの各アクションにトランザクションを適用しています。このように、transactional()
メソッドの引数にアクション名を並べるだけで、トランザクションをアクションに適用することができます。
以下は、ユーザコントローラの update アクションの例です。
/** * Updates a user record. * * @param int $id * @return \Apricot\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')); } // Redirects to the user edit page. return redirect(route("user/{$id}/edit")); }
この例では、ユーザモデル( $this->user ) の update() メソッドで、ユーザデータを変更しています。updateアクションにはコントローラの transactional() メソッドによってトランザクションが適用されているので、データベースのトランザクションはフレームワークが自動で開始してくれます。
アクションがしなければならないことは、モデルの例外を捕捉して、ApplicationException をスローすることだけです。フレームワークは ApplicationException またはそれを親に持つ例外のみを捕捉します。それ以外の例外は、集約エラーハンドラーによって補足されシステムエラーになります。即ち、フレームワークは、ApplicationException のみ前画面に戻すリダイレクトレスポンスを生成します。
モデルは次の場合に ApplicationException をスローします:
OptimissticLockException
)
アクションでは、これ以外の例外が発生した場合は、パースエラーなどの Error
を除き、ApplicationException
を生成してそれをスローすべきです。
通常、トランザクションは更新系の処理で設定するするので、クエリーで発生した全ての例外は集約エラーハンドラで処理されエラー画面が表示されます。この状況を避けたい場合は、次の何れかの選択になるでしょう。