PHP5.2のPDOでSQLiteを使う

  • ApacheでPHPを使うならPHP5.2だ。それならSQLiteはよほどの抜き差しならぬ理由が無い限りPDOでやるべき。選択の余地は無い。SQLite3を使うべきだから。そんな当たり前のことすらよくわかっていなかったというところからスタートした。少しはわかってきた。
  • このページはコードをたくさん書いているのでText::Hatenaモードにした。Wikiモードだとコードに反応してしまうので。
  • SQLite入門 西沢直木著 のサンプルアプリ、メモ帳アプリというのがあって、それを使いつつ、読みつつPHPからSQLiteを使うノウハウをネタばらしにならない範囲で、書きとめておく。
  • メモ帳アプリは公開されている。
  • http://www.seshop.com/book/download/
  • ここでSQLiteで検索すれば、ダウンロードのリンクがある。
  • これは、ネタばらしじゃないでしょ。WEBにあるんだし。これを読みながら勉強しようと思う。

1.とりあえず起動

  • 中身は知らないがとりあえず動かそうと思ってhtdocs\に置いて、http://localhost/memo-pdo.phpを見たが白紙だよ。因みにSQLite3はPDOだと思うのでそれやったんだけど・・・。PHP5.3だけどPDOも使えるようにしてるつもりだけど…。でも、データベースファイルらしきものはできているので、一応動いているのか。エラーすら出ない。真っ白。意味わからん。動いてる?IEのタイトルバーにタイトルすら出ないぜ。はあー?
  • こういう時は、変なわけのわからんことになってる。絶対エラーかタイトルぐらい出るはず。原因は追究するな!対症療法だ。というわけで、ez-HTMLでこいつを開き、(ポコッと音がする。なんかおかしい。)新規PHPページを作り、そこへこいつを全部コピペする。そしてコピペしたファイルをmemo-pdo.phpに上書き。要するにクリップボードでテキスト以外を除外するフィルタをかけたようなもんだ。CもVBもPHPもCGIもHTMLもCSSもSQLだって何だってTEXTで書かれてるんだろ?だからTEXT以外は要らないんだよ。案の定動いた。
  • 後日談(9月17日)どうもこのような挙動不審はApacheでPHP5.3を使っているから?という感じがして今はPHP5.2に戻した。SQLiteは無論バージョン3を使うので、PDO関数を使う。PHP5.2であればこのような挙動不審はない。

2.データを入れてみる

  • これ、掲示板みたいなもんだな。題名と本文のテキストエリアがあって、新規作成でいくつでも登録できて、題名の一覧が下に出てそれらはデータにリンクしている。リンクをクリックしてデータを呼び出し、テキストエリアの内容を変えれば変更ができる。チェックボックスをチェックして削除ボタンを押せばそのデータは削除される。シンプルだが、多くのノウハウが詰まっているな。いい教材だと思う。ややこしくなるからエラー処理はない。その方がいい。ところで、PHP5.3だからか、日付の書き方がどうかしてるのか警告が出る。Warning: date() [function.date]: これはエラーというわけでもなくプログラムはちゃんと動く。どうせPDOでやるので、PHP5.2に戻す
  • 教材としてではなく実用にもなりそう。自宅サーバーはWEBサーバーのみだが、これを使えば頻繁に更新する部分のコードをデータとしてアップできるので、FTPサーバーを立ち上げるまでもない。
  • このプログラムにはほとんど僕がやろうとしていることのすべてが入っている。片っ端から読み解いてまとめ、あとは改造していく形で目的は達成できると思う。
  • 僕にとってプログラムを読むというのは日本語訳するということ。1行ずつ1句ずつ何をしているのかを日本語で書くこと。プログラムは言語であり、言語であるからには日本語で表せるはずである。日本語で表した時にものすごい量になるから各種のプログラム言語で表すのだ。僕はこの量的な問題だけによってプログラム言語があると思っている。こうしなさい、ああしなさい、こういうときはこうしなさい、そういうことを簡潔に書ける言語としてプログラム言語がある。ただし、簡潔であればあるほどわかりにくい。簡潔とわかりやすさの兼ね合いをどこに取るかがスキルである。スキルの高い人は簡潔。僕みたいにスキルの低い人はわかりやすさを重点に置く。唯一気を付けるべきは、「わからんのにわかったように話をすすめてはいけない」ということ。自分がわからない限り決して先に進まない。PHPについてもほとんどわかっていないのでこのプログラムを読むのにはかなり努力が要る。
  • よく「猿でもわかる」とか「犬でもわかる」という表現をする人が居るが、猿や犬にもプログラムはもしかして理解させることができるかもしれないが、それよりは日本語に訳す方が労力は少ないと思う。

