PHPでTODOリストを作ってみた(1) - テーブルの表示

TODOリストを作ってみます。

作り方を調べたら真似をして終わりになってしまうので、調べることは最低限にして試行錯誤していこうと思います。

☆以下は必要に応じて追記していきます。

環境

  • MAMP(ローカル環境)
  • PHP(7.4.9)
  • MySQL(5.7.30)

仕様

普段TODOリストのツールを使っていないので(手書き。アナログ)、こんなものがあればいいなで考えました。

  • 登録した日付、重要度、内容、備考、期限 カテゴリ の項目がある
  • TODOの作成、削除、並び替え、絞り込みができる

必要なもの(ファイル、ソフトなど)

  • DB(MySQL
  • phpMyAdmin(4.9.5)
  • フォーム画面(form.php)・・・入力・確認・完了画面も1つのファイル

作り方の手順

まずは最短でTODOの登録を考えます(表側にこだわりすぎるとそこで終わってしまいそうなので)。

  1. フォーム画面を作る(html)
    この時点では日付を除いてテキストのフォーム
    POST送信
  2. DBにテーブルを作る
    PHPMyAdmin
    適当に2、3件直接登録しておく
  3. フォーム画面に一覧(テーブル)の表示
    DBへの接続
    SQL文でデータを取得してphpで繰り返し処理
  4. SQL文でレコードの追加
    「登録」ボタンを押してPOST送信したデータを追加する
  5. TODO登録後の一覧表示
    新しく登録したTODOも表示する

果たして最後までできるかどうか。

TODOの登録までできてから、削除や並び替えなどをやるか考えます。

1. フォーム画面を作る(html)

form関連はあまり書く機会も少ないので早速調べました。

【HTML入門】formタグを使ってフォームを作る方法を1から解説 | 侍エンジニアブログ

見た目は想像通りです!

form.php

せめて隙間ぐらい空ければよかったかなと思いましたが、まぁいいです。

POST送信の確認

formタグを method="post" とするとPOST送信になります。

また action 属性にデータの送信先を指定します。
フォーム画面であるこの同一ファイルに送ることもできます。
この場合 action="/form.php"(ルート相対パスaction="form.php相対パス) でも form.phpに送ることができました。

ちゃんと届いているかどうか、form.phpに出力してみました。

<?php if ($_POST['content']) : ?>
  <h2>登録したタスク</h2>
  <p><?= $_POST['content'] ?></p>
<?php endif; ?>

TODO(content)入力後登録ボタンを押すとタスク内容が無事表示されました。

2. DBにテーブルを作る

探り探りの部分へ突入です。

おそらくあらかじめテーブルを作っておく必要があると思います。

コマンドラインは習得に時間がかかるのでMAMPに入っているPHPMyAdminを使います。
コマンドラインでの作り方がたくさん記事にあったので若干後悔)

①データベースを作る

PHPMyAdminにて。
「Create database」でデータベース名を付けて新規で作ります。
今後も使うかと思い「practice」と付けました(複数テーブル使う練習するかもしれませんが、今のところ)。

②テーブルを作る

①で作ったデータベースの中に「todo」というテーブルを作ります。 そのときにカラムの数も設定するので、登録日、重要度、内容...と数えて6と入れました。

f:id:yokoyoko_115:20210522013659p:plain

そしてカラムの設定をしました。

一通り入れて「Preview SQL」ボタンを押すと、SQL文が確認できました。

CREATE TABLE `practice`.`todo` ( `registration-date` DATE NOT NULL , `priority` TEXT NOT NULL , `content` TEXT NOT NULL , `note` TEXT NOT NULL , `period-date` DATE NOT NULL , `category` TEXT NOT NULL ) ENGINE = InnoDB;

何となく読めはするけど書けないです。

「Save」ボタンを押すと、テーブルが作れました。

f:id:yokoyoko_115:20210522014019p:plain

テーブル作成において触ったのはNameとTypeだけです。
すべてTypeはTEXTにしようかとも思いましたが、せっかく選べるので日付のTypeはDATEにしました。
重要度やカテゴリーも本当は数字(INT)にして、別テーブルでカテゴリーIDとカテゴリー名の対応表を用意して紐付けるのがベストなのだろうと思いましたが、いきなりそれはできないのでTEXTです。

インデックスを作る

テーブルの表示の下に

Indexes
No index defined!

とありました。その下には
Create an index on [  ] columns

と、インデックスを作るところがありました。

インデックスとは?

インデックスの意味とメリット・デメリット | SQLite入門

インデックスを簡単に言うと対象のカラムのデータを取り出し、高速に検索できるように手を加えて保存しておいたものです。

インデックスを作成することでテーブルとは別に検索用に最適化された状態で必要なデータだけがテーブルとは別に保存されるということを覚えておいて下さい。

「対象のカラム」(記事の中ではname、人の名前)はアルファベットや数値が昇順などで並び替えられているので、それを保存しておくことで検索に役立てるみたいです。

この場合nameで検索するときは早くなるかもしれませんが、別のカラムで検索する場合にはインデックスは効果があるのでしょうか。
インデックスには複数カラム、さらには複数のインデックスが登録できるようです。
検索に使うカラムは登録した方がいいのでしょうか。

今回の場合、重要度、期日、カテゴリで検索することになりそうです。
全カラムの半分近くがインデックスって、果たして妥当なのかどうか。

インデックスを作るかどうか書かれています。

MySQLのIndexをはるコツ - Qiita

SQLの理解が何となくだと厳しいです。

ひとまず必要そうなidカラムの追加と、インデックスの作成(idと期日、period-date)をします。

色々いじったらこうなりました。

f:id:yokoyoko_115:20210522235806p:plain

idだけUNIQUEにしたかったのですが、これだと2つともUNIQUEになっていますね。
さらにテーブルにはidとperiod-date両方にうっすら鍵マークが付きました。

インデックスはidで検索することはないので期日だけで良かったのかもしれません。

プライマリキー:id
インデックス:period-date(Index choice:INDEX ←Index choiceとは?)

に修正します。

最終的にはこうなりました。

f:id:yokoyoko_115:20210523005602p:plain

idの「AUTO_INCREMENT」はオートインクリメント。自動で連番を振ってくれるものです。
設定画面で「A_I」と書かれていて、はじめ何のことだか分かりませんでした。

何となくの設定でも、今回のような簡単なものなら問題はなさそうです。勘です。
まだまだ理解が微妙です。

③レコードを登録する

PHPMyAdminから適当にTODOを入れてみます。

今まで見ていた画面はStructiure(構造)タブで、レコードの登録はInsert(挿入)タブです。

登録後にSQL文が表示されます。

INSERT INTO `todo` (`id`, `registration-date`, `priority`, `content`, `note`, `period-date`, `category`) VALUES (NULL, '2021-05-23', '中', '部屋の掃除', '朝やる', '2021-05-28', '家事');

3つ入れました。idが飛び飛びになっているのは、誤って入れてしまったレコードを削除したためです。さっそく気持ち悪い。

f:id:yokoyoko_115:20210523011659p:plain

ここからがいよいよ問題です。

3. フォーム画面に一覧(テーブル)の表示

まずはDBに接続が必要!だと思うので調べました。

MySQLSQLとは

いきなりおさらいです。

MySQL

MySQLとは?初心者にわかりやすい説明

MySQLオープンソースリレーショナルデータベース管理システムです。

リレーショナルデータベース管理システムRDBMS:Relational DataBase Management System)です。
データベース管理システムも色々あるようです。

