【2019年版】パーフェクトPHP解説その４ 6章 PDOによるMySQLの利用　接続編

学習開始80日目！

[前回](https://qiita.com/untrustworth/items/bb01016baeaaadbfbae2)作成した投稿フォームから投稿したデータをデータベースに保存する仕組みを作ります。
本文でいうと、181ページ、見出し6.2.6に該当します。

しかし、まさか本文記載の方法が古くて廃止されていたため、新しいやり方を一から解説しますので、２記事に分けることにしました。

今回はPHPとMySQLの接続確立と、エラー（例外）設定について扱います。
POSTで送られてきたデータの保存については次回にします。

# 6.2.6 保存処理の作成
送信ボタンによって　フォームの内容がbbs.phpに送信されるのですが現状データベースに保存されません。
bbs.phpにデータを保存する機能を書き加えます。

## 6.2.6-1 データ保存の流れ
データベースの保存の流れはパーフェクトPHP本文によれば下記のようになります。

① MySQLに接続しoneline_bbsデータベースを選択する。
② エラーを格納する$error変数を初期化する。
③ リクエストメソッドがPOSTかを判定する。
④ 入力された内容のバリデーションを行う。
⑤ バリデーションエラーがないかを判定する。 
⑥ SQLのINSERT文を作成する。
⑦ SQLを実行し、保存する。

しかし、だいぶ古いようで、現在は本文記載の方式は廃止されています。
今回は、上記①〜②部分について、バージョンアップしていきます。

## 6.2.6-2 【エラー】PHPとMySQLの接続方式について
PHP7.0より`mysql_***`系の関数は削除されました。
なので本文の関数は全部使えませんので、調べながら書くしかありません。

PHP7.x系にはMySQL接続用のAPIが2つ用意されています。
PDO方式とMySQLiです。この違いについては[公式リファレンス](https://www.php.net/manual/ja/mysqlinfo.api.choosing.php)に詳しいです。
APIとは「Application Programming Interface」の略で、外部からあるプログラムを使用するための窓口、仕事の依頼の方法という感じです。
今回で言えば、PHPからMySQLを利用するための窓口＋仕様書がAPIです。

今回はこのうちのPDO方式を選択します。
PDO方式はPHPと複数のデータベースマネジメントサービスをつなぐ抽象レイヤの役割をするので、MySQL以外のDBMSを使っていても、ほとんど同じ書き方で対応できるようです。
MySQLiは名前のとおりMySQLでしか使えません。

## 6.2.6-3 PDO方式とは
PHP Data Objectsの略です。公式リファレンスは[こちら](https://www.php.net/manual/ja/book.pdo.php) です。
SQLite、MySQL、PostgreSQLなど、複数のデータベースをほぼ同じコードで使えるようにしてくれます。
汎用的な技術なので、初心者にオススメのようです。

手元に体系だった参考書などもありませんので手探りで調べていきます。
参考にさせていただいたサイトのリンクを最後に掲載します。

### 6.2.6-3.1 PDOの定義済みクラス
クラス形式と言われてもピンと来ないかもしれません。
PDOには、`PDO`クラス、`PDOStatement`クラス、`PDOException`クラスという3つの定義済みクラスがあります。

`PDO`クラスは「PHP とデータベースサーバーの間の接続」を表します。
`PDOStatement`クラスは「プリペアードステートメント」に関するメソッドが記述されています。
プリペアードステートメントについて詳しくは後述しますが、データベースを操作する命令文の虫食いのテンプレートのようなもので、先にデータベースに送られます。その後、虫食い部分の値を別にデータベースに送って虫食いに当てはめることで、命令文を実行します。具体的には、想定外の操作を回避するセキュリティ目的で使われます。
ステートメント実行後は、`PDOStatement`クラスのインスタンスに結果セットが格納されます。
結果セットとは、データベースから取得されたデータが一時的に置かれる仮想テーブルのことです。
`PDOExceotion`クラスは「エラー」を表す変数とメソッドが記述されています。
PDOでは必ず例外の`try`ブロック内に処理を記述しますので、このクラスを用いて例外を`catch`することになります。
例外の書き方についても後述します。

PDO方式では基本的に上記の3つのクラスのインスタンスを作成して、クラスの定義済みメソッドを呼び出しながら、データベースを扱っていきます。

## 6.2.6-4 PHPとデータベースとの接続方法の解説
### 6.2.6-4.1 接続はPDOクラスのインスタンス化
データベースとの接続関係の処理は「PDOクラス」で行います。
公式によれば
>PDO 基底クラスのインスタンスを作成することにより、接続が確立されます。
コードで書くと次のとおりです。

```php:
$dbh = new PDO('DSN','ユーザー名','パスワード',オプション);
```

dbhとはデータベースハンドラの略です。
DSNとはデータソースネームの略で、「DBのサーバー名」、「データベース名」、「文字コード」のセットです。

####  文字コードとは？
なお、文字コードとは「文字集合を定義し、その集合の各文字に対応するビット組み合わせを一意に定めたもの」のことで、要はある文字をどのビットが表すかを定めた定義集です。
後述しますが、特定の文字コードではある文字を表すビットの組み合わせの一部が、他の文字コードやHtmlなどで別の何かを表してしまうケースがあり、これを利用してプログラムを誤作動させて情報を抜き取る「SQLインジェクション」という攻撃があります。
これを回避するため、文字コードは必ず「utf8mb4」を用います。

では次に、具体的にインスタンスを生成するために、自分の環境のこれらの情報を取得しましょう。

### 6.2.6-4.2 DSN等の値の確認
ホスト名などは下記のコマンドを使って確認できます。

```
mysql> select HOST,User,Password from mysql.user;
| HOST | User | Password |
+-----------+------+-------------------------------------------+
| localhost | root | *** |
| 127.0.0.1 | root | *** |
| ::1 | root | ***|
mysql> quit
```

3段出てきますがどれも同じことを意味しています。
ただし、MySQLにおいては「localhost(ホスト名)」と「127.0.0.1(IPアドレス)」を用いるのとでは接続方法が異なります。
ホスト名接続ではUnix ソケットを使用して接続しますが、IPアドレスではTCP/IPを用いることになります。
どうやら、my.cnf に skip-networking (TCPポートを無効化) を設定している環境では localhost でないと接続できないらしく、ホスト名での接続としておくのが確実なようです。

### 6.2.6-4.3  `$driver_options`について
上のPDOインスタンスにおけるオプションというやつです。
ここには、接続時のオプションを連想配列で入れられます。

```php:
array(
        変更したい属性 => 値,
        変更したい属性 => 値,
    )
```

こんな感じです。
ちなみにオプションはインスタンス化した後でも設定可能です。

```php:
$dbh->setAttribute(変更したい属性 , 値);
$dbh->setAttribute(変更したい属性 , 値);
```

ただし、コンストラクタ内でしか使用できないオプションもあるので、理由がない限りコンストラクタに連想配列で設定しておきます。
今回使用するオプションは2つです。

- `PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION` エラー発生時に`PDOException`を投げてくれるようになります。
- `PDO::ATTR_EMULATE_PREPARES => false` 静的プレースホルダを動的プレースホルダにエミュレートする機能をオフにします。
プレースホルダについては次回説明します。

では接続のためのPDOをインスタンス化するコードを次項で紹介します。

## 6.2.6-5 PDOクラスを実際にインスタンス化する。
6.2.6-4で確認したものを合体させると、下記のようなコードになります。

```php:
// コンストラクタの引数に渡す変数を定義
$dsn = 'mysql:host=localhost;dbname=oneline_bbs;charset=utf8mb4';
$username = 'root';
$password = '***’; // パスワードは各自の環境の値を入れてください。

// インスタンス化します
$dbh = new PDO(
  $dsn,
  $username,
  $password
  $array(
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    PDO::ATTR_EMULATE_PREPARES => false
  )
);
```

変数については直に入れるよりも、外で定義して、引数に渡す方が良さそうですね。

## 6.2.6−6 例外
今回は` PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION`のオプションによってPDOのエラーは全て例外としてキャッチするようにしていますので、例外を用いた記述に直す必要があります。

### 6.2.6-6.1 例外とは？
例外とは、エラー処理の方法の１つです。
ほかに、if文で条件分岐した先にエラー時の処理を直接書くことも可能です。

#### 例外の構文
構文は下記のとおりです。

```php:
try {
    // 例外が発生する可能性があるコード
    
    // エラーが起きるので例外処理したい場合例外クラスのインスタンスを投げます。
    throw new 例外クラス名(引数)

} catch(例外クラス名　例外クラスのインスタンスを格納する変数名){
    //例外時の処理
}
```

#### 例外処理では何が行われているか？
まず`try{}`内の処理が行われていきます。
そしてエラーとなるような箇所には`throw new 例外`によって例外クラスがインスタンス化されるようにしておきます。これを「例外を投げる」と言います。
例外を投げると処理は中断し、一気に`catch()`まで進みその下に書かれた例外時の処理が実行されます。
例外発生以降の処理は行われないので、エラーの有無に関わらず必須の処理がある場合は、例外より上に書くようにしてください。

ちなみに、「例外」と言っているモノは、`Exception`クラス（かそのサブクラス）のインスタンスです。このクラスにはエラー時に行うメソッドが定義されていて、インスタンス化することで呼び出すことができるようになります。
[ここにPDOException](https://www.php.net/manual/ja/class.pdoexception.php)クラスのソースコードが載っているので、メソッド一覧を見ておくといいでしょう。
投げられた例外インスタンスは、`catch`というメソッドによって捕捉されます。第一引数のクラスのインスタンスを第二引数の変数に代入する処理です。
このインスタンス変数から例外時のメソッドを呼び出して、`catch()`以下に例外時の処理を記述します。

#### 例外のメリットデメリット
例外の利点は、まず、エラーが発生するとそれ以降の処理は自動的に中断される点です。エラーが起きた場合にプログラムが止まってくれないと困る場合に便利です。
次に、例外が起きなかった場合の処理と、例外後の処理を分けて書けることです。
ifの分岐先にずらずらっとエラー時の処理を書くよりも単に見やすく、また条件分岐などで間違いを減らしやすいです。

一方でデメリットとして、これはエラー、これは動作、これはエラー、これは処理、というように、プログラムを上から順に全部処理して、それぞれエラーの場合と動作する場合の結果を出力したいような時には適応しません。
エラー時点で中断してしまうからです。
必要に応じて使い分けましょう。

### 6.2.6-6.2 エラー構文を用いてデータベースに接続する
では実際に、先ほどのコードにエラー構文を加えてみます。

```php:
<?php

$dsn = 'mysql:host=localhost;dbname=oneline_bbs;charset=utf8mb4';
$username = 'root';
$password = '***’;

// try構文を始めます。
try{
  $dbh = new PDO(
    $dsn,
    $username,
    $password
    $array(
      // throw new と明示していないのですが、このオプションでエラー時に例外を投げるように設定されます。
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
      PDO::ATTR_EMULATE_PREPARES => false
    )
  );
  // 接続できた場合は、そのようにメッセージを出します。
  echo "接続成功\n";

        /* データベースから値を取ってきたり， データを挿入したりする処理は`try{}`内に書きます */

// エラー発生時は処理を中断し、`catch()` に飛び、PDOExceptionクラスのインスタンスを$eに代入します。
} catch (PDOException $e) {
// エラーが発生した場合「接続失敗: エラーメッセージ」と表示する。
  echo "接続失敗: " . $e->getMessage() . "\n";
  exit();
} 
```

## 参考URL
[【PHP超入門】クラス～例外処理～PDOの基礎](https://qiita.com/7968/items/6f089fec8dde676abb5b)
非常に詳しく、紹介されている参考サイトも非常に有用でした
[PHPでデータベースに接続するときのまとめ](https://qiita.com/mpyw/items/b00b72c5c95aac573b71)
こちらもとても詳細ですが、無知識だと大変だったので、上のサイトと公式マニュアルで基礎を学んでから見るとわかりやすかったです。
[【モダンなPHP】PHPでPDOを使ってMySQLに接続する方法！](https://blog.codecamp.jp/programming-php-pdo-mysql-1)
PDOのコンストラクタに与える引数をtrycatch構文の外で変数として定義する書き方が書かれていたのをみていいなと思いました。
[PHP Labo](http://www.php-labo.net/tutorial/class/pdo.html)
色々詳しいチュートリアルサイト？です。ちょっと情報が古いのかも？
[Cloud9で学ぶ！Webフォームでデータベースを操作する](https://programming-study.com/trouble/cloud9-database/) 
今の環境と同じcloud9での解説があって助かります。hostなどの調べ方や、次回解説するPOSTによるデータの受け渡し方法が書いてありました。

他にもたくさんのサイトをみましたが、内容重複が結構多いので、厳選させていただきました。


## 次回予告
次回は、POSTで送られてきたデータを保存するコードをPDO方式にアップデートしていきます。
SQLインジェクションやプレースホルダといった、セキュリティ面についても触れていきます。