PHPプログラミング

最終更新日:
公開日:

ワークショップ

ひと言掲示板を作る(11)

投稿データをデータベースに登録する

「ひと言掲示板」に投稿されたデータを、ファイルへの保存からデータベースに登録するように改修していきます。

この記事のポイント

  • PDOを使ってMariaDBにデータを登録する
  • データベースの処理はプリペアドステートメントを使用する
  • トランザクションを実装する

目次

データベースにデータを登録する

前回はphpMyAdminからMariaDBを操作し、データベース「board」とテーブル「message」を作成してきました。
今回はこのテーブルに、掲示板に投稿されたメッセージを登録していくように設定してきます。

「ひと言掲示板を作る」の概要については「ひと言掲示板を作る」をご覧ください。
デモはこちら

前回までに作成したコードはこちら:Github

データベースに接続する

データベースを使用する手順は次のようになります。

  • データベースに接続
  • データベースにクエリ(読み込み&書き込み)を送る
  • 接続を閉じる

流れ自体はファイル操作と非常によく似ています。

「ファイルを開く」が「データベースの接続」に置き換わり、最後に「ファイルを閉じる」と同じように「データベースの接続を閉じる」必要があります。
接続した後(ファイルを開いた後)に読み込みや書き込みの処理が入るのも同じです。
つまり、ファイル操作と同じような感覚で手軽に操作することができます。

データベースへのアクセス方法

PHPではデータベースへのアクセス方法がいくつか用意されていますが、代表的なものの1つに「PDO(PHP Data Object)」があります。
PDOはPHPから各種データベースを操作するために用意されたクラスで、今回のようにMariaDBやMySQLをはじめ、その他のデータベースを使う際にも(ほぼ)共通の方法でデータベースを操作することができる便利な機能を提供します。
当ワークショップではデータベースの操作をするときはPDOを使っていきます。

それでは早速、データベースに接続を行ってみましょう。
まずは、既存のファイルへの書き込み操作に関するコードを次のようにコメントアウトしてください。

index.php

<?php

-- 省略 --