SQL (Structured Query Language)

SQLとは?基礎知識から具体例まで分かりやすく解説!

SQLはデータベース(RDBMS)を操作するための言語です。 SQLは国際標準化されているため、さまざまなデータベースで利用できます。有名なデータベースとしては、OracleMySQLPostgreSQLSQLiteなどが、いずれもSQLで操作可能です。

SQLは他のデータベースでも使われているので最低限は覚えたほうがいいですね。

PHPのデータベース接続の方法

記事を見ました。とても詳しいです。それぞれ2020年、2019年に更新されているので情報もさほど古くないのではないでしょうか(2021年5月現在)。

【PHP超入門】クラス~例外処理~PDOの基礎 - Qiita

PHPでデータベースに接続するときのまとめ - Qiita

データベースへの接続方法の一つにmysql関数を使った方法があります。

mysql関数を使った接続方法は、PHP 5.5.0 で非推奨になり、PHP 7.0.0 で削除されました。

mysql関数以外でデータベースに接続するには、PDOまたはMySQLiを使用します。

PDOをよく見るのでこちらを使う方法が主流のようです。

PDO(PHP Data Objects)とは

データベース抽象化レイヤの一つ
だそうです。

しっかり記事を読もうとすると奥が深く、心が折れそうなので、載っている方法で接続してみます!

$pdo = new PDO($dsn, $username, $password, $driver_options);

PDOクラスのインスタンスを生成してコンストラクタによって接続しています。

