PHPでログイン機能を実装するチュートリアル #4


1. [基本設計、ユーザーモデル](http://qiita.com/ShibuyaKosuke/items/f114ffccf441edb2b745)
2. [オートローダー](http://qiita.com/ShibuyaKosuke/items/6109810464a7293293bd)
3. [例外・ログ](http://qiita.com/ShibuyaKosuke/items/88b1305a4ff0c5f3e728)
4. [PDO シングルトン　SQLインジェクション](http://qiita.com/ShibuyaKosuke/items/094df7f399912ae7df56)
	* [汎用データベース・クラス作成](http://qiita.com/ShibuyaKosuke/items/8502492ac024362b528d)

5. [ユーザーモデルの作成](http://qiita.com/ShibuyaKosuke/items/81c556d836d70a29c5f3)
6. [クラスの継承](http://qiita.com/ShibuyaKosuke/items/490ffa6a92670cf6c644)
7. [テンプレート・クラスの実装](http://qiita.com/ShibuyaKosuke/items/803dc58a23601118942e)
8. [アカウントロック機能の実装](http://qiita.com/ShibuyaKosuke/items/e890becdb8193d3def05)
9. [メール送信機能の実装](http://qiita.com/ShibuyaKosuke/items/78c8673782ebbf51584b)
10. [アカウントロック解除機能の実装](http://qiita.com/ShibuyaKosuke/items/ee4b1896071eeca37628)
11. [CSRF対策](http://qiita.com/ShibuyaKosuke/items/8100e304e4447e44e98c)

データベース操作クラスを実装するのですが、前提とする基礎知識が必要なので、曖昧なままになっている場合は、以下の用語について明らかにしておいてください。

* [トランザクション](http://e-words.jp/w/%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3.html)
* [コミット](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%9F%E3%83%83%E3%83%88)
* [ロールバック](http://e-words.jp/w/%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF.html)
* [SQLインジェクション](https://ja.wikipedia.org/wiki/SQL%E3%82%A4%E3%83%B3%E3%82%B8%E3%82%A7%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)

ここで説明する「ログイン機能」についての処理を整理しておきましょう。

【ログイン画面】
1. 「メールアドレス」と「パスワード」を POST で送信
2. メールアドレスで ユーザーを検索
3. パスワードが一致すれば、ログイン成功。ログイン失敗回数をリセットして、トップページに遷移する。失敗したら、ログイン失敗回数をインクリメントし、ログインページにエラーメッセージを表示する。このとき、ログイン失敗が３回に到達したら、アカウントをロックする。

上記の処理から、それぞれ、処理を細分化して関数に切り出します。

1. メールアドレスからユーザーを検索する。
2. パスワードが一致しているかどうかを判定する。
3. ログイン失敗をリセットする。
4. ログイン失敗をインクリメントする。
5. アカウントがロックされているかどうかを判定する。

こうしてみると、DBとのやりとりでSELECTやUPDATEが一連の処理で幾つか登場してきます。データの一貫性を保つためにも、トランザクションや、ロールバックの必要があるので、 **DBのコネクションは、処理が終わるまで、同一性を保たなくてはなりません** 。どういうことかというと、SELECTのときに使ったコネクションとUPDATEのときに使うコネクションが同一でない場合、コミットやロールバックが効かなくなってしまいます。

ではユーザーモデルにこれらの関数を実装します。いきなり実装しようとするのではなく、以下のように、雛形を作っておくと、時間を空けても次回に何をするかがわかりやすい。

データベースに問い合わせをする関数は別の汎用的なクラスを作成したほうが便利そうです。実装において注意すべきは「接続の同一性を保証する」ことですね。

```php:UserModel.class.php
<?php

namespace MyApp\model;

/**
 * UserModel
 */
final class UserModel
{

	/**
	 * アカウントをロックするログイン失敗回数
	 */
	const LOCK_COUNT = 3;

	/**
	 * アカウントをロックする時間（分）
	 */
	const LOCK_MINUTE = 30;

	/**
	 * ユーザーID
	 */
	private $_userId = null;

	/**
	 * パスワード（ハッシュ）
	 */
	private $_password = null;

	/**
	 * 表示名
	 */
	private $_displayName = null;

	/**
	 * メールアドレス
	 */
	private $_email = null;

	/**
	 * トークン
	 */
	private $_token = null;

	/**
	 * ログイン失敗回数
	 */
	private $_loginFailureCount = null;

	/**
	 * ログイン失敗日時
	 */
	private $_loginFailureDatetime = null;

	/**
	 * 削除フラグ
	 */
	private $_deleteFlag = null;

	/**
	 * メールアドレスからユーザーを検索する
	 * @param string $email
	 * @return \MyApp\model\UserModel
	 */
	public function getModelByEmail($email)
	{
		return $this;
	}

	/**
	 * パスワードが一致しているかどうかを判定する
	 * @param type $password
	 * @return bool
	 */
	public function checkPassword($password)
	{
		return true;
	}

	/**
	 * ログイン失敗をリセットする
	 * １以上のときに０にする
	 * @return bool
	 */
	public function loginFailureReset()
	{
		return true;
	}

	/**
	 * ログイン失敗をインクリメントする
	 * 指定回数（self::LOCK_COUNT）に満たないときのみ＋１
	 * @return bool
	 */
	public function loginFailureIncrement()
	{
		return true;
	}

	/**
	 * アカウントがロックされているかどうかを判定する
	 * @return bool ロックされていたら true
	 */
	public function isAccountLock()
	{
		return false;
	}

	/**
	 * ユーザーIDを設定する
	 * @param int $userId
	 * @return \MyApp\model\UserModel
	 */
	public function setUserId($userId)
	{
		$this->_userId = $userId;
		return $this;
	}

	/**
	 * パスワード（ハッシュ）を設定する
	 * @param string $password
	 * @return \MyApp\model\UserModel
	 */
	public function setPassword($password)
	{
		$this->_password = $password;
		return $this;
	}

	/**
	 * 表示名を設定する
	 * @param string $displayName
	 * @return \MyApp\model\UserModel
	 */
	public function setDisplayName($displayName)
	{
		$this->_displayName = $displayName;
		return $this;
	}

	/**
	 * メールアドレスを設定する
	 * @param string $email
	 * @return \MyApp\model\UserModel
	 */
	public function setEmail($email)
	{
		$this->_email = $email;
		return $this;
	}

	/**
	 * トークンを設定する
	 * @param string $token
	 * @return \MyApp\model\UserModel
	 */
	public function setToken($token)
	{
		$this->_token = $token;
		return $this;
	}

	/**
	 * ログイン失敗回数を設定する
	 * @param int $loginFailureCount
	 * @return \MyApp\model\UserModel
	 */
	public function setLoginFailureCount($loginFailureCount)
	{
		$this->_loginFailureCount = $loginFailureCount;
		return $this;
	}

	/**
	 * ログイン失敗日時を設定する
	 * @param string $loginFailureDatetime
	 * @return \MyApp\model\UserModel
	 */
	public function setLoginFailureDatetime($loginFailureDatetime)
	{
		$this->_loginFailureDatetime = $loginFailureDatetime;
		return $this;
	}

	/**
	 * 削除フラグを設定する
	 * @param bool $deleteFlag
	 * @return \MyApp\model\UserModel
	 */
	public function setDeleteFlag($deleteFlag)
	{
		$this->_deleteFlag = $deleteFlag;
		return $this;
	}

	/**
	 * ユーザーIDを取得する
	 * @return int
	 */
	public function getUserId()
	{
		return $this->_userId;
	}

	/**
	 * パスワード（ハッシュ）を取得する
	 * @return string
	 */
	public function getPassword()
	{
		return $this->_password;
	}

	/**
	 * 表示名を取得する
	 * @return string
	 */
	public function getDisplayName()
	{
		return $this->_displayName;
	}

	/**
	 * メールアドレスを取得する
	 * @return string
	 */
	public function getEmail()
	{
		return $this->_email;
	}

	/**
	 * トークンを取得する
	 * @return string
	 */
	public function getToken()
	{
		return $this->_token;
	}

	/**
	 * ログイン失敗回数を取得する
	 * @return int
	 */
	public function getLoginFailureCount()
	{
		return $this->_loginFailureCount;
	}

	/**
	 * ログイン失敗日時を取得する
	 * @return string
	 */
	public function getLoginFailureDatetime()
	{
		return $this->_loginFailureDatetime;
	}

	/**
	 * 削除フラグを取得する
	 * @return bool
	 */
	public function getDeleteFlag()
	{
		return $this->_deleteFlag;
	}

}
```

ログイン処理を記述しておきましょう。さっくり。

```php:LoginController.class.php
<?php

namespace MyApp\controller;

use \MyApp\model\UserModel;

/**
 * UserController
 */
class LoginController
{

	/**
	 * メールアドレスとパスワードでログインする
	 * @return void
	 */
	static public function login()
	{
		try {
			if (filter_input_array(INPUT_POST)) {

				//フォームからの値を受け取る
				$email = filter_input(INPUT_POST, 'email');
				$password = filter_input(INPUT_POST, 'password');
				
				//トランザクションを開始する
				//@todo

				//	email から ユーザーモデル を取得する
				$objUserModel = new UserModel();
				$objUserModel->getModelByEmail($email);

				//	ロックされたアカウントかどうかをチェックする
				if ($objUserModel->isAccountLock()) {
					//	ロックされている
					throw new \Exception('アカウントはロックされています。');
				}

				//パスワードチェック
				if (!$objUserModel->checkPassword($password)) {
					//ログイン失敗
					$objUserModel->loginFailureIncrement();
					throw new \Exception('ログインに失敗しました。');
				}

				//	ログイン失敗をリセット
				$objUserModel->loginFailureReset();
				
				//コミット
				//@todo

				//ページ遷移
				header("location: top.php");
			}
		} catch (\Exception $e) {
			//ロールバック
			//@todo
		}
	}

	/**
	 * ログインしているかどうかチェックする
	 * @return bool
	 */
	static public function checkLogin()
	{

	}

	/**
	 * ログアウトする
	 * @return void
	 */
	static public function logout()
	{

	}

}


```

次回は、汎用のデータベース・クラスを作成します。