読解1.データの取得(変数の初期化)まで

コメント以外のコードはメモ帳アプリケーション(西沢直木氏作)より引用

<html>
<!-- html文書のbodyにPHPを書く。 -->
<head>
<title>メモ帳アプリケーション(PDO関数)</title>
</head>
<body>
<h3>メモ帳アプリケーション(PDO関数)</h3>
<!-- 小見出し -->

<?php
// データの取得 
//ここからPHP。データの取得というより変数の初期化。

$act = (isset($_POST["act"])) ? $_POST["act"] : "";
//$actという変数は「新規作成」とか「保存」などの行為(action)を示す値をとる。 
//?は三項条件演算子(a? b : c aが真ならb,偽ならc)C言語でよく使う。IF文の方がわかりやすいけど。 
//isset(変数)は変数がセットされているどうか調べてセットされていればTRUE、されていなければFALSE。 
//$_POST["act"]はフォームのPOSTメソッドのinputでactという名前でブラウザから要求された値がセットされる変数。 
//結局$actは$_POST["act"]に値があればその値とし、なければNULLとする、ということ。 

$id = (isset($_REQUEST["id"])) ? intval($_REQUEST["id"]) : "";
//$_REQUEST["id"]は、GETメソッドかPOSTメソッドでidという名前でブラウザから要求された値がセットされる変数。 
//POSTメソッドとGETメソッドの違いはhttp://nyx.pu1.net/practice/external_var/external_var2.php
//要するにフォーム以外のハイパーリンクとかのブラウザからの要求は全部GETメソッド。 
//intvalは整数の数値。 
//結局、結局$idは$_REQUEST["id"]に値があればその整数値とし、なければNULLとする、ということ。 

$contents = (isset($_POST["contents"])) ? $_POST["contents"] : "";
//$contentsは本文。 
//$_POST["contents"]はフォームのPOSTメソッドのinputでcontentsという名前でブラウザから要求された値がセットされる変数。 
//結局$actは$_POST["contents"]に値があればその値とし、なければNULLとする、ということ。 

$title = (isset($_POST["title"])) ? $_POST["title"] : "(無題)";
//$titleは題名。 
//$_POST["title"]はフォームのPOSTメソッドのinputでtitleという名前でブラウザから要求された値がセットされる変数。 
//結局$actは$_POST["title"]に値があればその値とし、なければ(無題)とする、ということ。 

$dt = date("Y-m-d H:i:s");
//$dtに現在の日付と時刻をセット。 
//yyyy-mm-dd hh:mm:ssという形式はSQLiteの日付、時刻関数でも認識できる形式。

読解2.データベースの初期設定まで

// データベースの初期設定 
$conn = init();
//init()はユーザー定義関数(あとで記述されるfunction init()で戻り値は$conn)。 
//ユーザー定義関数というのはサブルーチンのようなもんで、一連の処理を書いたものであり、 
//必要に応じてそれを呼び出す。 
//引数を与えればそれに処理を加えた戻り値が返ってくる。 
//引数が無くても処理後の戻り値だけある場合や、引数も戻り値も無い単なるサブルーチンという場合もある。 
//function 関数名(引数) { 
//  処理に必要なプログラム 
//  return(戻り値) 
//   } 
//関数は上記の形で定義される。このプログラムの場合、関数名はinit()であり、これを書くことで呼び出される。 
//引数は無いし、$connの値を戻すだけの関数である。 
//$connが何を意味するのか?connection、つまり、接続ということで、 
//具体的には$conn = new PDO("sqlite:memo.sqlite")という値がinit()でセットされる。 
//ともかくここで$connにinit()というユーザー定義関数の戻り値をセットし、直後の処理分岐で使う。

読解3.処理分岐まで

