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


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)

前回の続きがこんなに空いてしまいました。DB周りの実装にかなり悩んでいて、悩んでいるうちにめんどくさく…

DB周りの実装については、PDOを利用する方法一択かと思います。
この辺りの機能は一度作ってしまえば、再利用しやすいし、使い慣れると、開発速度も速くなります。また、SQLインジェクションなどの脆弱性を埋め込みにくくなりますので一石二鳥。

基本的にDB周りで必要な機能は以下のとおり。

* SELECT
* INSERT
* UPDATE
* DELETE

これらのSQLが手っ取り早く実行できればいいわけです。

クラス作成にあたっては、設計思想によって、実装方法は様々なバリエーションがあることになりますが、以下の目的で作成することとします。

* メソッドの呼び出し方法が簡単であること
* 接続（コネクションを使い回すこと）
* SQLのエラーが適切に通知されること

これらを目的として実装します。

以下のようなスケルトンを作成します。

```php:Db.class.php
<?php

namespace MyApp\common;

class Db
{

	/**
	 * SELECT実行
	 * @param string $sql
	 * @param array $arr
	 * @return array
	 */
	public static function select($sql, array $arr = array())
	{
	}

	/**
	 * INSERT実行
	 * @param string $sql
	 * @param array $arr
	 * @return int
	 */
	public static function insert($sql, array $arr)
	{
	}

	/**
	 * UPDATE実行
	 * @param string $sql
	 * @param array $arr
	 * @return bool
	 */
	public static function update($sql, array $arr)
	{
	}

	/**
	 * DELETE実行
	 * @param string $sql
	 * @param array $arr
	 * @return bool
	 */
	public static function delete($sql, array $arr)
	{
	}
	
}
```

`static` でメソッドを設定しておくと、呼び出し元からは、以下のように呼び出しできます。

```php
<?php

use MyApp\common\Db;

$sql = 'SELECT id FROM Users';
$res = Db::select($sql);

$sql = 'INSERT INTO Users (id, name) VALUES (null, :name)';
$arr = array(
	':name' => 'Sato'
);
Db::insert($sql, $arr);
```

楽チン！

今回のメインは、「コネクションをいかにして使いまわすか」ということですが、以下のように実装します。
大事なのは、`getInstance()` の実装です。`$instance` が `null` のとき、`PDO` オブジェクトを生成し、それを返す。`null` でないときは、生成済みの `PDO` オブジェクトを返す仕組みになっています。

トランザクション処理に関連する、`transaction()`, `commit()`, `rollback()` を実装しています。

```php:Db.class.php
<?php

class Db
{

	/**
	 * 接続文字列
	 */
	const DSN = 'mysql:dbname=%s;host=localhost;charset=utf8;';

	/**
	 * データベース名
	 */
	const DBNAME = 'sample';

	/**
	 * ユーザー名
	 */
	const USER_NAME = 'root';

	/**
	 * パスワード
	 */
	const PASSWORD = 'password';

	/**
	 * PDOインスタンス
	 * @var \PDO
	 */
	static private $instance = null;

	/**
	 * コンストラクタ
	 * @access private
	 */
	private function __construct()
	{
		// 外部からインスタンス化できないように、private で宣言
	}

	/**
	 * インスタンスを取得
	 * @return \PDO
	 */
	private static function getInstance()
	{
		if (is_null(self::$instance)) {
			$options = array(
				\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
				, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC
				, \PDO::ATTR_AUTOCOMMIT => true
			);
			self::$instance = new \PDO(
				sprintf(self::DSN, self::DBNAME)
				, self::USER_NAME
				, self::PASSWORD
				, $options
			);
		}
		return self::$instance;
	}

	/**
	 * クローン
	 * @throws \Exception
	 */
	final public function __clone()
	{
		$msg = sprintf('Clone is not allowed against %s', get_class($this));
		throw new \Exception($msg);
	}

	/**
	 * トランザクション実行
	 */
	public static function transaction()
	{
		self::getInstance()->beginTransaction();
	}

	/**
	 * コミット
	 */
	public static function commit()
	{
		self::getInstance()->commit();
	}

	/**
	 * ロールバック
	 */
	public static function rollback()
	{
		self::getInstance()->rollBack();
	}

	/**
	 * SELECT実行
	 * @param string $sql
	 * @param array $arr
	 * @return array
	 */
	public static function select($sql, array $arr = array())
	{
		$stmt = self::getInstance()->prepare($sql);
		$stmt->execute($arr);
		return $stmt->fetchAll();
	}

	/**
	 * INSERT実行
	 * @param string $sql
	 * @param array $arr
	 * @return int
	 */
	public static function insert($sql, array $arr)
	{
		if (!self::getInstance()->inTransaction()) {
			throw new \Exception('Not in transaction!');
		}
		$stmt = self::getInstance()->prepare($sql);
		$stmt->execute($arr);
		return self::getInstance()->lastInsertId();
	}

	/**
	 * UPDATE実行
	 * @param string $sql
	 * @param array $arr
	 * @return bool
	 */
	public static function update($sql, array $arr)
	{
		if (!self::getInstance()->inTransaction()) {
			throw new \Exception('Not in transaction!');
		}
		$stmt = self::getInstance()->prepare($sql);
		return $stmt->execute($arr);
	}

	/**
	 * DELETE実行
	 * @param string $sql
	 * @param array $arr
	 */
	public static function delete($sql, array $arr)
	{
		if (!self::getInstance()->inTransaction()) {
			throw new \Exception('Not in transaction!');
		}
		$stmt = self::getInstance()->prepare($sql);
		return $stmt->execute($arr);
	}

}
```