if( !empty($_POST['btn_submit']) ) {
	
	// 表示名の入力チェック
	if( empty($_POST['view_name']) ) {
		$error_message[] = '表示名を入力してください。';
	} else {
		$clean['view_name'] = htmlspecialchars( $_POST['view_name'], ENT_QUOTES, 'UTF-8');
		$clean['view_name'] = preg_replace( '/\\r\\n|\\n|\\r/', '', $clean['view_name']);
	}
	
	// メッセージの入力チェック
	if( empty($_POST['message']) ) {
		$error_message[] = 'ひと言メッセージを入力してください。';
	} else {
		$clean['message'] = htmlspecialchars( $_POST['message'], ENT_QUOTES, 'UTF-8');
		$clean['message'] = preg_replace( '/\\r\\n|\\n|\\r/', '
', $clean['message']); } if( empty($error_message) ) { /* コメントアウトする if( $file_handle = fopen( FILENAME, "a") ) { // 書き込み日時を取得 $current_date = date("Y-m-d H:i:s"); // 書き込むデータを作成 $data = "'".$clean['view_name']."','".$clean['message']."','".$current_date."'\n"; // 書き込み fwrite( $file_handle, $data); // ファイルを閉じる fclose( $file_handle); $success_message = 'メッセージを書き込みました。'; } ここまでコメントアウト */ } } -- 省略 --

続いて、PDOのオブジェクトを作成してMariaDBのデータベースに接続を行います。
PDOはオブジェクトを作成するときにデータベースの情報(ホスト名、データベース名、ユーザー名、パスワードを渡すことで接続を行うことができます。

以下の赤字になっているコードを記述してください。

index.php

<?php

// メッセージを保存するファイルのパス設定
define( 'FILENAME', './message.txt');

// タイムゾーン設定
date_default_timezone_set('Asia/Tokyo');

// 変数の初期化
$current_date = null;
$data = null;
$file_handle = null;
$split_data = null;
$message = array();
$message_array = array();
$success_message = null;
$error_message = array();
$clean = array();
$pdo = null;
$stmt = null;
$res = null;
$option = null;

// データベースに接続
$pdo = new PDO('mysql:charset=UTF8;dbname=board;host=localhost', 'root', 'password');

if( !empty($_POST['btn_submit']) ) {
	
	-- 省略 --

	if( empty($error_message) ) {

		/* コメントアウトする
		if( $file_handle = fopen( FILENAME, "a") ) {
	
		    // 書き込み日時を取得
			$current_date = date("Y-m-d H:i:s");

			// 書き込むデータを作成
			$data = "'".$clean['view_name']."','".$clean['message']."','".$current_date."'\n";
		
			// 書き込み
			fwrite( $file_handle, $data);
		
			// ファイルを閉じる
			fclose( $file_handle);
	
			$success_message = 'メッセージを書き込みました。';
		}
		ここまでコメントアウト */
	}
}

-- 省略 --

PDOのオブジェクトを作成するとき3つのパラメータを指定しています。
その内訳は以下の内容です。

  1. 第1パラメータ – 使用するデータベースのドライバ「mysql」、文字コード「charset=utf8」、データベース名「board」、ホスト「localhost
  2. 第2パラメータ – ユーザー名「root
  3. 第3パラメータ – パスワード「password

特に第1パラメータは複数の情報が混在しているため注意してください。
ホスト、ユーザー名、パスワードはそれぞれの開発環境によって異なると思いますので置き換えてください。

データベースの接続に成功するとPDOクラスのインスタンスが$pdoに入ります。

続いて、データベースに接続できなかったときの対応を用意します。
接続できないときの原因は接続情報が間違っていたり、データベース自体が起動していないなど様々ですが、PDOでは接続に失敗するとPDOException による例外をスロー(発生)させます。
そこで、この例外を受け取ることができるようにtry文を追加しましょう。

index.php

<?php

// メッセージを保存するファイルのパス設定
define( 'FILENAME', './message.txt');

// タイムゾーン設定
date_default_timezone_set('Asia/Tokyo');

// 変数の初期化
$current_date = null;
$data = null;
$file_handle = null;
$split_data = null;
$message = array();
$message_array = array();
$success_message = null;
$error_message = array();
$clean = array();
$pdo = null;
$stmt = null;
$res = null;
$option = null;

// データベースに接続
try {
    $pdo = new PDO('mysql:charset=UTF8;dbname=board;host=localhost', 'root', 'password');

} catch(PDOException $e) {

    // 接続エラーのときエラー内容を取得する
    $error_message[] = $e->getMessage();
}

if( !empty($_POST['btn_submit']) ) {

-- 省略 --

データベースに正常に接続できた場合はtry{〜}の部分のみが実行されます。

もし何かしらの原因で接続に失敗したときのみ、例外がスロー(発生)されてcatch{〜}の部分が実行されます。
PDOではエラーが起こるとPDOExceptionオブジェクトが発生しますが、ここでは変数$eで受け取って、getMessageメソッドよりエラーメッセージを取得しています。

エラーメッセージはバリデーションの時に作成した$error_messageと同じ変数に加えて、次のように表示します。
(PHPのエラー表示設定によっては、ページ上部に「Warning」メッセージが表示されます。)

エラーの表示例

エラーにはいくつか種類があります。
例えば、上のような「2002」エラーはデータベースが起動していないか、「ホスト名」が間違っていてデータベースが見つからないときに出るエラーです。

次のような「1045」エラーは、データベースのユーザー情報が間違っているときに表示されます。
この場合は、上記コードのうちユーザー名とパスワードの組み合わせが正しいか確認する必要があります。

ユーザー情報関係のエラー

1049」エラーは指定したデータベースが存在しない場合に表示されます。
この場合は、上記コードのうちデータベース名を指定する「board」が正しいかもしくはphpMyAdminなどでデータベースが存在するかを確認する必要があります。

データベース名のエラー

ここまででデータベースへの接続は可能ですが、ここでセキュリティ対策の一環としてオプションの指定を追記します。
以下のように$optionに設定するオプションを入れて、PDOのオブジェクトを作成するときに第4パラメータとして$optionを渡します。

index.php

<?php

// メッセージを保存するファイルのパス設定
define( 'FILENAME', './message.txt');

// タイムゾーン設定
date_default_timezone_set('Asia/Tokyo');

// 変数の初期化
$current_date = null;
$data = null;
$file_handle = null;
$split_data = null;
$message = array();
$message_array = array();
$success_message = null;
$error_message = array();
$clean = array();
$pdo = null;
$stmt = null;
$res = null;
$option = null;

// データベースに接続
try {

	$option = array(
		PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
		PDO::MYSQL_ATTR_MULTI_STATEMENTS => false,
	);
	$pdo = new PDO('mysql:charset=UTF8;dbname=board;host=localhost', 'root', 'password', $option);

} catch(PDOException $e) {

	// 接続エラーのときエラー内容を取得する
	$error_message[] = $e->getMessage();
}

if( !empty($_POST['btn_submit']) ) {

-- 省略 --

$optionでは2つの属性を設定していますが、以下で内容を解説していきます。
現時点で全てを理解したり完璧に覚える必要はないので、力まずに「こういう設定もできるんだな」程度で目を通していただけると幸いです。

属性設定する内容
PDO::ATTR_ERRMODE
  • PDO::ERRMODE_SILENT
  • PDO::ERRMODE_WARNING
  • PDO::ERRMODE_EXCEPTION
PDOを実行しているときにエラーが起こった時の挙動を設定
PDO::MYSQL_ATTR_MULTI_STATEMENTS
  • true、または1
  • false、または0
PDOのprepareメソッドqueryメソッドにおいて、マルチクエリ(1度に複数のクエリを実行すること)を許可するか設定

PDO::ATTR_ERRMODE

PDO::ATTR_ERRMODEはエラーが起こったときの挙動を3つの値から指定します。
PHP 8.0.0より前のバージョンでは「PDO::ERRMODE_SILENT」が初期値となっており、エラーが起こった時に例外をスロー(発生)するのはデータベース接続時のみでした。
そこで、今回は「PDO::ERRMODE_EXCEPTION」を指定してPDO実行中のエラーはデータベース接続時以外でも例外をスローするように設定しています。
PHP 8.0.0以降のバージョンでは今回指定した「PDO::ERRMODE_EXCEPTION」が初期値になっているため、もしお使いのPHPが8.0.0以降のバージョンであればこの属性のオプションは指定する必要はありません。

PDO::MYSQL_ATTR_MULTI_STATEMENTS

PDO::MYSQL_ATTR_MULTI_STATEMENTSはPDOでprepareメソッドqueryメソッドを実行するときに、一度に複数のクエリを実行するか設定することができます。
ここでは「false」を指定してマルチクエリは不可にしています。
初期値は「1(true)」でマルチクエリを許可する設定となっていますが、今回はオプションで不可とすることで意図しない複数のSQLの実行を防ぐことができます。

データを登録してみる

データベースに接続できたら、続いて投稿データを登録するコードを記述していきましょう。
次の赤字のコードを追記してください。

index.php

<?php

-- 省略 --

if( !empty($_POST['btn_submit']) ) {
	
	-- 省略 --

	if( empty($error_message) ) {

		/* コメントアウトする
		if( $file_handle = fopen( FILENAME, "a") ) {
	
		 	// 書き込み日時を取得
			$current_date = date("Y-m-d H:i:s");

			// 書き込むデータを作成
			$data = "'".$clean['view_name']."','".$clean['message']."','".$current_date."'\n";
		
			// 書き込み
			fwrite( $file_handle, $data);
		
			// ファイルを閉じる
			fclose( $file_handle);
	
			$success_message = 'メッセージを書き込みました。';
		}
		ここまでコメントアウト */


		// 書き込み日時を取得
		$current_date = date("Y-m-d H:i:s");

		// SQL作成
		$stmt = $pdo->prepare("INSERT INTO message (view_name, message, post_date) VALUES ( :view_name, :message, :current_date)");

		// 値をセット
		$stmt->bindParam( ':view_name', $clean['view_name'], PDO::PARAM_STR);
		$stmt->bindParam( ':message', $clean['message'], PDO::PARAM_STR);
		$stmt->bindParam( ':current_date', $current_date, PDO::PARAM_STR);

		// SQLクエリの実行
		$res = $stmt->execute();
		
		if( $res ) {
			$success_message = 'メッセージを書き込みました。';
		} else {
			$error_message[] = '書き込みに失敗しました。';
		}
		
		// プリペアドステートメントを削除
		$stmt = null;
	}
}

// データベースの接続を閉じる
$pdo = null;

-- 省略 --

上から順にコードを解説していきます。

まず、「// 書き込み日時を取得」の箇所はファイルに登録するときと同じく投稿を登録したタイミングの日時を取得して、$current_dateに入れておきます。

続く「// SQL作成」から「// SQL作成」の箇所までが実際にデータを登録するコードになります。

// SQL作成
$stmt = $pdo->prepare("INSERT INTO message (view_name, message, post_date) VALUES ( :view_name, :message, :current_date)");

// 値をセット
$stmt->bindParam( ':view_name', $clean['view_name'], PDO::PARAM_STR);
$stmt->bindParam( ':message', $clean['message'], PDO::PARAM_STR);
$stmt->bindParam( ':current_date', $current_date, PDO::PARAM_STR);

// SQLクエリの実行
$res = $stmt->execute();

ここでは「プリペアドステートメント」という機能を使い、次の3ステップでデータを登録します。

  1. (1)SQL作成
  2. (2)SQLのプレースホルダーにデータをセット
  3. (3)実行

プリペアドステートメントを使う理由は、1つはSQLインジェクション対策です。
入力データを自動的に文字列定数として組み込むことができるため、記号が含まれていてもただの文字として扱うことができるようになります。
そのため、ファイル登録ではhtmlspecialchars関数でサニタイズを行なっていましたが、データベースに登録するタイミングでは不要になります(後ほど削除します)。

また、通常のSQL文を作成するときは値を「(クォート)」で囲む必要がありますが、これもプリペアドステートメントでは自動的に行なってくれるため不要になります。

データを登録する3つのステップに話しを戻しましす。
まずprepareメソッドを使って(1)SQLを作成します。
ここで、新しく登録するデータはSQLには直接含めません。
その代わり、「:view_name」のように「:(コロン)」から始まる「プレースホルダー」を設置します。
prepareメソッドを実行するとPDOStatementオブジェクトのインスタンスが作成されるため、変数$stmtで受け取ります。

続いて、先ほど取得したPDOStatementオブジェクトのインスタンスが持つbindParamメソッドを使って、(1)で作成したSQLのプレースホルダーにデータをセットしていきます。
今回は表示名、ひと言メッセージ、登録日時の3つのデータをセットします。
なお、bindParamメソッドの3つ目のパラメータで指定しているPDO::PARAM_STRはPDOの定数で、セットする値が文字列であることを明示的に指定しています。

最後にPDOStatementオブジェクトexecuteメソッドを実行すると、上記の3つのデータがデータベースに新しい投稿データとして登録されます。
executeメソッドはクエリが成功するとtrue、失敗したらfalseを返り値にするため、変数$resで受け取ります。

$resに入っている値をif文で確認して、表示するメッセージをそれぞれ作成します。

if( $res ) {
	$success_message = 'メッセージを書き込みました。';
} else {
	$error_message[] = '書き込みに失敗しました。';
}

登録の処理が終了したら、$stmt$pdoに入っているオブジェクトを明示的にnullで空にすることで、データベースとの接続を解除します。

// プリペアドステートメントを削除
$stmt = null;
// データベースの接続を閉じる
$pdo = null;

以上でデータベースへのデータ登録ができるようになりました。
試しにメッセージを投稿してみましょう。

掲示板で書き込みを行う

正常に書き込まれると、ファイル書き込みのときと同様に「メッセージを書き込みました」と表示されます。

正常に書き込みが行われたときの例

続いて、phpMyAdminから「message」テーブルの「表示」タブを開いてみてください。

phpMyAdminから登録データを確認

このように投稿したメッセージが表示されたら成功です。
テーブルに登録されたデータは「表示」からいつでも確認することができます。

トランザクションを実装する

ここまでで投稿データを登録できるようになりましたが、もう少し踏み込んでデータベースのトランザクションを実装していきます。

まずはデータベースの「トランザクション」についてざっくりとご紹介します。

データベースにおけるトランザクションは、複数のデータ更新処理(参照を除く新規登録、更新、削除)を「コミット(処理実行)」の命令があるまで仮実行(下書きのようなイメージ)に留めておき、コミットの命令が出された時に一括で決定処理を行います。

トランザクションのイメージ
トランザクションのイメージ

トランザクション処理を行なっている間は、「コミット(処理実行)」か「ロールバック(処理取り消し)」のいずれかが行われるまでは他のトランザクション処理を受け付けません。
そのため、データベースの同時アクセスによるデータの行き違い(データの不整合)を未然に防ぐことができ、適切なタイミングでトランザクション処理を行うことでシステムの堅牢性を向上させる効果があります。

簡単にではありましたが、以上がトランザクションの機能になります。

早速、先ほどまでのデータ登録のコードにトランザクションを追加していきましょう。
「// SQL作成」から「// SQL作成」をトランザクションのtry文で囲むようにします。
次の赤字のコードを追記してください。

index.php

<?php

-- 省略 --

if( !empty($_POST['btn_submit']) ) {
	
	-- 省略 --

	if( empty($error_message) ) {

		/* コメントアウトする
		if( $file_handle = fopen( FILENAME, "a") ) {
	
		 	// 書き込み日時を取得
			$current_date = date("Y-m-d H:i:s");

			// 書き込むデータを作成
			$data = "'".$clean['view_name']."','".$clean['message']."','".$current_date."'\n";
		
			// 書き込み
			fwrite( $file_handle, $data);
		
			// ファイルを閉じる
			fclose( $file_handle);
	
			$success_message = 'メッセージを書き込みました。';
		}
		ここまでコメントアウト */


		// 書き込み日時を取得
		$current_date = date("Y-m-d H:i:s");

		// トランザクション開始
		$pdo->beginTransaction();

		try {

			// SQL作成
			$stmt = $pdo->prepare("INSERT INTO message (view_name, message, post_date) VALUES ( :view_name, :message, :current_date)");

			// 値をセット
			$stmt->bindParam( ':view_name', $clean['view_name'], PDO::PARAM_STR);
			$stmt->bindParam( ':message', $clean['message'], PDO::PARAM_STR);
			$stmt->bindParam( ':current_date', $current_date, PDO::PARAM_STR);

			// SQLクエリの実行
			$stmt->execute();

			// コミット
			$res = $pdo->commit();

		} catch(Exception $e) {

			// エラーが発生した時はロールバック
			$pdo->rollBack();
		}
		
		if( $res ) {
			$success_message = 'メッセージを書き込みました。';
		} else {
			$error_message[] = '書き込みに失敗しました。';
		}
		
		// プリペアドステートメントを削除
		$stmt = null;
	}
}

// データベースの接続を閉じる
$pdo = null;

-- 省略 --

トランザクションの開始はPDOのbeginTransactionメソッドで行います。
このメソッドが実行されると、PDOで登録したデータはcommitメソッドが呼び出されるまでデータベースに反映されません。

try文の中では今まで作成したデータの登録に加えて、commitメソッドを追記しています。
先ほどまでは変数$resexecuteメソッドの返り値を受け取っていましたが、ここでcommitメソッドの返り値を受け取るように変更しています。

無事にcommitメソッドが実行されたらtry文を抜けて、表示するメッセージを判定するif文に進みます。

もしtry文の途中で不具合が起こった時(例外が発生したとき)はcatch文に移動して、rollBackメソッドを実行します。
このメソッドが実行されるとデータの登録は取り消しされ、データベースはトランザクション開始前の状態に戻ります。

ここまででトランザクションを実装することができました。
データ取得のみの操作のときは不要ですが、今後もデータを更新、削除するときは同じ流れでトランザクションを実装していきます。

コードを調整する

データの保存先をファイルからデータベースに切り替えることができました。
最後に、サニタイズ周りのコードを今回の変更にあわせて調節しましょう。

index.php

<?php

-- 省略 --

if( !empty($_POST['btn_submit']) ) {

	// 空白除去
	$view_name = preg_replace( '/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $_POST['view_name']);
	$message = preg_replace( '/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $_POST['message']);

	// 表示名の入力チェック
	if( empty($view_name) ) {
		$error_message[] = '表示名を入力してください。';
	}

	// メッセージの入力チェック
	if( empty($message) ) {
		$error_message[] = 'ひと言メッセージを入力してください。';
	}

	if( empty($error_message) ) {

		/* コメントアウトする
		if( $file_handle = fopen( FILENAME, "a") ) {
	
		    // 書き込み日時を取得
			$current_date = date("Y-m-d H:i:s");
		
			// 書き込むデータを作成
			$data = "'".$clean['view_name']."','".$clean['message']."','".$current_date."'\n";
		
			// 書き込み
			fwrite( $file_handle, $data);
		
			// ファイルを閉じる
			fclose( $file_handle);
	
			$success_message = 'メッセージを書き込みました。';
		}
		ここまでコメントアウト */
		
		// 書き込み日時を取得
        $current_date = date("Y-m-d H:i:s");

        // トランザクション開始
        $pdo->beginTransaction();

        try {

            // SQL作成
            $stmt = $pdo->prepare("INSERT INTO message (view_name, message, post_date) VALUES ( :view_name, :message, :current_date)");

            // 値をセット
            $stmt->bindParam( ':view_name', $view_name, PDO::PARAM_STR);
            $stmt->bindParam( ':message', $message, PDO::PARAM_STR);
            $stmt->bindParam( ':current_date', $current_date, PDO::PARAM_STR);

            // SQLクエリの実行
            $res = $stmt->execute();

            // コミット
            $res = $pdo->commit();

        } catch(Exception $e) {

            // エラーが発生した時はロールバック
            $pdo->rollBack();
        }

        if( $res ) {
            $success_message = 'メッセージを書き込みました。';
        } else {
            $error_message[] = '書き込みに失敗しました。';
        }

        // プリペアドステートメントを削除
        $stmt = null;
	}
}

-- 省略 --

「// 空白除去」では、正規表現を使って入力された「表示名」と「ひと言メッセージ」の前後にある空白(スペース、タブ、リターン)を取り除きます。
もし入力が空白のみだった場合は入力値が空になるため、未入力チェックでも引っ掛かるようになります。

\p{C}」と「\p{Z}」はUnicodeの文字プロパティと呼ばれるものです。
\p{C}」は「コントロール文字」「非可視整形用文字」などを含む「その他」の文字、「\p{Z}」は「行区切り文字」「段落区切り文字」「空白文字」などの「区切り文字」を指しています。

preg_replace関数を使って、入力値の先頭、または末尾のいずれかに空白文字があるときは「(空白)」に置き換えることで、空白を取り除きます。
この処理についてより詳しく知りたい方は、別記事「文字列の前後にある空白文字(スペース・タブ)を取り除く」を参照ください。

以降の未入力チェックやデータベース周りの処理では、不要な空白を取り除いた入力値が入っている$view_name$messageを使うように変更しています。
また、プリペアドステートメントを使うことでデータ登録の際のサニタイズ処理は不要になったためelse文ごと削除します。

今回はここまでになります。
データベースに書き込みを行えるようになりましたが、次回は書き込んだデータを読み込んで表示していきます。

今回作成したコード:Github

こちらの記事は役に立ちましたか?

ありがとうございます。
もしよろしければ、あわせてフィードバックや要望などをご入力ください。

コメントありがとうございます!
運営の参考にさせていただきます。