if ($act == "保存" and $contents <> "") { // 保存
    if (empty($id)) { // 新規保存
        $id = add_data($conn, $title, $contents, $dt);
    } else { // 既存データの保存
        update_data($conn, $id, $title, $contents, $dt);
    }
//もし$act(行為)が"保存"でかつ$contents(本文)がNULLなら保存だけど 
//$idが空なら新規保存だけどユーザー定義関数add_dataを4つの引数で実行し、戻り値を$idにする。 
//それ以外なら既存データの保存で、ユーザー定義関数update_dataを5つの引数($idを含む)で実行。 

} elseif ($act == "選択したデータを削除") { // 削除
    if (isset($_POST["c1"])) {
        delete_data($conn, $_POST["c1"]);
    } else {
        echo "削除するデータが選択されていません";
    }
//それ以外でもし$act(行為)が"選択したデータを削除"なら削除だけど、 
//フォームのPOSTメソッドのinputで"c1"という名前(チェックボックス)で 
//ブラウザから要求された値(具体的にはidの値)があれば、 
//ユーザー定義関数delete_dataを2つの引数$connとidの値で実行する。 
//それ以外なら"削除するデータが選択されていません"と表示。 

} elseif ($act == "全データを削除") { // 全データ削除
    delete_all_data($conn);
//それ以外でもし$act(行為)が"全データを削除"なら全データ削除つまり 
//ユーザー定義関数delete_all_dataを引数$connで実行する。 

} elseif ($act ==  "新規作成") { // 新規作成
    clear_data();
//それ以外でもし$act(行為)が"新規作成"新規作成つまり 
//ユーザー定義関数clear_data();を引数なしで実行する。 

} elseif (!empty($id)) { // 編集
    $row = get_data($conn, $id);
    $title = $row["title"];
    $contents = $row["contents"];
}
//!は論理演算子で否定。!aならaでないということ。それ以外でもし$idが空でなければ編集つまり 
//ユーザー定義関数get_dataを$connと$idの2つの引数で実行し、戻り値を$rowにする。 
//$title(題名)をtitle番目の$rowとする。え?row番目の$titleじゃないの? 
//正しくは$title = $title["row"];じゃないの? 
//$contents(本文)をcontents番目の$rowとする。え?え?row番目の$contentsじゃないの? 
//正しくは$contents = $contents["row"];じゃないの? 
//おかしいよ。でもプログラムは正常に動作している。 
//僕の思う正しくはのように書き変えたら正常に動作しなかった。そりゃそうだ。 
//$rowは行番号ではない!!$idに書かれているデータ(タイトルと本文)である。rowなんて名前にしないで! 
//この場合の戻り値$rowは、連想配列である。連想配列というのは添字が文字の配列。 
//$row["title"]は、この$idのタイトルのデータ。$row["contents"]は、この$idの本文のデータ。 
//つまり、編集の時に該当$idのタイトルと本文を$titleと$contentsにセットするということ。 

//ここで、$rowという変数名について考えておきたい。Excelなどでrowといえば行番号だと反射的に思ってしまったのが 
//大きな間違い。リレーショナルデータベースにおいて、行というのは、複数のデータの組み合わせを格納する単位である。 
//データベース全体を表として見た場合、1つの行は横長に伸びた形で表示される。 
//1つの行に含まれるデータはひとかたまりとして扱われ、それぞれの行は主キー($id)によって識別される。 
//例えば住所録の場合、氏名や住所・電話番号などを合わせた1人分のデータが1つの行を構成する。 
//つまりRDBにおいて、ROWといえばIDによって識別される1件分のデータの塊である。 
//だから列を添字とした連想配列で表す。行番号の働きは$idが担っている。 

//だいぶ読めてきた。後は心臓部であるユーザー定義関数。 
//ここに、具体的にどのようにPHPとSQLiteが連携するのかが書かれている。 
//このプログラムは教材だけあって非常に解読しやすく書かれている。 
//他人のプログラムは、とかく意味不明理解不能ということになりがちだ。 
//ひどい場合には、読めるもんなら読んでみろ! 
//なんてプログラムを親切そうな顔してわざわざ持ってくる人も居る。有難迷惑だ。 
//しかし、このプログラムは自分で書くよりよくわかるような書き方だ。

読解4.データベースの初期設定まで

