PHPでTODOリストを作ってみた(2) - TODOの追加

TODOリストを作ってみています。
ピンポイントの「TODOリストの作り方」記事は見ずに、できるかぎり調べて書いています。

前回はDBのテーブルの表示までできました。

次は「登録」ボタンを押してPOST送信したデータをテーブルに追加します。

4. SQL文でレコードの追加

いきなりどうすればいいのか分からないので調べました。

formタグから入力したデータをデータベースに登録 | CBC | Webデザインやプログラミングの基礎学習

【プログラミング構築】入力したデータをPOST送信でデータベースに登録しよう | 株式会社LIG

1つ目の記事を見ると、接続部分と追加部分、それぞれでtry/catch で囲っています。
2つ目の記事では追加部分はそのまま書かれています(クラスのメソッドになっている...複雑)。

ひとまずtry/catch で囲わずに書いてみます。
一旦一覧表示は非表示にします。

SQL INSERT文

INSERT文もすっかり忘れました。

INSERT文(SQLを基本から学ぶシリーズ)

INSERT INTO テーブル名 (列名1, 列名2,...) VALUES (値1, 値2,...);

こちらに倣って書こうと思いますが、VALUES の中は「パラメータをつかってバインドしている」ようです。
VALUES ($POST_['day-r'], $POST_['priority'],...) と、直接VALUES の中に POST送信のデータを入れるのは良くないということなのでしょう。

ここはまず固定値を入れてINSERTしてみます。

サーバーエラーになる

POST送信のタイミングでNSERTしてみましたが、500のエラーが返ってきました。

エラーになったコードです。

//TODO追加
if (!empty($_POST['content'])) {
  $sql  = 'INSERT INTO todo(id, registration-date, priority, content, note, period-date, category) VALUES(NULL, 2021-06-01, "中", "病院に行く", "10時に予約", 2021-06-05, "外出")';
  $stmt = $pdo->query($sql);
  $stmt->execute();
}

そこでtry/catchを入れてみます。

すると以下のエラーが返ってきました。

Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '-date, priority, content, note, period-date, category) VALUES(NULL, 2021-06-01, ' at line 1

貴重な手がかり!
どうやらSQLの構文ミスのようです。

「'id, registration-date...'付近」ではなく「'-date, ..' 付近」の構文を確認してください、というのが引っかかります。
そもそもハイフン -カラム名(フィールド名)に使えるのかどうか調べました。

データベース名にハイフン(-)入れたらSQL文でエラーになった 【特殊文字】 - Qiita

DB名やカラム名では極力ハイフンを使わない方がよいですね。

ハイフンを使う場合はバッククオート「`」で囲むエスケープ処理が必要だそうです。
また「table」などの予約語を使う場合もエスケープ処理が必要です。

なので、上記のSQL文はregistration-dateperiod-date をバッククオートで囲みました。

すると、無事にDBに追加されていました!
(ページ読み込みを何回かしたためか、たくさん入っている)

プレースホルダでバインドする

VALUEに固定値を入れていたので、パラメータを使ってPOSTデータをINSERTします。

バインドとは

バインド (bind)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
https://wa3.i-3-i.info/word12448.html

バインドとは関連付ける紐付けるという意味です。

先程のLIGさんの記事では

バインドとは、上記のようにプレースホルダに変数を割り当てる処理のことをいいます。

また、今回のようにPOSTデータを受け取り、データベースに登録を行う場合に、insert文に直接取得した変数を与える形だと「SQLインジェクション」というデータベースの不正操作を引き起こすことになってしまいますが、PDOを用いてプレースホルダを利用して取得変数を組み込んだり、バインドする際に型を指定することでSQLインジェクション対策を含むことが容易にできます。

とあります。

具体的には

$stmt->bindParam(':name', $_POST['name'], PDO::PARAM_STR);

ですね。

プレースホルダが「:name」で変数が「$_POST['name']」ですね。

プレースホルダとは

プレースホルダ「:で始まる代替文字列」の部分です。

引き続きLIGさんの記事では

プレースホルダとは実際の内容を後から挿入するために仮で確保している場所

とあります。

プレースホルダとは何か?SQLインジェクション攻撃を回避せよ!

こちらでもSQLインジェクションについて具体的に説明されています。

SQL文の中で「変動する箇所」には必ずプレースホルダを使いましょう。

とあるので、以下の書き方を必ずするということですね。

バインドパラメータ、バインド変数は文字通りバインドした変数ですね。多分。
記事をいろいろ見ていると表現もいろいろで、考えると混乱しますが、きっとそういう解釈でいいはずです。

データを入れる前の事前準備のprepareメソッドを使って以下のようにしました。

☆idはオートインクリメントなので、飛ばしました。

  if (!empty($_POST['content'])) {
    $sql  = 'INSERT INTO todo(`registration-date`, priority, content, note, `period-date`, category) VALUES(:registration_date, :priority, :content, :note, :period_date, :category)';
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam(':registration_date', $_POST['day-r'], PDO::PARAM_STR);
    $stmt->bindParam(':priority', $_POST['priority'], PDO::PARAM_STR);
    $stmt->bindParam(':content', $_POST['content'], PDO::PARAM_STR);
    $stmt->bindParam(':note', $_POST['note'], PDO::PARAM_STR);
    $stmt->bindParam(':period_date', $_POST['day-p'], PDO::PARAM_STR);
    $stmt->bindParam(':category', $_POST['category'], PDO::PARAM_STR);
    $stmt->execute();
  }

登録時に日付のフォーマット(yyyy-mm-dd ←正式な書き方ではないかも)に気をつけて入力、送信したら無事にINSERTできました◎

5. TODO登録後の一覧表示

一覧表示は以前にできていたので、今度は新しく登録したtodoを含めてtodoリストを表示します。

DOCTYPEタグの前にいろいろとPHPで処理を書いていますが、try/catch の中にまとめて入れてみました。

try {
  // DBに接続
  $dsn = 'mysql:dbname=practice;host=localhost;charset=utf8mb4';
  $username = 'root';
  $password = 'root';
  $driver_options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
  ];
  $pdo = new PDO($dsn, $username, $password, $driver_options);

  //TODO追加
  if (!empty($_POST['content'])) {
    $sql  = 'INSERT INTO todo(`registration-date`, priority, content, note, `period-date`, category) VALUES(:registration_date, :priority, :content, :note, :period_date, :category)';
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam(':registration_date', $_POST['day-r'], PDO::PARAM_STR);
    $stmt->bindParam(':priority', $_POST['priority'], PDO::PARAM_STR);
    $stmt->bindParam(':content', $_POST['content'], PDO::PARAM_STR);
    $stmt->bindParam(':note', $_POST['note'], PDO::PARAM_STR);
    $stmt->bindParam(':period_date', $_POST['day-p'], PDO::PARAM_STR);
    $stmt->bindParam(':category', $_POST['category'], PDO::PARAM_STR);
    $stmt->execute();
  }

  // テーブル情報取得
  $stmt = $pdo->query('SELECT * FROM todo');
  $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

} catch {
  header('Content-Type: text/plain; charset=UTF-8', true, 404);
  exit($e->getMessage());
}

これでうまく動きました!

同じ $stmt を使い回していいものなのか、catch の中身も果たして妥当なのか(エラーはログに書き出すはず)まだ怪しいですがひとまず。

次回へ続けたい...

追加ときたら削除まではやっておきたいです。

ただ削除ボタンは作らないといけないので、ボタンを付けたら次へ進もうと思います。

答え合わせまではやりたいです。