PSR-7(HTTP Message Interface) や PSR-15(HTTP Middlewares) を用いた軽量で薄いオレオレフレームワークです。
以下のようなライブラリを利用して構築しています。
- zendframework/zend-diactoros
- PSR-7 の実装
- zendframework/zend-stratigility
- PSR-15 の実装
- nikic/fast-route
- ルーター
- php-di/php-di
- DI コンテナ
このフレームワークでは、いわゆるコントローラークラスで継承を使用していません。アプリケーションで実装するコントローラーの継承元となる AbstractController
のようなクラスは存在しません。
コントローラーのインスタンスは DI コンテナで管理されているため、コントローラーが必要とするオブジェクトはすべて DI でインジェクションします。
また、コントローラーの前後で共通のフックポイントが必要な場合は(AbstractController
クラスの preDispatch
や postDispatch
で実現していたもの)、PSR-15 ミドルウェアをディスパッチの前段に置くことで実現できます。
一般的なフレームワークのコントローラーは多くのオブジェクトに依存している重量級であるのに対して、このフレームワークのコントローラーは超軽量です。そのため、コントローラーのユニットテストも容易に可能です。
下記にサンプルアプリがあります。
次のようにサンプルアプリが実行できます。
git clone https://github.com/ngyuki/php-ritz-app.git
cd php-ritz-app
composer install
php -S 0.0.0.0:8888 -t public/
open http://localhost:8888/
サンプルアプリは次のディレクトリ構造になっています。
- app/
- オートロードされる PHP クラスを格納するディレクトリ
- bootstrap/
- DI コンテナの初期化のためのコードを格納するディレクトリ
- 環境依存しない初期化の設定を格納します
- cache/
- キャッシュファイルを書き込むディレクトリ
- config/
- DI コンテナの初期化のためのコードを格納
- 環境依存のアプリの設定ファイルを格納します
- public/
- サーバのドキュメントルートに設定するディレクトリ
- resource/
- ビューのテンプレートファイルなどを格納するディレクトリ
- tests/
- サンプルアプリのテストコード
サンプルアプリのネームスペースは次の構造になっています。
- Ritz\App\Bootstrap
- アプリケーションクラスやコンテナファクトリなどのアプリの初期化で使用されるクラス
- Ritz\App\Component
- Controller や Service で使用されるその他のコンポーネントクラスが格納されます
- Ritz\App\Controller
- アプリのコントローラークラスが格納されます
- Ritz\App\Middleware
- アプリで独自に使用する PSR-15 Middleware が格納されます
- Ritz\App\Service
- アプリのユースケースを実装するサービスクラスを格納します
### 処理の流れ
サンプルアプリは次の流れで処理されます。
- `ContainerFactory` がコンテナを作成する
- `Configure` クラスが `bootstrap/` と `config/` から DI コンテナの初期化のコードを読む
- `config/` からは環境変数 `APP_ENV` に基づいたファイルも読まれる
- ルート定義も `bootstrap/` に含まれている
- コンテナから `Application` クラスのオブジェクトを取り出す
- `Application` オブジェクトを `Server::run` クラスに渡してアプリケーションを実行する
- `Application` が PSR-15 Middleware として実行される
- 実行時にパイプラインを構築する
- パイプラインが順番に実行される
サンプルアプリの幾つかのコードには詳細なコメントが記述されているため、アプリケーション構成の参考にしてください。
## Application クラス
`Application` クラスは PSR-15 の `MiddlewareInterface` を実装し、`process` メソッドでパイプラインを初期化して実行する必要があります。
下記の 3 つのミドルウェアはフレームワークが提供する基本的なミドルウェアで、基本的には必須です(もちろん独自のミドルウェアに差し替えることも出来ます)。
- `RenderMiddleware`
- `RouteMiddleware`
- `DispatchMiddleware`
必要に応じて、アプリケーション独自のミドルウェアをパイプラインの任意の場所に差し込むことができます。例えば、次のようなアプリケーション独自パイプラインが考えられます。
- 例外発生時に特定のテンプレート選択する
- CSRF トークンのチェックを行う
- 認証のチェックとリダイレクトを行なう
- アクションの前後でリクエスト・レスポンスオブジェクトを加工する
## ルート定義
ルート定義は下記のようになります。ルーターの実装には `FastRoute` を使用しているため、具体的なルート定義の方法は `FastRoute` のドキュメントを参照してください。
```php
use function DI\value;
use FastRoute\RouteCollector;
use App\Controller\HomeController;
use App\Controller\UserController;
return [
'app.routes' => value(function(RouteCollector $r) {
$r->get('/', [HomeController::class, 'indexAction']);
$r->get('/user/{id}', [UserController::class, 'showAction']);
}),
];
ルートのハンドラ(get
メソッドの第2引数)にはコントローラー名とアクション名からなる配列を指定します。コントローラー名は DI コンテナからコントローラーインスタンスを取り出す際のエントリ名です(典型的にはコントローラーのクラス名)。アクション名はコントローラーのメソッド名です。
ルートのハンドラに文字列のインデックスが含まれる場合、リクエストの属性にそのままセットされます。
// routes.php
return [
'app.routes' => value(function(RouteCollector $r) {
$r->get('/', [HomeController::class, 'indexAction', 'attr' => 'val']);
}),
];
// HomeController.php
class HomeController
{
public function attrAction(ServerRequestInterface $request)
{
$attr = $request->getAttribute('attr'); // val
}
}
コントローラーのアクションメソッドでは、引数の名前やタイプヒントに基いて下記から自動的に値やオブジェクトが注入されます。
- DI コンテナ
- リクエストのアトリビュート
- PSR-15 の
process
メソッド引数(ServerRequestInterface
とDelegateInterface
)
public function showAction(
// DI コンテナから取得
UserRepository $userRepository,
// リクエストオブジェクト
ServerRequestInterface $request,
// リクエストのアトリビュートに設定されたルートパラメータ
$id
) {
return ['user' => $userRepository->get($id)];
}
リクエストのアトリビュートには、いわゆるルートパラメータ、即ち、ルート定義の /user/{id}
のようなプレースホルダのパラメータを含みます。↑の例では、URL が /user/123
であれば、ルートパラメータを元に $id
には 123 が設定されます。
他のミドルウェアでリクエストのアトリビュートへ任意の値が追加されていれば、それらもメソッドの引数に注入できます。
// ミドルウェアでリクエストに属性を追加
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
$requset = $requset->withAttribute('foo', 987);
$requset = $requset->withAttribute(Bar::class, new Bar());
return $delegate->process($request);
}
// アクションメソッドに注入される
public function showAction($foo, Bar $bar)) {
// ...
}
コントローラーのアクションメソッドの戻り値で ViewModel
を返すと RendererMiddleware
にとってテンプレートのレンダリングが行われ、その結果がレスポンスボディに反映されます。
// ViewModel オブジェクトを返す
return new ViewModel();
// テンプレートにアサインする変数を指定する
return new ViewModel(['val' => 123]);
戻り値が配列の場合は自動的にその配列がアサインされた ViewModel
に変換されます
// 配列は自動的に ViewModel オブジェクトに変換される
return ['val' => 123];
レンダリングされるテンプレート名は、コントローラーのクラス名とアクションメソッド名から自動的に導出されます。例えば App\Controller\HomeController::indexAction
-> App/Home/index
のようにテンプレート名が導出されます。導出の規則は後述します。
ViewModel
でテンプレート名を指定すれば別のテンプレートを使うことができます。
return (new ViewModel())->withTemplate('App/Home/index');
自動的に導出されたテンプレート名に対して相対でテンプレートを指定することができます。例えば自動的に導出されたテンプレート名が App/Home/index
の場合、下記のコードではテンプレート名が App/Home/edit
となります。
return (new ViewModel())->withTemplate('./edit');
ViewModel
は PSR-15 の ResponseInterface
を実装しています。ステータスコードやレスポンスヘッダを加工したいときは ResponseInterface
のメソッドが使用できます。
// 200 以外のステータスコードとカスタムヘッダを指定する
return (new ViewModel())->withStatus(400)->withHeader('X-Custom', 'xxx');
アクションメソッドが ViewModel
ではない ResponseInterface
を実装したオブジェクトを返すと、そのレスポンスオブジェクトがそのままブラウザに応答されます。
// リダイレクト
return new RedirectResponse('/');
アクションメソッドが文字列を返すと自動的に TextResponse
に変換されます。
// 文字列は TextResponse に変換される
return "hello";
// 上と等価
return new TextResponse("hello");
ViewModel
でテンプレート名が指定されていない場合、RenderMiddleware
のコンストラクタに渡された TemplateResolver
がコントローラーのクラス名とアクションメソッド名を元にテンプレート名が導出されます。
TemplateResolver
はコンストラクタで、コントローラーの名前空間とテンプレートのプレフィックスのマッピングの配列を受け取ります。例えば、次のように TemplateResolver
のインスタンスを設定します。
return [
'app.view.autoload' => [
'Ritz\\App\\Controller\\' => 'App/',
],
TemplateResolver::class => function (ContainerInterface $container) {
return new TemplateResolver($container->get('app.view.autoload'));
},
];
app.view.autoload
は Composer の PSR-4 のオートローダーの設定に似ています。上の設定では Ritz\\App\\Controller\\
という名前空間のプレフィックスは App/
というテンプレート名に置換されます。
さらに、クラス名とメソッド名に次の加工が施されて、テンプレート名が導出されます。
- コントローラーのクラス名のサフィックスが
Controller
なら除去 - アクションメソッドのサフィックスが
Action
なら除去 - 名前空間区切りはディレクトリ区切りに置き換わる
- コントローラー名とアクションメソッド名はディレクトリ区切りで結合される
例えば Ritz\App\Controller\HomeController::indexAction
というクラス名・メソッド名であれば、テンプレートは App/Home/index
となります。