// データベースの初期設定
function init() {
    // データベースに接続
    $conn = new PDO("sqlite:memo.sqlite");

    // データベースの作成
    $sql = "CREATE TABLE IF NOT EXISTS memo (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                contents TEXT NOT NULL,
                dt TEXT NOT NULL
            )";
    $stmt = $conn->prepare($sql);
    $stmt->execute();
    return $conn;
}
//いよいよPHPのPDOでSQLiteに接続する。 
//$conn = new PDO("sqlite:パスとデータベースファイル名"); 
//接続の決まり文句。これで$connにPDOクラスのインスタンス(戻り値)がセットされて、接続される。 
//$conn = NULL とすれば切断されるが、わざわざやらなくていい。 
//普通、PHPはSQLiteにクエリ(query:問い合わせ)を投げかけ、 
//SQLiteはその問い合わせ(クエリ)に応じて、回答するが、 
//上記の場合は 
//1.あらかじめSQL文を定義する($sqlにSOL文を代入する。文の意味はコマンドラインの練習でやったようなことなので略)。 
//2.必要に応じて値をセットする(prepareにSQL文を引数として渡し、戻り値をプリペアドステートメントとする)。 
//3.実行する(execute()でそのプリペアドステートメントを実行する)。 
//$connを戻り値として保持しつつ呼び出し元に帰る。 
//PDOプリペアドステートメントに対するメソッドや定義済み定数は下記URL参照。 
//http://www.phpmanual.jp/ref.pdo.html

読解5.データの追加まで

//データの追加(処理分岐では新規保存となっている) 
function add_data($conn, $title, $contents, $dt) {
    $sql = "INSERT INTO memo(title, contents, dt)
                VALUES(:title, :contents, :dt)";
    $stmt = $conn->prepare($sql);
    $stmt->bindParam(":title", $title);
    $stmt->bindParam(":contents", $contents);
    $stmt->bindParam(":dt", $dt);
    $stmt->execute();
    $id = $conn->lastInsertId();
    return $id;
}
//データの追加(処理分岐では新規保存となっている) 
//題名、本文、投稿日時を追加するSQL文を$sqlに代入。 
//$sqlによるプリペアドステートメントをセットする。 
//bindParam - プリペアドステートメントのパラメータに PHP 変数をバインドする 
//なんてややこしいことがマニュアルに書いてあるが、(引数でもらっている)変数を 
//プリペアドステートメントの各項目のデータとして入力するって事である。 
//$stmt->execute()でプリペアドステートメントを実行。 
//$conn->lastInsertId()は最後に追加した行の IDを返す。 
//この値を$idとして戻り値とする。

読解6.データの変更まで

// データの変更
function update_data($conn, $id, $title, $contents, $dt) {
    $sql = "UPDATE memo SET
                title = :title,
                contents = :contents,
                dt = :dt
                WHERE id = :id";
    $stmt = $conn->prepare($sql);
    $stmt->bindParam(":id", $id);
    $stmt->bindParam(":title", $title);
    $stmt->bindParam(":contents", $contents);
    $stmt->bindParam(":dt", $dt);
    $stmt->execute();
}
//データの変更(処理分岐では既存データの保存となっている) 
//引数でもらったidの題名、本文、投稿日時をとりあえずセットするSQL文を$sqlに代入。 
//$sqlによるプリペアドステートメントをセットする。
//bindParam - プリペアドステートメントのパラメータに PHP 変数をバインドする
//なんてややこしいことがマニュアルに書いてあるが、(引数でもらっている)変数を
//プリペアドステートメントの各項目のデータとして入力するって事。 
//$stmt->execute()によりプリペアドステートメントを実行。戻り値無し。

読解7.指定データの削除まで

// 指定データの削除 
function delete_data($conn, $data) {
    $sql = "DELETE FROM memo WHERE (id = :id)";
    $stmt = $conn->prepare($sql);
    for ($i = 0; $i < count($data); $i++) {
        $stmt->bindParam(":id", $data[$i]);
        $stmt->execute();
    }
    clear_data();
}
//$dataはチェックボックスの表示で何番目のチェックボックスなのかを示す値。 
//未定のidの行を削除するSQL文を$sqlに代入。 
//$sqlによるプリペアドステートメントをセットする。 
//idを$dataの値とするが、複数チェックされている場合があるので 
//それぞれの$data(配列変数)のidについて、 
//$stmt->execute()によりプリペアドステートメントを実行。(削除実行。) 
//$data(配列変数)をすべてNULLにする。

読解8.全データの削除まで

// 全データの削除 
function delete_all_data($conn) {
    $sql = "DELETE FROM memo";
    $stmt = $conn->prepare($sql);
    $stmt->execute();
    clear_data();
}
//全データを削除するSQL文を$sqlに代入。 
//$sqlによるプリペアドステートメントをセットする。 
//$stmt->execute()によりプリペアドステートメントを実行。(削除実行。) 
//$data(配列変数)をすべてNULLにする。