$pdo の中身が気になるので var_dump() してみました。

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

  var_dump($pdo);
  echo '<p>$pdoオブジェクトってvar_dumpできるの?</p>';

var_dumpもできないし、テキストもでませんでした。

接続部分でエラーが起こっているのかもしれません。
try,catchを使ってみることにします。

☆try/catch句についてはこちらに書きました。

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

    var_dump($pdo);
    echo '<p>PDOクラスってvar_dumpできるの?</p>';
  } catch (PDOException $e) {
    exit($e->getMessage());
  }

すると

SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost' (using password: NO)

と出てきました。エラーメッセージ便利!
パスワードが空なのがダメなようです。$password にrootを入れてみました。
すると

object(PDO)#1 (0) {} PDOクラスってvar_dumpできるの?

と出ました。接続はうまくいったみたいですね!

object(PDO)#1 (0) {}
データ型(クラス名) #id番号 (メンバ変数の個数、private protected public 全部) { メンバリスト}

だそうです。
#1 は`オブジェクトのIDのようです。

中身がさっぱり不明なこのPDOオブジェクトとは一体何なのでしょうか。
データベース抽象化レイヤがオブジェクトになったものなのでしょう。

header()について

参考時にコードで飛ばしていしまいましたが、レスポンス(?)時に送信するHTTPヘッダーの中身だと思われます。

header('Content-Type: text/plain; charset=UTF-8', true, 500);

DBに接続できないなどエラーになったときにレスポンスステータスコード500を返すように指定しています。

ヘッダー関連のやり取りがあるので、DB接続の記述はhtmlソースよりも前に記述しています(html要素よりも前)。

PDO::query、PDO::execメソッドとPDO::prepareメソッド

PDOオブジェクトからクエリを実行するのですが、使い分けをします。

ユーザー入力を伴わない、単にテーブルの一覧を表示させたいときは PDO::query を使います。

$result = $pdo->query(SQL文);

SQL文がMySQLに送られ実行され、結果が返ってきます。
返り値はPDOStatementクラスのオブジェクトで、$result に代入されます。

ステートメント、文...。

そしてqueryメソッドを使うときの注意点は

プリペアドステートメントを使わずにデータを取得するので、自分でエスケープ処理する必要があります。使うときは注意してください。

とのことです。意味がまだ分かりません。入力した値?SQL文?をエスケープするのでしょうか。

その下を読むと

ユーザーからの入力を伴うSQL文は、エスケープ処理する必要があります。 ユーザーからの入力を伴うSQL文のときは、プレースホルダを使いましょう。

エスケープする必要があるのはSQL文でした。

今回は不要ですね。

execメソッドとprepareメソッドを使う場合もおいおい調べます。

ひとまずPDOStatementクラスのオブジェクトをvar_dumpしてみます。

  $stmt = $pdo->query('SELECT * FROM todo');
  var_dump($stmt);

すると

object(PDOStatement)#2 (1) { ["queryString"]=> string(18) "SELECT * FROM todo" }

と出ました。

やはりPDOStatementはオブジェクトのようです。
SQL文がPDOStatementのメンバ変数として入っていますね。

PDOのオブジェクトといい、中身があっさりしすぎていて底が知れません。

このままではデータの表示ができないのでfetchメソッドで行または配列で取得するようにします。

$result = $stmt->fetch();
 var_dump($result);

すると、見事配列で表示されましたー!

array(14) {
  ["id"]=>
  int(1)
  [0]=>
  int(1)
  ["registration-date"]=>
  string(10) "2021-05-23"
  [1]=>
  string(10) "2021-05-23"
  ["priority"]=>
  string(3) "中"
  [2]=>
  string(3) "中"
  ["content"]=>
  string(15) "部屋の掃除"
  [3]=>
  string(15) "部屋の掃除"
  ["note"]=>
  string(9) "朝やる"
  [4]=>
  string(9) "朝やる"
  ["period-date"]=>
  string(10) "2021-05-28"
  [5]=>
  string(10) "2021-05-28"
  ["category"]=>
  string(6) "家事"
  [6]=>
  string(6) "家事"
}

ただこれでは1件取得なので、fetchAllメソッドを使って全件配列で取得します。
また、fetchAllメソッドの引数を PDO::FETCH_ASSOC にして、デフォルトであった0からの添字の配列を省きました。

  $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

配列を foreach して 以下のようになりました。

f:id:yokoyoko_115:20210530021706p:plain

次回へ続く...

TODOのDB登録と表示までやりたかったですが、長くなってしまったので一旦ここで区切ります。

cssも整えたい!
おしまい!