====== Apricot コントローラ ======
--- //[[http://www.y2sunlight.com|y2sunlight]] 2020-07-29//
[[apricot:usage:ja|Apricot ドキュメント に戻る]]
目次
* [[apricot:usage:ja:features|Apricot 特徴と概要]]
* [[apricot:usage:ja:config|Apricot 配置と構成]]
* [[apricot:usage:ja:errors-logging|Apricot ログとエラー処理]]
* [[apricot:usage:ja:http|Apricot リクエストとレスポンス]]
* [[apricot:usage:ja:frontend|Apricot フロントエンド]]
* [[apricot:usage:ja:database|Apricot データベース]]
* [[apricot:usage:ja:model|Apricot モデルとサービス]]
* [[apricot:usage:ja:middleware|Apricot ミドルウェア]]
* Apricot コントローラ
* [[apricot:usage:ja:validation|Apricot バリデーション]]
* [[apricot:usage:ja:provider|Apricot サービスプロバイダー]]
* [[apricot:usage:ja:authentication|Apricot ユーザ認証]]
* [[apricot:usage:ja:utility|Apricot ユーティリティ]]
----
本章では主にApricotのスケルトンに含まれているユーザコントローラを例にとって説明します。ユーザコントローラは、以下に配置されています。
/your-project/app/Controllers/UserController.php
\\
===== ルーティング =====
リクエストルータは、アプリケーションのエンドポイント(URI)がどの コントローラ@アクション を実行するのかを決定します。Apricotでは、リクエストルータに FastRouteを使用しています。ルーティングに関する詳細は [[https://github.com/nikic/FastRoute|FastRoute]] を参照して下さい。
ルーティングの設定は、''/your-project/config/routes.php'' ファイルを編集することで行います。以下はApricotのスケルトンとして提供されている ''routes.php'' です。
{{fa>folder-open-o}} ** /your-project/config **
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)に対するルーティングの例です。
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+'')を指定しています。使用できる正規表現についても [[https://github.com/nikic/FastRoute|FastRouteのREAMME]] を参照して下さい。
\\
===== コントローラとアクション =====
ルーティングによって設定されているエンドポイント(URI)がアクセスされると、Apricotはハンドラー(クロージャでない場合)で指定されている ''コントローラ@アクション'' をインボークします。コントローラは ''App\Foundation\Controller'' を継承する必要があります。そして、コントローラは以下の場所に配置する必要があります( 名前空間は''\App\Controllers'' です )。
/your-project/app/Controllers
以下は、Apricotのスケルトンに含まれているHomeコントローラです。このコントローラにはindexアクションだけが含まれています。
{{fa>folder-open-o}} ** /your-project/app/Controllers **
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');
}
// ...
}
=== Auto Wiring ===
public function __construct(User $user)
コントローラのコンストラクタは、Auto Wiring をサポートします。これは、コンストラクター引数の型ヒントを調べることにより、オブジェクトとそのすべての依存関係を再帰的に自動的に解決する機能です。但し、注入できるのはオブジェクト型の変数だけです。Auto Wiring には外部ライブラリーの [[https://github.com/thephpleague/container|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'' になります。パターン中の名前とパラメータの名前は同じでなければなりません。
\\
===== インターセプター =====
インターセプター とはアクションの前処理の事です。ミドルウェアと同じでリクエストを中断してレスポンスオブジェクトを生成することもできますが、アクションの後処理はできません。これを図示すると以下のようになります。
=== インターセプター構造 ===
{{:apricot:usage:ja:controller:ext-fig02.svg?nolink&800}}
上図から分かるようにミドルウェアパイプラインから見ると、インターセプターはアクションに含まれます。ミドルウェアとの一番の違いは、ミドルウェアは基本的に全てのコントローラを対象としているのに対し、インターセプターは、各コントローラで独自に設定ができるという点です。
インターセプターの主な用途としては以下のものがあります:
* 入力データの検証(バリデーション)
* 不要な入力データの除去
* 入力データの変換
インターセプターを利用することで、簡潔なアクションを作ることができます。
\\
==== インターセプターの登録 ====
インターセプターの登録は、コントローラーのコンストラクタの中でコントローラーの ''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 )の例を示します。
{{fa>folder-open-o}} ** /your-project/app/Controllers/Interceptors **
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()'' メソッドを使ってバリデーションを実行してます。バリデーションについては[[apricot:usage:ja:validation|次章]]を参照して下さい。
バリデーションが失敗した時、withInputs() で入力変数を、withErrors() で[[apricot:usage:ja:validation#validatorerrorbagクラス|バリデーションのエラーバッグ]]をフラッシュ変数に保存します。そして、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'' を生成してそれをスローすべきです。
=== クエリーの例外処理 ===
通常、トランザクションは更新系の処理で設定するするので、クエリーで発生した全ての例外は集約エラーハンドラで処理されエラー画面が表示されます。この状況を避けたい場合は、次の何れかの選択になるでしょう。
* クエリーのアクションにもトランザクションを設定する
* アクション内のcatchブロックでエラーメッセージ付きの入力ページをレンダリングする
\\