# ToolBox フレームワーク 利用ガイド

サンプルショッピングサイト(`web/shop/`)を題材に、フレームワークの使い方を説明します。

---

## 目次

1. [設計思想 —— 5層の責務](#1-設計思想--5層の責務)
2. [ディレクトリ構成](#2-ディレクトリ構成)
3. [リクエストの流れ](#3-リクエストの流れ)
4. [Application クラス](#4-application-クラス)
5. [ルーティング](#5-ルーティング)
6. [Controller クラス](#6-controller-クラス)
7. [Model クラス](#7-model-クラス)
8. [Repository クラス](#8-repository-クラス)
9. [View クラス](#9-view-クラス)
10. [Request / Session / Response](#10-request--session--response)
11. [CSRF対策と PRG パターン](#11-csrf対策と-prg-パターン)
12. [ログイン認証](#12-ログイン認証)
13. [エラー処理](#13-エラー処理)
14. [新しいアプリケーションを追加する手順](#14-新しいアプリケーションを追加する手順)
15. [新しい画面を追加する手順](#15-新しい画面を追加する手順)
16. [公開設定(.htaccess / バーチャルホスト)](#16-公開設定htaccess--バーチャルホスト)

---

## 1. 設計思想 —— 5層の責務

| 層 | クラス | 責務 |
|---|---|---|
| Application | `ShopApplication` など | ルーティング定義、DB接続設定、Viewディレクトリの決定などアプリケーション全体の初期設定 |
| Controller | `ProductController` など | **旗振り役**。リクエストを受け取り、Model へ指示を出す。HTMLは組み立てない |
| Model | `ProductModel` など | View へ渡すデータの生成と **エスケープ**、View の呼び出し、Repository の呼び出し |
| Repository | `ProductRepository` など | SQL の実行と、その結果を Model へ返す処理 |
| View | `View`(core) | Model の要求に応じてテンプレートエンジンへ処理を渡し、最終的な画面描画を行う |

**重要な原則**

- **描画に必要なエスケープは Model が担う。** ただし `htmlspecialchars()` を手書きするのではなく、`ViewData` の格納APIを使い分けることで行う(詳細は [02_template_engine_guide.md](02_template_engine_guide.md))
- **Controller は HTML を組み立てない。** `$model->renderIndex()` の戻り値をそのまま `return` する
- **表示用の整形(金額のカンマ区切り・日付書式・URLの組み立て・選択状態の判定)はすべて Model で行う。** テンプレートには判定と出力だけを残す
- **Repository は SQL の実行に徹する。** 業務判断は Model 側に置く

---

## 2. ディレクトリ構成

```
ToolBox/
    .htaccess                  プロジェクトルート全体を非公開にする(Require all denied)
    .gitignore                 生成物・環境固有ファイルの除外設定
    bootstrap.php              オートローダの登録
    cache/
        template/              テンプレートのコンパイル結果(自動生成)
    classes/
        applications/          Application クラス(web/ 配下と同じ構造で分類する)
            form/
                FormApplication.php
            shop/
                ShopApplication.php
                admin/
                    AdminApplication.php
        controllers/           Controller クラス(コンテンツごとに分類する)
            form/
                FormController.php
            shop/
                ProductController.php
                CartController.php
                OrderController.php
                admin/
                    DashboardController.php
                    AuthController.php
                    AdminProductController.php
                    AdminShippingController.php
                    AdminOrderController.php
        core/                  フレームワーク本体
            Application.php    抽象クラス。ルーティングと初期設定
            Router.php         ルーティング解決
            Controller.php     Controller の基底クラス
            Model.php          Model の抽象クラス
            View.php           テンプレートエンジンのファサード
            DbManager.php      PDO接続とリポジトリの管理
            DbRepository.php   Repository の抽象クラス
            Request.php        リクエスト情報
            Session.php        セッション操作
            Response.php       レスポンス生成
            ClassLoader.php    オートローダ
            HttpNotFoundException.php
            UnauthorizedActionException.php
        models/                Model クラス(コンテンツごとに分類する)
            form/
                FormModel.php
            shop/
                ShopModel.php          ショッピングサイト共通の基底Model
                ProductModel.php
                CartModel.php
                OrderModel.php
                admin/
                    AdminModel.php     管理画面共通の基底Model
                    DashboardModel.php
                    AuthModel.php
                    AdminProductModel.php
                    AdminShippingModel.php
                    AdminOrderModel.php
        repositories/          Repository クラス(コンテンツごとに分類する)
            form/
                ContactRepository.php
            shop/
                ProductRepository.php      管理画面からも使う(親コンテンツへフォールバック)
                CategoryRepository.php
                PrefectureRepository.php
                OrderRepository.php
                OrderDetailRepository.php
                CarrierRepository.php
                ShippingFeeRepository.php
                ShippingSettingRepository.php
                admin/
                    AdminUserRepository.php  管理画面専用
        vendors/
            Framework/
                Template/      テンプレートエンジン本体(名前空間 Framework\Template)
    config/
        database.sample.php    DB接続設定の雛形(これをコピーして使う)
        database.local.php     DB接続設定の実値(.gitignore で除外。各自の環境で作成する)
    db/
        schema.sql             テーブル定義
        seed.sql               初期データ
    docs/
        README.md              ドキュメントの索引とコーディング規約
        01_framework_guide.md  本書
        02_template_engine_guide.md
        03_html_template_guide.md
        04_template_syntax_showcase.md
        vhost.sample.conf      バーチャルホスト設定の雛形
    logs/                      Logger の出力先(中身は .gitignore で除外)
    templates/                 テンプレートルート(公開ディレクトリの外)
        view/
            form/              web/form/ に対応
            shop/              web/shop/ に対応
                admin/         web/shop/admin/ に対応
    web/                       公開ディレクトリ
        .htaccess              公開許可(Require all granted)
        index.html             サンプル一覧
        form/                  サンプルフォーム
            .htaccess          リライト設定(実在しないパスを index.php へ)
            index.php          フロントコントローラ(本番用)
            index_dev.php      フロントコントローラ(開発用。.htaccess で配信禁止)
            commons/           このアプリ専用の CSS・JS・画像
                css/style.css
        shop/                  ショッピングサイト
            .htaccess
            index.php
            index_dev.php
            commons/
                css/style.css
            admin/             管理画面
                .htaccess
                index.php
                index_dev.php
                commons/
                    css/style.css
```

**`classes/` `templates/` と `web/` の対応**

`classes/` 配下の4種類(`applications` / `controllers` / `models` / `repositories`)と
`templates/view/` は、いずれも `web/` 配下のディレクトリ構造をそのまま踏襲しています。
フロントコントローラの場所から、対応するクラスとテンプレートを辿れるようにするためです。

| フロントコントローラ | コンテンツ | クラスの置き場所 | Viewディレクトリ |
|---|---|---|---|
| `web/form/index.php` | `form` | `classes/<種別>/form/` | `templates/view/form/` |
| `web/shop/index.php` | `shop` | `classes/<種別>/shop/` | `templates/view/shop/` |
| `web/shop/admin/index.php` | `shop/admin` | `classes/<種別>/shop/admin/` | `templates/view/shop/admin/` |

コンテンツ名は Application クラスの `getContentDirectory()` で決まります。
この1つの値から Controller / Model / Repository / View の置き場所がすべて決まります。

**クラスの探索順**

Controller / Model / Repository は、深いコンテンツから順に探索し、
見つからなければ親コンテンツ、最後に共通ディレクトリへフォールバックします。
`getContentDirectory()` が `'shop/admin'` の場合は次の順です。

| 優先度 | ディレクトリ | 用途 |
|---|---|---|
| 1 | `classes/<種別>/shop/admin/` | 管理画面専用 |
| 2 | `classes/<種別>/shop/` | ショッピングサイト共通(管理画面からも使える) |
| 3 | `classes/<種別>/` | 全アプリ共通 |

この仕組みにより、**コンテンツが違えば同じクラス名を使えます**。
`classes/controllers/form/ProductController.php` と
`classes/controllers/shop/ProductController.php` は共存でき、
実行中のアプリケーションのものだけが読み込まれます。

> 逆に、共通ディレクトリへ置いたクラスは全アプリから見えます。
> アプリを横断して使うものだけを共通ディレクトリへ置いてください。

**オートローダの登録**

`classes/applications/` はサブディレクトリまでオートロード対象にしているため、
アプリを増やしてもオートローダの登録を追加する必要はありません。

```php
//アプリケーションクラスは web/ 配下と同じ構造で分類しているためサブディレクトリまで探索する
$loader->registerDir(dirname(__FILE__).'/classes/applications', true);
```

`models/` と `repositories/` は共通ディレクトリだけを登録します(再帰探索しません)。
コンテンツ配下は `Application::registerContentDirs()` が、
**実行中のアプリケーションのものだけ** を優先度の高い探索対象として追加します。

> `models/` `repositories/` も `registerDir()` に `true` を指定すれば再帰探索できますが、
> それでは他コンテンツの同名クラスまで探索対象に入り、
> 先に見つかった方(`form/` と `shop/` なら `form/`)が使われてしまいます。
> そのため再帰探索は使わず、アプリケーション側から明示的に追加しています。

`classes/controllers/` はオートロード対象にしていません。
`Application::findController()` が同じ探索順で直接 `require_once` します。

---

## 3. リクエストの流れ

```
ブラウザ
  ↓  http://.../web/shop/detail/3
web/shop/.htaccess          実在しないパスを index.php へリライト
  ↓
web/shop/index.php          フロントコントローラ
  ↓  require bootstrap.php  →  new ShopApplication(false)  →  run()
Application::run()
  ├─ Request::getPathInfo()      "/detail/3" を取り出す
  ├─ Router::resolve()           ルーティング定義と突き合わせる
  │                              → array('controller'=>'product', 'action'=>'detail', 'id'=>'3')
  ├─ Application::runAction()    "product" → ProductController を生成
  │   └─ Controller::run()       "detail" → detailAction($params) を実行
  │       ├─ Controller::model('ProductModel')
  │       └─ ProductModel::renderDetail()
  │            ├─ Model::repository('Product') → ProductRepository::fetchPublishedById()
  │            ├─ ViewData へ整形済みの値を格納(エスケープはここで完了)
  │            └─ Model::render('product/detail', $data)
  │                 └─ View::render()
  │                      ├─ TemplateEngine::render('view/shop/product/detail')
  │                      └─ TemplateEngine::render('view/shop/layout')  ← {_contents} へ差し込み
  ├─ Response::setContent()
  └─ Response::send()
```

---

## 4. Application クラス

`classes/core/Application.php` を継承し、アプリケーションごとに1クラス作成します。

### 4.1 実装が必須のメソッド

| メソッド | 内容 |
|---|---|
| `getRootDir()` | プロジェクトルートの絶対パス。クラスの設置階層に応じて `..` の数を合わせる |
| `registerRoutes()` | ルーティング定義の配列を返す |

### 4.2 必要に応じて上書きするメソッド・プロパティ

| メソッド / プロパティ | 既定値 | 内容 |
|---|---|---|
| `getContentDirectory()` | `''` | コンテンツ名(`'shop'` / `'shop/admin'` など)。Controller / Model / Repository / View の置き場所がこの値で決まる |
| `getViewDirectory()` | `'view/'.getContentDirectory()` | テンプレートルートからの相対パス。レイアウト・共通部品・エラーページもここから解決される。テンプレートだけ別階層に置きたい場合のみ上書きする |
| `configure()` | 空 | DB接続などアプリ固有の初期設定 |
| `$loginAction` | `array()` | 未認証時の遷移先 `array(コントローラ名, アクション名)` |

### 4.3 実装例(`classes/applications/shop/ShopApplication.php`)

```php
<?php

/**
 * サンプルショッピングサイト用のアプリケーションクラス
 *
 * フロントコントローラは web/shop/ に配置する。
 * ルーティング定義はフロントコントローラの設置ディレクトリを起点とした相対パスで記述するため、
 * 「web/shop/ をサブディレクトリとして公開する場合」と
 * 「バーチャルホストで web/shop/ をドキュメントルートにする場合」の
 * どちらでも同じ定義がそのまま使える。
 *
 * @author S.Morikane
 */
class ShopApplication extends Application {
	//認証が必要なアクションへ未認証でアクセスされた場合の遷移先(Application::run()で使用される)
	protected $loginAction = array('product', 'index');

	/**
	 * プロジェクトのルートディレクトリを取得する関数
	 * @author S.Morikane
	 * @return string
	 */
	public function getRootDir() {
		//classes/applications/shop/ から3階層上がプロジェクトルートになる
		return dirname(__FILE__).'/../../..';
	}

	/**
	 * このアプリケーションのコンテンツディレクトリを取得する関数
	 *
	 * web/shop/ に対応するため 'shop' を返す。
	 * Controller / Model / Repository / View の置き場所がこの値で決まる。
	 *   classes/controllers/shop/  classes/models/shop/  classes/repositories/shop/
	 *   templates/view/shop/
	 *
	 * @author S.Morikane
	 * @return string
	 */
	public function getContentDirectory() {
		return 'shop';
	}

	/**
	 * ルーティングを定義する関数
	 * @author S.Morikane
	 * @return array
	 */
	protected function registerRoutes() {
		return array(
			//商品一覧(GETで描画。?page=N でページ送り)
			'/'=>array('controller'=>'product', 'action'=>'index'),
			//商品詳細(GETで描画)
			'/detail/:id'=>array('controller'=>'product', 'action'=>'detail'),
		);
	}

	/**
	 * アプリケーション固有の設定を行う関数
	 * @author S.Morikane
	 * @return void
	 */
	protected function configure() {
		//DB接続情報を読み込む(実値は config/database.local.php に置く)
		$config = $this->loadConfig('database');

		//DB接続情報を設定する(スキーマと初期データは db/schema.sql / db/seed.sql を参照)
		$this->dbManager->connect('master', $config['master']);
	}
}
```

### 4.4 Application が提供するゲッター

Controller / Model はコンストラクタで Application を受け取り、以下から依存を取得します。

| メソッド | 戻り値 |
|---|---|
| `getRequest()` | `Request` |
| `getResponse()` | `Response` |
| `getSession()` | `Session` |
| `getDbManager()` | `DbManager` |
| `isDebugMode()` | `bool` |
| `getRootDir()` | プロジェクトルート |
| `getControllerDir()` | `<root>/classes/controllers`(探索の基準ディレクトリ) |
| `getModelDir()` | `<root>/classes/models`(探索の基準ディレクトリ) |
| `getRepositoryDir()` | `<root>/classes/repositories`(探索の基準ディレクトリ) |
| `getContentDirectory()` | `shop` など |
| `getTemplateDir()` | `<root>/templates` |
| `getTemplateCacheDir()` | `<root>/cache/template` |
| `getViewDirectory()` | `view/shop` など |
| `getWebDir()` | `<root>/web` |

### 4.5 フロントコントローラ

```php
<?php
/**
 * サンプルショッピングサイトのフロントコントローラ(本番用)
 */
require(dirname(__FILE__).'/../../bootstrap.php');

//アプリケーションクラスのインスタンスを生成
$app = new ShopApplication(false);
//アプリケーション実行
$app->run();
```

コンストラクタ引数がデバッグモードフラグです。`index_dev.php` では `true` を渡します。
デバッグモードでは以下が変わります。

- テンプレートの更新を毎リクエスト検証する(キャッシュを自動で作り直す)
- 404 ページに例外メッセージを表示する

> `index_dev.php` と `*.sample` は各アプリの `.htaccess` で配信を禁止しています。

### 4.6 環境固有の設定(`config/`)

DB接続情報のように環境ごとに異なる値は、アプリケーションクラスへ直接書かず
`config/` 配下の設定ファイルへ分離します。

| ファイル | 追跡 | 内容 |
|---|---|---|
| `config/<設定名>.sample.php` | する | 雛形。キーの一覧と書き方を示す。**実在する認証情報は書かない** |
| `config/<設定名>.local.php` | しない | 実値。`.gitignore` で除外している。各自の環境で作成する |

**セットアップ手順**

```
copy config\database.sample.php config\database.local.php
```

コピーしたら `config/database.local.php` を自分の環境の値へ書き換えます。

**設定ファイルの形式**

```php
<?php
return array(
	//接続設定の名前 => DbManager::connect() へ渡す接続情報
	'master'=>array(
		'dsn'=>'mysql:host=127.0.0.1;port=3307;dbname=toolbox_shop;charset=utf8mb4',
		'user'=>'your_db_user',
		'password'=>'your_db_password',
		'options'=>array(
			//プリペアドステートメントをMySQL側で処理させる(エミュレーションを無効にする)
			PDO::ATTR_EMULATE_PREPARES=>false,
		),
	),
);
```

**読み込み方**

`Application::loadConfig()` が `config/<設定名>.local.php` を読み込み、
そのファイルが返す配列をそのまま返します。

```php
protected function configure() {
	//DB接続情報を読み込む(実値は config/database.local.php に置く)
	$config = $this->loadConfig('database');

	$this->dbManager->connect('master', $config['master']);
}
```

設定ファイルが無い場合は `RuntimeException` を投げ、
雛形をコピーするよう促すメッセージを表示します。

> `config/` はプロジェクトルート直下にあり、ルートの `.htaccess` が
> `Require all denied` にしているため外部からは参照できません。

---

## 5. ルーティング

> **注意(URLの形式)**: 既定では画面へ出力するURLは `./index.php?_route=/detail/3` のGETパラメータ形式・相対パスで生成し、
> 受け付ける側は GETパラメータ・PATH_INFO・リライトのいずれでも解決する(サーバ設定に依存しないため)。
> 詳細は [README.md](README.md) の「URLとルーティング」を参照。ルーティング定義の書き方自体は変わらない。

### 5.1 定義の書き方

```php
protected function registerRoutes() {
	return array(
		//固定パス
		'/'=>array('controller'=>'product', 'action'=>'index'),
		'/cart'=>array('controller'=>'cart', 'action'=>'index'),
		'/cart/add'=>array('controller'=>'cart', 'action'=>'add'),

		//パラメータ付き(:名前 でキャプチャする)
		'/detail/:id'=>array('controller'=>'product', 'action'=>'detail'),
		'/category/:id'=>array('controller'=>'product', 'action'=>'index'),
	);
}
```

- **定義した順に評価される。** 固定パスを先に、パラメータ付きを後に並べる
  (`/shipping/carrier/create` を `/shipping/carrier/:id` より先に書く)
- `:id` は `[^/]+` にコンパイルされ、`$params['id']` としてアクションに渡る
- パスは **フロントコントローラの設置ディレクトリを起点とした相対パス** で書く。
  サブディレクトリ公開でもバーチャルホストでも同じ定義がそのまま使える

### 5.2 コントローラ名の解決

`'controller'=>'adminShipping'` は `ucfirst()` を適用して `AdminShippingController` に解決されます。
アクションは `'action'=>'carrierEdit'` → `carrierEditAction()` です。

Controller クラスは `classes/controllers/<コンテンツ>/<クラス名>.php` に配置します
(オートローダではなく `Application::findController()` が直接 `require_once` します)。

探索順はコンテンツ配下 → 親コンテンツ → 共通です。
`getContentDirectory()` が `'shop/admin'` のとき `AdminShippingController` は次の順に探されます。

```
classes/controllers/shop/admin/AdminShippingController.php
classes/controllers/shop/AdminShippingController.php
classes/controllers/AdminShippingController.php
```

そのため、コンテンツが違えば `ProductController` のような同じクラス名を使えます。

---

## 6. Controller クラス

### 6.1 基本形

```php
<?php

/**
 * 商品一覧・商品詳細のコントローラクラス
 *
 * 画面の描画はモデルが担うため、本クラスはリクエストの受け取りと
 * モデルへの指示出しに徹する。
 *
 * @author S.Morikane
 */
class ProductController extends Controller {
	/**
	 * 商品詳細を描画する関数
	 * @author S.Morikane
	 * @param $params  //ルーティングパラメータ
	 * @return string
	 * @throws HttpNotFoundException
	 */
	public function detailAction($params=array()) {
		$id = 0;

		//商品IDの指定があるかチェックする
		if(isset($params['id'])) {
			$id = (int)$params['id'];
		}

		//モデルのインスタンスを生成する
		$model = $this->model('ProductModel');

		//モデルへ商品詳細の描画を指示する
		$html = $model->renderDetail($id, $this->generateCsrfToken('cart/add'));

		//存在しない、または非公開の商品が指定された場合は404にする
		if(is_null($html)) {
			$this->forward404();
		}

		return $html;
	}
}
```

### 6.2 Controller が提供するメソッド

| メソッド | 内容 |
|---|---|
| `model($modelClass)` | Model のインスタンスを生成する(Application を渡す) |
| `forward404()` | `HttpNotFoundException` を投げる |
| `redirect($url)` | 302 リダイレクト。`/cart` のような相対パスを渡すとベースURLを補完する |
| `generateCsrfToken($formName)` | CSRFトークンを発行してセッションへ保持する |
| `checkCsrfToken($formName, $token)` | CSRFトークンを検証する(検証に成功したトークンは破棄される) |

### 6.3 利用できるプロパティ

`$this->request` / `$this->response` / `$this->session` / `$this->dbManager` / `$this->application`

### 6.4 「存在しない」の返し方

Model は「対象が見つからない」場合に **`null` を返す** 規約にしています。
Controller はそれを受けて `forward404()` を呼びます。Model に HTTP の知識を持たせないためです。

---

## 7. Model クラス

### 7.1 基本形

```php
<?php

/**
 * 商品一覧・商品詳細のモデルクラス
 *
 * 表示用の整形(金額のカンマ区切り、URLの組み立てなど)はすべて本クラスで行い、
 * テンプレート側には判定と出力だけを残す。
 *
 * @author S.Morikane
 */
class ProductModel extends ShopModel {
	/**
	 * 商品詳細を描画する関数
	 * @author S.Morikane
	 * @param $id           //商品ID
	 * @param $token        //カート追加フォームへ埋め込むCSRFトークン
	 * @return string|null  //描画結果のHTML。商品が存在しない場合はnull
	 */
	public function renderDetail($id, $token) {
		//指定された商品を取得する
		$product = $this->repository('Product')->fetchPublishedById((int)$id);

		//存在しない、または非公開の商品は呼び出し側で404にできるようnullを返す
		if($product === false) {
			return null;
		}

		$stock = (int)$product['stock'];

		$data = $this->createShopViewData($product['name'].' | サンプルショップ');

		//商品情報を設定する
		$data->set('_name', $product['name']);
		$data->set('_price', $this->formatPrice($product['price']));
		$data->set('_stock', $stock);

		//在庫の有無で購入ボタンと在庫切れ表示を切り替える
		$data->set('_soldOut', ($stock <= 0));

		//カート追加フォーム用のCSRFトークンを設定する
		$data->set('_token', $token);

		return $this->render('product/detail', $data);
	}
}
```

### 7.2 Model が提供するメソッド

| メソッド | 内容 |
|---|---|
| `createViewData()` | `ViewData` を生成する。`_baseUrl` と `_currentYear` が設定済み |
| `render($path, $data, $layout='layout')` | View へ描画を委譲する。`$layout` に `null` を渡すとレイアウトなし |
| `repository($name)` | リポジトリを取得する。`repository('Product')` → `ProductRepository` |
| `trimInput($value)` | 前後の空白(全角スペース含む)を安全に除去する |
| `isValidUtf8($value)` | 正しいUTF-8かどうかを判定する |

> **`trimInput()` を必ず使うこと。**
> `preg_replace('/.../u', ...)` は不正なUTF-8を渡すと `null` を返します。
> `null` をそのまま入力値として扱うと必須チェック(`$value === ''`)をすり抜け、
> DB へ `NULL` が渡って `Column cannot be null` になります。`trimInput()` はこれを吸収します。

### 7.3 アプリ共通の基底 Model を作る

ショップ側は `ShopModel`、管理画面側は `AdminModel` を挟み、共通処理をまとめています。

```php
abstract class AdminModel extends Model {
	//管理画面のレイアウトファイル名
	const LAYOUT = 'layout';

	/**
	 * 管理画面用のViewDataを生成する関数
	 * @author S.Morikane
	 * @param $title    //画面タイトル
	 * @param $current  //現在地を示すナビゲーションのキー
	 * @return ViewData
	 */
	protected function createAdminViewData($title, $current='') {
		$data = $this->createViewData();
		$baseUrl = $this->request->getBaseUrl();

		//レイアウトとヘッダーで使用する値を設定する
		$data->set('_title', $title.' | 管理画面');
		$data->set('_heading', $title);

		//ヘッダーのリンクを設定する(フロントコントローラの設置場所に依存させないためモデル側で組み立てる)
		$data->set('_navShippingUrl', $baseUrl.'/shipping');

		//現在地に応じてナビゲーションの強調表示を切り替える
		$data->set('_navCurrentShipping', ($current === 'shipping'));

		return $data;
	}
}
```

このように、**URL の組み立ても「現在地の判定」も Model が行います。**
テンプレート側は `{_navShippingUrl}` と `{_navCurrentShipping:y}` を書くだけになります。

---

## 8. Repository クラス

### 8.1 基本形

```php
<?php

/**
 * カテゴリテーブル(category)のリポジトリクラス
 *
 * 【テーブル定義】db/schema.sql を参照
 *
 * @author S.Morikane
 */
class CategoryRepository extends DbRepository {
	/**
	 * カテゴリを表示順に全件取得する関数
	 * @author S.Morikane
	 * @return array
	 */
	public function fetchAllOrderBySort() {
		//実行するSQLを設定
		$sql = 'SELECT id, name, sort_order FROM category ORDER BY sort_order ASC, id ASC';

		return $this->fetchAll($sql);
	}

	/**
	 * カテゴリを1件取得する関数
	 * @author S.Morikane
	 * @param $id     //カテゴリID
	 * @return mixed  //該当が無い場合はfalse
	 */
	public function fetchById($id) {
		//実行するSQLを設定
		$sql = 'SELECT id, name, sort_order FROM category WHERE id = :id';

		return $this->fetch($sql, array(':id'=>$id));
	}
}
```

### 8.2 DbRepository が提供するメソッド

| メソッド | 戻り値 | 内容 |
|---|---|---|
| `execute($sql, $params=array())` | `PDOStatement` | INSERT / UPDATE / DELETE |
| `fetch($sql, $params=array())` | `array` / `false` | 1行取得 |
| `fetchAll($sql, $params=array())` | `array` | 全行取得 |

- 値のバインドは必ずプレースホルダを使う
- `LIMIT` / `OFFSET` はプレースホルダだと文字列化されて構文エラーになるため、`(int)` へキャストして埋め込む

```php
//LIMIT/OFFSETはプレースホルダで文字列化されると構文エラーになるため整数へキャストして埋め込む
$sql .= ' ORDER BY id DESC';
$sql .= ' LIMIT '.(int)$limit.' OFFSET '.(int)$offset;
```

### 8.3 トランザクション

PDO 接続は `DbManager` が一元管理しているため、どのリポジトリから開始しても同じ接続に効きます。
`OrderRepository` に薄いラッパを用意しています。

```php
//都道府県分まとめて更新するためトランザクションで囲む
$this->repository('Order')->beginTransaction();

try {
	//(略)複数のリポジトリ呼び出し
	$this->repository('Order')->commit();

} catch(Exception $e) {
	//途中で失敗した場合は更新ごと取り消す
	$this->repository('Order')->rollBack();

	throw $e;
}
```

---

## 9. View クラス

Model からは `$this->render()` 経由で使うため、直接生成することはほとんどありません。

```php
public function __construct($templateDir, $cacheDir, $debug=false, $viewDirectory='view')
public function render($path, ViewData $data, $layout='layout')
public function clearCache()
```

- `$path` は **Viewディレクトリからの相対パス(拡張子なし)**。
  `getViewDirectory()` が `view/shop` のとき `render('product/detail', ...)` は
  `templates/view/shop/product/detail.html` を指す
- `$layout` も同じ Viewディレクトリ配下から解決される。
  `render('login', $data, 'layout_bare')` → `templates/view/shop/admin/layout_bare.html`
- `$layout` に `null` を渡すとレイアウトなしで描画する(Ajax レスポンス、404 ページなど)

---

## 10. Request / Session / Response

### 10.1 Request

| メソッド | 内容 |
|---|---|
| `isPost()` | POST リクエストかどうか |
| `getGet($name, $default=null)` | GET パラメータの取得 |
| `getPost($name, $default=null)` | POST パラメータの取得 |
| `getHost()` | ホスト名 |
| `isSsl()` | HTTPS かどうか |
| `getRequestUri()` | リクエストURI |
| `getBaseUrl()` | フロントコントローラの設置ディレクトリまでのURL |
| `getPathInfo()` | ベースURLを除いたパス(ルーティングの入力) |

`getBaseUrl()` は以下をすべて吸収します。

| 公開形態 | リクエストURI | getBaseUrl() | getPathInfo() |
|---|---|---|---|
| サブディレクトリ + リライト | `/ToolBox/web/shop/detail/3` | `/ToolBox/web/shop` | `/detail/3` |
| バーチャルホスト | `/detail/3` | `` (空文字) | `/detail/3` |
| リライトなし | `/ToolBox/web/shop/index.php/detail/3` | `/ToolBox/web/shop/index.php` | `/detail/3` |

**ただし `getBaseUrl()` はサーバ上のパスを返すため、ロードバランサ配下では公開URLと一致しないことがあります。
テンプレートへ渡すURLは Model の `url()`(現在のページからの相対パス)で生成し、
`{_baseUrl}`(相対パス)は CSS・JS・画像の参照にだけ使います。** 詳細は [README.md](README.md) の「URLとルーティング」を参照。

### 10.2 Session

| メソッド | 内容 |
|---|---|
| `set($name, $value)` / `get($name, $default=null)` / `remove($name)` / `clear()` | セッション値の操作 |
| `regenerate($destroy=true)` | セッションIDの再生成 |
| `setAuthenticated($bool)` | 認証状態の設定(内部で `regenerate()` を呼ぶ) |
| `isAuthenticated()` | 認証済みかどうか |

キーは `'admin/shipping/messages'` のようにスラッシュ区切りで名前空間を作る運用にしています。

### 10.3 Response

Controller が直接触るのは `redirect()` 経由がほとんどです。
`setStatusCode()` / `setHttpHeader()` / `setContent()` / `send()` を持ちます。

---

## 11. CSRF対策と PRG パターン

### 11.1 PRG(POST/Redirect/GET)

**画面の描画は GET のアクションでのみ行い、POST のアクションは
「検証 → セッションへ保持 → リダイレクト」だけを行います。**
リロードによる二重送信を防ぐためです。

```php
public function indexAction() {
	//モデルのインスタンスを生成する
	$model = $this->model('AdminShippingModel');

	//POSTかチェック(設定フォームからの送信)
	if($this->request->isPost()) {
		//CSRFトークンを検証する
		if(!$this->checkCsrfToken(self::TOKEN_SETTING, $this->request->getPost('_token'))) {
			$this->session->set(self::SESSION_ERRORS, array('セッションの有効期限が切れました。'));

			return $this->redirect('/shipping');
		}

		//POSTデータを取得してモデルで整形する
		$input = $model->formatSettingInput(
			array(
				'defaultCarrierId'=>$this->request->getPost('defaultCarrierId', ''),
				'freeShippingThreshold'=>$this->request->getPost('freeShippingThreshold', ''),
			)
		);
		$this->session->set(self::SESSION_INPUT, $input);

		//モデルへ入力値の検証を依頼する
		$errors = $model->validateSetting($input);

		//エラーがあったら設定画面へ戻す
		if(count($errors) > 0) {
			$this->session->set(self::SESSION_ERRORS, $errors);

			return $this->redirect('/shipping');
		}

		//モデルへ保存を指示する
		$result = $model->saveSetting($input);

		//入力途中の値を破棄して結果を伝える
		$this->session->remove(self::SESSION_INPUT);
		$this->session->remove(self::SESSION_ERRORS);
		$this->session->set(self::SESSION_MESSAGES, array($result['message']));

		return $this->redirect('/shipping');
	}

	//ここからGETの処理。直前のPOSTで保持した入力値とメッセージを取得する
	$input = $this->session->get(self::SESSION_INPUT, array());
	$errors = $this->session->get(self::SESSION_ERRORS, array());
	$messages = $this->session->get(self::SESSION_MESSAGES, array());

	//一度表示したら破棄する(リロードで残り続けるのを防ぐ)
	$this->session->remove(self::SESSION_ERRORS);
	$this->session->remove(self::SESSION_MESSAGES);

	//モデルへ設定画面の描画を指示する
	return $model->renderIndex($input, $errors, $messages, $this->generateCsrfToken(self::TOKEN_SETTING));
}
```

### 11.2 CSRFトークン

- フォーム名ごとに `generateCsrfToken('admin/shipping/setting')` で発行する
- **検証に成功したトークンは破棄される(ワンタイム)**。同一フォーム名で最大10個まで保持される
- テンプレートには `<input type="hidden" name="_token" value="{_token}">` を置く
- 一覧の各行に削除フォームを並べる場合は、1つのトークンを全行で共有してよい

---

## 12. ログイン認証

### 12.1 アクション単位の保護

```php
class AdminShippingController extends Controller {
	//全アクションで認証を必須にする
	protected $authActions = true;
}
```

- `protected $authActions = true;` … 全アクションを保護する
- `protected $authActions = array('edit', 'delete');` … 指定アクションのみ保護する

未認証でアクセスされると `UnauthorizedActionException` が投げられ、
Application が `$loginAction` で指定されたアクションへ処理を移します。

```php
class AdminApplication extends Application {
	//認証が必要なアクションへ未認証でアクセスされた場合の遷移先
	protected $loginAction = array('auth', 'login');
}
```

> リダイレクトではなく **同一リクエスト内でログイン画面を描画** します。
> そのため URL はアクセスしようとしたパスのまま、内容がログイン画面になります。

### 12.2 ログイン処理

```php
//パスワードを照合する
if(!password_verify($password, $user['password'])) {
	return false;
}

//セッションIDを再生成してから認証済みにする(セッションフィクセーション対策)
$this->session->setAuthenticated(true);
$this->session->set(self::SESSION_USER, array('id'=>$user['id'], 'name'=>$user['name']));
```

パスワードは `password_hash()`(bcrypt)で保存し、`password_verify()` で照合します。

---

## 13. エラー処理

| 例外 | 発生元 | Application の処理 |
|---|---|---|
| `HttpNotFoundException` | ルート不一致 / `forward404()` | ステータス404 + `error/404` をレイアウトなしで描画 |
| `UnauthorizedActionException` | `$authActions` による保護 | `$loginAction` のアクションを実行 |
| `Framework\Template\TemplateException` | テンプレート不備 | 404 描画時のみ捕捉し、素のHTMLで応答する |

404 テンプレートは各アプリの Viewディレクトリ配下に置きます
(`templates/view/shop/error/404.html`)。デバッグモードでは例外メッセージが `{_message}` に入ります。

---

## 14. 新しいアプリケーションを追加する手順

例として `blog` を追加する場合。

1. **Application クラスを作る**
   `classes/applications/blog/BlogApplication.php`
   `getRootDir()` / `getContentDirectory()`(`'blog'`)/ `registerRoutes()` / `configure()` を実装
   `getRootDir()` の `..` の数は設置階層に合わせる(`classes/applications/blog/` なら3階層上)
   `getContentDirectory()` を実装すれば、Controller / Model / Repository / View の
   置き場所はすべて `blog` 配下に決まる(`getViewDirectory()` の上書きは不要)
2. **公開ディレクトリを作る**
   `web/blog/` に `index.php` / `index_dev.php` / `.htaccess` / `commons/`(css・js・画像)を配置
   `.htaccess` は `web/shop/.htaccess` をコピーして使う
3. **テンプレートディレクトリを作る**
   `templates/view/blog/` に `layout.html` / `_parts/` / `error/404.html` を配置
4. **Controller / Model / Repository を作る**
   `classes/controllers/blog/` `classes/models/blog/` `classes/repositories/blog/`
   他のコンテンツと同じクラス名を使ってよい(実行中のアプリのものだけが読み込まれる)
5. **DB を使う場合は接続設定を読み込む**
   既存のデータベースをそのまま使うなら `configure()` で
   `$config = $this->loadConfig('database');` → `$this->dbManager->connect('master', $config['master']);` と書くだけでよい
   別のデータベースを使う場合は `config/database.sample.php` へキーを追加し、
   各自の `config/database.local.php` にも同じキーを追加してから、そのキーを `connect()` へ渡す

> **アプリケーションクラスは混ぜず、必ず別クラスとして作成すること。**
> `commons/`(CSS・JS・画像)もアプリごとに持たせること。
> バーチャルホストで `web/blog/` をドキュメントルートにした場合、
> 他アプリの `commons/` は配信できなくなるためです。

---

## 15. 新しい画面を追加する手順

例として管理画面へ「送料設定」を追加した実際の手順。

| # | 作業 | ファイル |
|---|---|---|
| 1 | テーブルを定義する | `db/schema.sql` / `db/seed.sql` |
| 2 | Repository を作る | `classes/repositories/shop/ShippingFeeRepository.php`(ショッピングサイト側からも使うため shop/ へ置く) |
| 3 | Model を作る(データ整形・検証・保存・描画) | `classes/models/shop/admin/AdminShippingModel.php` |
| 4 | Controller を作る(PRG と CSRF) | `classes/controllers/shop/admin/AdminShippingController.php` |
| 5 | ルーティングを追加する | `classes/applications/shop/admin/AdminApplication.php` |
| 6 | ナビゲーションを追加する | `classes/models/shop/admin/AdminModel.php` + `templates/view/shop/admin/_parts/header.html` |
| 7 | テンプレートを作る | `templates/view/shop/admin/shipping/*.html` |
| 8 | CSS を追加する | `web/shop/admin/commons/css/style.css` |

ルーティングは **固定パスを先に、パラメータ付きを後に** 並べます。

```php
//送料設定(POSTで保存してリダイレクト / GETで描画)
'/shipping'=>array('controller'=>'adminShipping', 'action'=>'index'),
//配送業者の新規登録(POSTで登録してリダイレクト / GETで描画)
'/shipping/carrier/create'=>array('controller'=>'adminShipping', 'action'=>'carrierCreate'),
//配送業者の削除(POSTで処理してリダイレクト)
'/shipping/carrier/delete'=>array('controller'=>'adminShipping', 'action'=>'carrierDelete'),
//配送業者の編集(POSTで更新してリダイレクト / GETで描画)
'/shipping/carrier/edit/:id'=>array('controller'=>'adminShipping', 'action'=>'carrierEdit'),
//都道府県別の送料(POSTで保存してリダイレクト / GETで描画)
'/shipping/fee/:id'=>array('controller'=>'adminShipping', 'action'=>'fee'),
```

---

## 16. 公開設定(.htaccess / バーチャルホスト)

### 16.1 サブディレクトリ公開(DocumentRoot がプロジェクトの外)

| ファイル | 役割 |
|---|---|
| `ToolBox/.htaccess` | `Require all denied` —— `templates/` `classes/` `config/` `cache/` `db/` `logs/` を全部非公開にする |
| `web/.htaccess` | `Require all granted` —— 公開ディレクトリだけ許可を戻す。`DirectoryIndex index.html index.php` |
| `web/shop/.htaccess` | `DirectoryIndex index.php` + リライト + `index_dev.php` / `*.sample` の配信禁止 |

`web/shop/.htaccess` のリライト部分。

```apache
# このディレクトリの既定ドキュメントはフロントコントローラとする
DirectoryIndex index.php

<IfModule mod_rewrite.c>
	RewriteEngine On

	# RewriteBase /ToolBox/web/shop/

	# 実在するファイルはそのまま配信する(css / js / 画像など)
	RewriteCond %{REQUEST_FILENAME} !-f
	# 実在するディレクトリもそのまま扱う
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
```

**注意点**

- `DirectoryIndex` は **配下のディレクトリへ継承される**。`web/.htaccess` に `index.html` だけを書くと、
  `web/shop/` が 403 になる。両方を併記し、各アプリ側で `index.php` を明示する
- `RewriteRule` は子の `.htaccess` が親を上書きするため、`RewriteBase` を書かなければ
  「その `.htaccess` を置いたディレクトリ」が基準になり、サブディレクトリ公開でもバーチャルホストでも動く
- 階層が深くて 404 になる場合のみ `RewriteBase` を実際の公開パスに合わせて有効化する

### 16.2 バーチャルホスト公開

`docs/vhost.sample.conf` を参照。`web/shop/` をドキュメントルートにします。

```apache
<VirtualHost *:80>
	ServerName shop.local
	DocumentRoot "D:/www/mori/tools/ToolBox/web/shop"

	<Directory "D:/www/mori/tools/ToolBox/web/shop">
		AllowOverride All
		Require all granted
		Options -Indexes +FollowSymLinks
	</Directory>
</VirtualHost>
```

この場合 `ToolBox/.htaccess` と `web/.htaccess` は読み込まれませんが、
`templates/` や `classes/` はドキュメントルートの外になるため、そもそも配信されません。

### 16.3 リライトなしでの動作

`mod_rewrite` が使えない環境では `index.php/detail/3` の形式でアクセスできます。
`Request::getPathInfo()` がフロントコントローラ名を除いてパスを取り出すため、
ルーティング定義を変更する必要はありません。