読解9.データのクリアまで

// データのクリア 
function clear_data() {
    global $id, $title, $contents;
    $id = "";
    $title = "(無題)";
    $contents = "";
}
//function外でも通用するように$id, $title, $contentsをglobal宣言し、 
//それぞれ消す。

読解10.編集データの取得まで

// 編集データの取得
function get_data($conn, $id) {
    $sql = "SELECT * FROM memo WHERE id = :id";
    $stmt = $conn->prepare($sql);
    $stmt->bindParam(":id", $id);
    $stmt->execute();
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    return $row;
}
?>
//引数でもらったidの行を選択するSQL文を$sqlに代入。 
//$sqlによるプリペアドステートメントをセットする。 
//引数でもらっているidをプリペアドステートメントのデータとして入力する。 
//$stmt->execute()によりプリペアドステートメントを実行。 
//fetch(PDO::FETCH_ASSOC)は行全体を各カラムを添字とする連想配列として返す。 
//行全体を各カラムを添字とする連想配列として$rowに代入する。 
//$rowを戻り値として呼び出し元に帰る。

読解11.フォームのHTML

<form method="POST" action="<?php echo $_SERVER["SCRIPT_NAME"]?>">
<table>
  <tr>
    <td><input type="text" value="<?php echo $title?>"
          size="50" name="title"></td>
  </tr>
  <tr>
    <td>
    <textarea  name="contents" rows="10"
      cols="60"><?php echo $contents?></textarea>
    </td>
  </tr>
  <tr>
    <td>
      <table>
        <tr>
          <td>
          <input type="hidden" value="<?php echo $id?>" name="id">
          <input type="submit" value="保存" name="act">
          </td>
          <td>
          <input type="submit" value="新規作成" name="act"
            onClick="return confirm('新規作成します')">
          </td>
          <td>
          <input type="submit" value="選択したデータを削除"
            name="act"
            onClick="return confirm('削除して良いですか?')">
          </td>
          <td>
          <input type="submit" value="全データを削除" name="act"
            onClick="return confirm('削除して良いですか?')">
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>
//いくらなんでも読めるが、1行目 
//<form method="POST" action="<?php echo $_SERVER["SCRIPT_NAME"]?>">
//<form method="postまたはget" action="URI"> ~ </form> 
//フォームの送信形式postまたはgetと送信先のURIを記述。 
//$_SERVER["SCRIPT_NAME"]は実行中のスクリプトのパス返す。 
//$idは表示しないのでhidden 
//onClick="return confirm('確認文')"はjava環境で確認文つきのボタンとなる
  • このままだとPOSTの度にバックスラッシュ\¥が増殖するので、$titleと$contents二ついてはstripslashes関数で\を取る。そのあたりの改造は下記。
<tr>
    <td><input type="text" value="<?php $title=stripslashes($title);echo $title?>"
          size="50" name="title"></td>
  </tr>
  <tr>
    <td>
    <textarea  name="contents" rows="15"cols="100"><?php $contents=stripslashes($contents);echo $contents?></textarea>
    </td>
  </tr>

読解12.最後まで

<?php
// 全データの取得
$sql = "SELECT * FROM memo ORDER BY dt DESC";
$stmt = $conn->prepare($sql);
$stmt->execute();

// データの一覧表示
echo "<table border=\"1\">";
echo "<tr>";
echo "<td>削除</td>";
echo "<td>タイトル</td>";
echo "<td>最終更新時刻</td>";
echo "</tr>";
while ($row = $stmt->fetch()) {
    echo "<tr>";
    echo "<td><input type=\"checkbox\"
           name=\"c1[]\" value=\"" . $row["id"] . "\"></td>";
    echo "<td><a href=\"" . $_SERVER["SCRIPT_NAME"]
        . "?id=" . $row["id"] . "\">". $row["title"]
        . "</a></td>";
    echo "<td>" . $row["dt"] . "</td>";
    echo "</tr>";
}
echo "</table>";
?>
</form>
</body>
</html>
//ORDER BY dt DESC 登録日時で降順に並び替え。 
//while ($row = $stmt->fetch()) { 
//配列変数である行全体の内容を受け取ることが真である間ということだろう。 
//この最後の表示のところのループがイマイチわからん。 
//まあ、ここまで読めたら良しとしよう。 
//次は改造。
最終更新:2011年09月23日 16:49