PHP で MySQL 接続時に必要な知識(最小限版)

# はじめに
[mpyw](https://qiita.com/mpyw) さんの [PHPでデータベースに接続するときのまとめ - Qiita](https://qiita.com/mpyw/items/b00b72c5c95aac573b71) という記事をいつも愛読しているのですが、初心者向けにはハードルが高い気がするので、接続に必要な最小限の内容にまとめてみました。
最終的には**【php のヤッカイなクセ】**も含めて理解する必要がありますが、学習当初は以下のテンプレートを使用することで、最低限守らなければならないルールは満たせます。

# テンプレート
コードは、mpyw さんの記事を元に微調整しています。

```php:コピペ用テンプレート
try {
    $pdo = new PDO(
        'mysql:dbname=testdb;host=localhost;charset=utf8',//or charset=utf8mb4
        'user',
        'password',
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
//            PDO::MYSQL_ATTR_MULTI_STATEMENTS => false,
        ]
    );
    $stmt = $pdo->prepare('SELECT * FROM users WHERE age = :age AND gender = :gender');
    $stmt->bindValue(':age', (int)$age, PDO::PARAM_INT);
    $stmt->bindValue(':gender', $gender);
    $stmt->execute();
    $result = $stmt->fetchAll();
} catch (PDOException $e) {
    header('Content-Type: text/plain; charset=UTF-8', true, 500);
    exit($e->getMessage()); 
}
var_dump($result);
```
## 記述の前提
接続には PDO を使用します。
また、`PDO::ATTR_EMULATE_PREPARES` は `false`、使用する`charset`は`utf8`を想定しています。
(`utf8mb4`を利用するケースも多いです)

# 定石
## PDO::ATTR_ERRMODE
SQL実行でエラーが起こった際、例外を投げる `PDO::ERRMODE_EXCEPTION` を選択します。このモードでは、エラーが発生した時点で スクリプトの実行を停止させ、`catch `で、問題発生箇所特定のための情報を表示します。
*実用時は、`catch`の内部はもっと考慮して記述する必要があります。

## PDO::ATTR_DEFAULT_FETCH_MODE
結果セットにカラム名で添字を付けた配列を返す `PDO::FETCH_ASSOC` を選択します。他のモードも便利なので、理解が進めば試してください。 
参考：[PDOフェッチパターン大全](https://qiita.com/mpyw/items/d52351bd1a8068344cc2)

## PDO::ATTR_EMULATE_PREPARES
エミュレートを　`false`にするとは、いわゆる**静的プレースホルダ**を使用することを意味します。`true`にすると**動的プレースホルダ**です。
現行の php では`true`が default ですが、過去のバージョンでは`false`が default だった時期があります。挙動に差があるので明記しておくのが適切です。
動的プレースホルダはクセがあるので、当面静的プレースホルダ(`false`)を使用してください。
動的プレースホルダを使用する場合、複文を利用できないように`MYSQL_ATTR_MULTI_STATEMENTS => false`しておきましょう。
(使用できるバージョンが限定されるので、一応コメントにしています。7 系ならコメントを外してしまって良いです。)

参考1：[PDOに複文実行を禁止するオプションが追加されていた-徳丸浩の日記](https://blog.tokumaru.org/2016/12/pdo.html)

また、`PDO::ATTR_EMULATE_PREPARES`の【現時点での】セキュリティ的な意味合いに関して質問したところ、徳丸さんに回答いただけたので紹介しておきます。

参考2：[PDO::ATTR_EMULATE_PREPARES => false は必要か？](https://teratail.com/questions/233051)

## プリペアドステートメントを使おう
SQL 文に、直接変数を埋め込むのは、SQL インジェクションを引き起こす原因となります。

```php:NGパターン
$pdo->query('SELECT * FROM users WHERE age = ' . $age . ' AND gender = ' . $gender);
```

`PDO::prepare()` で、プリペアドステートメントを用意し、変数をバインドする方式を覚えてください。
**バインドされる値に対して、引用符は自動付加されるので、`PDO::prepare()`内に引用符は記述しないことに注意してください。**

## bindValue を使おう
プレースホルダに値をバインドするメソッドには、`PDOStatement::bindValue`と`PDOStatement::bindParam`がありますが、`bindParam`は SQL 実行時に値がバインドされるため、直感的に分かりにくいです。（[あと、副作用があったり＾＾；](https://qiita.com/_dozen_/items/e3c00a0a581378a4cc70)）慣れないうちは、`bindValue`を使用しましょう。

#おまけ
## PDO::PARAM_INT は罠 [^1]
PDO::PARAM_INT は、以下のような実装になっているようです。
[PDOでの数値列の扱いにはワナがいっぱい（2)](http://blog.a-way-out.net/blog/2013/12/18/pdo-prepare-statement-numeric-literal-part2/)

> PDO::PARAM_INTはboolをintに変換（他は何もしない）

つまり、(string)'15' は、15 にならないのです。
この場合、'15' を渡すのはそもそもケシカラン！って考え方もありますが、テンプレートでは (int) でキャストする方法を記述しています。

[^1]: PHP 7.2 以降のエミュレーション ON の場合、int へキャストされます。詳しくは[あの PDO::PARAM_INT の挙動が変更になってる！](https://qiita.com/te2ji/items/a8e7211a69f313126f7c)を参照してください。


## LIKE も気をつけよう
MySQL であれば、LIKE はワイルドカードとして`_%`が使用されます。
（ワイルドカード使用時にエスケープに使う文字のデフォルトは、データベースエンジンによって違うので注意！エスケープのための文字の指定もできるので興味があれば調べてみてください。）
単純にバインドすると`100%`等の文字列の検索が正しく行われません。
ワイルドカードをエスケープしてやる必要があります。

```php:MySQLの場合
$stmt = $pdo->prepare('SELECT * FROM users WHERE name LIKE ?');
$stmt->bindValue(1, '%' . addcslashes($name, '\_%') . '%', PDO::PARAM_STR);
```


# まとめ
慣れれば大した事ではないし、そのうちフレームワークの機能で DB を扱うようになるので、あまり役に立つ情報ではないのですが、初心者が躓くポイントなのでまとめました。

最初に書いたとおり「最小限」の記述なので、最終的には元記事をよく読んで理解すると良いです。