前回はZFをインストールして、バージョン表示を行いました。
でもバージョン表示ができるくらいじゃ役に立ちそうにありません。

フレームワーク(FW)は使うことが目的ではなく、開発を楽にするための手段の1つと考えることができます。
便利な使い方ができてこそ、FWを使う意味がようやく見えてくるでしょう。

ではその第一歩として、MVCという設計モデルで作ることを考えます。
このMVCというものは役割分担の概念で、ほとんど全てのFWで採用されているようです。
それだけ開発において有効な概念ということになります。

Model -----データの出し入れ専門
View ------ページの描画専門
Controller --指示専門


Model-View-Controllerの頭文字がMVCというわけです。
これら3つがそれぞれ自分の仕事に徹することで、全体として処理が進行していくことになります。

それではシンプルな例としてMVCを使って、「Hello world!」を表示させることを考えてみます。

まず必要なものは「Controller」です。
これが全てのリクエストを受付けるところから処理は始まります。

この「全てのリクエストを受付ける」ということを実現するために、Apacheモジュールのmod_rewrite機能を利用してリクエストの全てを「Controller」に集めるリライト設定をすることになります。

リライト設定 - .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]


上記リライト設定は、リクエストされたファイルが実在しなければすべて index.php に処理を集めるように書いています。
なのでこの設定では index.php がコントローラとなります。
このリライト設定は設置したらあとは書き換えることはありません。つまり固定です。

さてコントローラindex.phpがリクエストを受けたら、後はアプリケーションとしての処理を行う必要があります。
今回は「Hello world!」を表示させるアプリケーションを作ろうとしているので、この場合、MVCでどういう役割分担にしたらいいかを考えていきます。

従来だったらリクエストを受けたindex.phpから直接「Hello world!」を表示させて終わらせた方が手っ取り早いやり方でした。
しかしMVCでは将来のシステム拡張(機能、ファイル数が増える)のことも考えて、管理しやすくなるよう整理した作りにします。

ここで重要なことがあります。
リクエストを受けるindex.phpにアプリケーション機能を含めてしまうと、複数のアプリケーションを混在させようとした場合にindex.phpの機能分けや統合が面倒なことになります。
なのでindex.php内容はアプリケーションに依存しない部分として切り分けた作りをする必要がありそうです。
理想を言えばどのアプリケーションを作った場合でもindex.phpが同じ内容であることが望ましいということです。


なのでイメージとしては以下のような配置関係になります。
※htdocsがドキュメントルートの例

Zend/            {ZFコンポーネント群}
application/    {1つのアプリケーションを内包}

htdocs/
    .htaccess   {リライト設定}
    index.php   {コントローラ}


リクエストを受けたindex.phpは、すぐにアプリケーション側へ処理を丸投げすることでアプリケーションの処理が開始できそうです。
そうなるとアプリケーション側にもコントローラのような指示役がいてくれないと処理が先に進みません。

ということでコントローラは「リクエストを受ける側」と、「アプリケーション側の指示役」の2つに分離して存在することになります。

「リクエストを受ける側」コントローラを『フロントコントローラ』、
「アプリケーション側の指示役」コントローラを『アクションコントローラ』と呼ぶようになっています。

ちゅーことで先にフロントコントローラを書いてみます。

フロントコントローラ - index.php
<?php

// Zendパスを通す
set_include_path('C:\xampp');

// 必要なクラス定義を読み込む
require_once 'Zend/Controller/Front.php';

// アクションコントローラへ丸投げ
$front = Zend_Controller_Front::getInstance();
$front->setControllerDirectory('../application/controllers');
$front->dispatch();


Zendクラス群へのパスを通してクラスを呼んでいます。
フルパス指定ではなく相対パス指定する場合はrealpath()も使った方がいいと思います。
set_include_path(realpath('../..'));


getInstance()メソッドを使ってオブジェクト取得を行い、setControllerDirectory()メソッドを使ってアクションコントローラの置き場所を教えています。
最後のdispatch()メソッドにより処理を丸投げします。

ではフロントコントローラからアプリケーション処理を任されるアクションコントローラを書いてみます。

アクションコントローラ - IndexController.php
require_once 'Zend/Controller/Action.php';

class IndexController extends Zend_Controller_Action
{
  public function indexAction() {

  }
}


まずクラス定義を呼び出しています。
でもあとはなんだか抜け殻です。

以上で指示役のコントローラ2つが書けたことになります。
次は「Hello world!」ページを描画するための雛形「ビュースクリプト」を書いていきます。

ビュースクリプト - index.phtml
<html>
<body>
  Hello world!
</body>
</html>


ここでは描画の要らない固定のHTMLを書いた形になってしまいました。
まずは全体の動き、連携を見るためにシンプルになるようにこうしてみました。

これまで書いてきたファイルをどう配置するかを以下に書いておきます。

ファイル配置
Zend/
application/
    models/
    views/
        scripts/
            index/
                index.phtml
    controllers/
        IndexController.php

htdocs/
    .htaccess
    index.php


配置できたらApacheが起動済みなのを確認してページ表示を試みます。
トップページを表示させたいので、ベースURL(基準となるURL)にアクセスしてみます。
http://localhost/


ここでは「Hello world!」が画面に出たらOKです。

application側にアプリケーション1つ分が丸々含まれているので開発においてはこちらを編集していくことになります。
htdocs側はドキュメントルート直下で、編集なし(固定)が基本です。

もちろんドキュメントルート直下よりも深い階層に設置する場合も考慮すべきです。
そのために常にベースURLを頭に入れて開発していく必要があります。

ドキュメントルートより深い階層の例
htdocs/
    foo/
        .htaccess
        index.php


トップページ表示をする場合は以下のようにベースURLをリクエストします。
http://localhost/foo/


では話は戻ってファイル配置を見てみると、application直下にはMVCを象徴するかのごとくmodels、views、controllersが並んでいます。
これは覚えやすい配置ですね、アプリケーション内がMとVとCにファイル分けされてるわけですから。

ただcontrollersの方は直下にファイルがありますが、viewsの場合は少々深い階層に配置されています。
これはビューには「ビュースクリプト」の他、「ビューヘルパー」「ビューフィルター」もあるのが1つと、
views/scripts以下はシステムの全画面の雛形が配置されることになるのでファイルを整理するための配置構造というわけです。

今回はMVCのうちコントローラとビューは使いました。
ただビューの描画はしていないことと、モデルを全く使ってないので次回フォローしたいと思います。


前回までの親クラスにおいて不十分だった箇所を幾ばくか解消していこうと思います。

まずコンストラクタ内で何度もparent::setAttributeをコールしているのがどうもイヤだね。
この辺のメソッドコールをまとめることを考える必要があるね。
これが解消できれば多少なりともセクシーなコードになる??


さてお次はprepare()メソッドをexecute()に統合してしまおうかね。
そうすれば一回のコールで済むわけだ。

あとは接続設定だけ先にできるようにして、実際にSQL発行するタイミングで接続するようにしたいけど、これってコンストラクタを大幅に改編する必要があるね。
っていうか、コンストラクタでparent::__construct()コールしちゃまずいということだから接続フラグをプロパティに持たせるしかないのかな?

とりあえず作ってみるさね。


親クラス(再修正版)
<?php
/**
* @filename DB.inc
*/
class DB extends PDO
{
  protected $_sth = null;
  protected $_conn = null;
  protected $_host = '';
  protected $_dbname = '';

  public function __construct($host='', $dbname='') {
    $this->_host = $host;
    $this->_dbname = $dbname;
  }
  protected function _connectdb() {
    list($host, $dbname, $user, $pass) = $this->_set_info($this->_host, $this->_dbname);
    try {
      parent::__construct("mysql:dbname={$dbname};host={$host}", $user, $pass,
        array(
          PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
          PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
          PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
        )
      );
      $this->_conn = true; // 接続確立フラグ
    } catch (PDOException $e) {
      die($e);
    }
  }
  protected function _set_info($host, $dbname) {
    if (empty($host)) $host = 'localhost';
    if (empty($dbname)) $dbname = 'mysql';
    return array($host, $dbname, 'root', '');
  }
  public function query($sql) {
    if ($this->_conn === null) $this->_connectdb();
    $this->_sth = parent::query($sql);
    return $this->_sth;
  }
  public function exec($sql) {
    if ($this->_conn === null) $this->_connectdb();
    return parent::exec($sql);
  }
  public function execute($sql, $param=array()) {
    if (!empty($sql)) {
      if ($this->_conn === null) $this->_connectdb();
      $this->_sth = parent::prepare($sql);
    }
    if (isset($param[0])) {
      foreach ($param as &$para) {
        $this->_sth->execute($para);
      }
    } else {
      $this->_sth->execute($param);
    }
    return $this->rowCount();
  }
  public function rowCount() {
    return $this->_sth->rowCount();
  }
  public function fetch() {
    return $this->_sth->fetch();
  }
  public function fetchAll() {
    return $this->_sth->fetchAll();
  }
}


前回からの変更点は、コンストラクタに渡された引数はプロパティに保持させて、PDOのSQL発行系メソッドquery(), exec(), prepare()を使用する直前に接続フラグ有無をチェックして、未接続の時だけ_connectdb()メソッドをコールするようにした。
これがうまく動けば、SQLを最初に発行したタイミングでデータベース接続することになる。逆に言えばSQL発行しなければデータベース接続しないことになる。

で_connectdb()メソッド内に移動させたPDOコンストラクタですが、これをコールした後にこれまでparent::setAttribute()メソッドを複数回コールしていたのをPDOコンストラクタの第4引数でまとめて指定している。
メソッドを分けてコールするよりも、引数で指定できた方が恐らく処理としては速いだろう。
ただ、計測してないので、実際は引数で渡した後、内部でメソッドに当てはめてるようなオチがあるかも知れないがまぁ良しとしよう。

あとはprepare()メソッドをexecute()メソッド内部で呼ぶようにしている。
バインドを考慮して、数字添え字配列で渡ってきたら複数レコード分の更新と見なして内部でforeachループさせて反映させるようにしている。

それからデバッグ処理だけど、ちょっと今日は疲れた。。。後回し。来年への宿題
PDOStatementオブジェクトのqueryStringプロパティにSQL文はセットされるようだけど、バインドデータがあっても展開されないらしいので自力置換するしかないのかな?


実行スクリプト
<?php
require '/path/to/DB.inc';

$db = new DB("", "MYDB"); // 接続
// バインドパラメータ作成
$param = array(
  array(':ID'=>4, ':NAME'=>"四"),
  array(':ID'=>5, ':NAME'=>"五"),
  array(':ID'=>6, ':NAME'=>"六"),
  array(':ID'=>7, ':NAME'=>"七"),
  array(':ID'=>8, ':NAME'=>"八"),
  array(':ID'=>9, ':NAME'=>"九"),
);
// DB保存用にUTF-8変換
mb_convert_variables("UTF-8", "SJIS-win", $param);
// プリペアドステートメントとバインドパラメータを同時に渡す
$db->execute("INSERT INTO MYTAB VALUES(:ID, :NAME)", $param);

// INSERT結果取得
$db->query("SELECT * FROM MYTAB");
$rows = $db->fetchAll(); // 全レコード取得
// 表示用にSJIS変換
mb_convert_variables("SJIS-win", "UTF-8", $rows);
var_dump($rows);


今回はプリペアドステートメントを使って、バインドさせています。
6レコードの更新処理を、1回のexecute()コールで実現する試みです。


これを実行した結果は以下のようになりました。

var_dumpの結果
array(9) {
  [0]=>
  array(2) {
    ["ID"]=>
    string(1) "1"
    ["NAME"]=>
    string(4) "富士"
  }
  [1]=>
  array(2) {
    ["ID"]=>
    string(1) "2"
    ["NAME"]=>
    string(2) "鷹"
  }
  [2]=>
  array(2) {
    ["ID"]=>
    string(1) "3"
    ["NAME"]=>
    string(6) "なすび"
  }
  [3]=>
  array(2) {
    ["ID"]=>
    string(1) "4"
    ["NAME"]=>
    string(2) "四"
  }
  [4]=>
  array(2) {
    ["ID"]=>
    string(1) "5"
    ["NAME"]=>
    string(2) "五"
  }
  [5]=>
  array(2) {
    ["ID"]=>
    string(1) "6"
    ["NAME"]=>
    string(2) "六"
  }
  [6]=>
  array(2) {
    ["ID"]=>
    string(1) "7"
    ["NAME"]=>
    string(2) "七"
  }
  [7]=>
  array(2) {
    ["ID"]=>
    string(1) "8"
    ["NAME"]=>
    string(2) "八"
  }
  [8]=>
  array(2) {
    ["ID"]=>
    string(1) "9"
    ["NAME"]=>
    string(2) "九"
  }
}


うまくいきました!!
これができたということでSQLインジェクションももう怖くないですね。


因みに実行スクリプトを以下のように接続準備だけにして、MySQLを落とした状態で実行させたら実際は接続してないので当然のごとくエラーもなにも出ません。
最初のSQL発行時タイミングまでDB接続してないことがこれで確認できます。
<?php
require '/path/to/DB.inc';

$db = new DB("", "MYDB"); // 接続


これで親クラスに関しては終着点が見えてきた気がするので、あとは子クラスなんだけど、子クラスに関しては具体的にシステム仕様ありきで作っていく必要があるので多少テストしておく程度しかできそうにないかもね。

これまでは単に「SELECT 1」として動きを見ていただけでテーブル参照をしてこなかったので、MySQLに接続できてさえいればそれで動きを確認できました。
今度は具体的にデータを作成して動きを見ていきましょ

特に文字コード周りなんぞ注視していくことにします。

ではデータベース領域を作成してそこにテーブルを作りテストデータを入れてみることにします。

まず初期化スクリプトをファイルに書いて、これを流し込んでデータ作成する手順でいきます。

-- test.ddl
CREATE DATABASE IF NOT EXISTS MYDB;

use MYDB;

CREATE TABLE MYTAB(
  ID INT,
  NAME VARCHAR(10)
) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_bin;

INSERT INTO MYTAB VALUES(1, '富士');
INSERT INTO MYTAB VALUES(2, '鷹');
INSERT INTO MYTAB VALUES(3, 'なすび');


【メモ】
ファイル名:test.ddl
データベース領域:MYDB
テーブル名:MYTAB

いつもはシフトJIS使いなんですが、グローバルスタンダードに合わせて使い慣れないUTF-8を使ってみましょ

初期化スクリプトファイルの文字エンコーディングがUTF-8であることを確認できたら、コマンドラインから流し込みます。
マルチバイトコードを含む場合、流し込む際に「--default-character-set」オプションを使って教える必要があります。
# mysql -uroot --default-character-set=utf8 </path/to/test.ddl

はいこれでデータはできました。
前回作成したDBクラスを使って、このデータを文字化けさせずに読めるか試してみます。

<?php
// 前回作成した親クラス呼び出し
require '/path/to/DB.inc';

$db = new DB("", "MYDB");
$db->query("SELECT * FROM MYTAB");
$rows = $db->fetchAll(); // 全レコード取得
// UTF-8で取得したものをシフトJISで表示させる変換
mb_convert_variables("SJIS-win", "UTF-8", $rows);
var_dump($rows);


今回から親クラスを別ファイルに分けました。

var_dumpの結果
array(3) {
  [0]=>
  array(2) {
    ["ID"]=>
    string(1) "1"
    ["NAME"]=>
    string(2) "??"
  }
  [1]=>
  array(2) {
    ["ID"]=>
    string(1) "2"
    ["NAME"]=>
    string(1) "?"
  }
  [2]=>
  array(2) {
    ["ID"]=>
    string(1) "3"
    ["NAME"]=>
    string(3) "???"
  }
}



あれれれ、、うまくいきませんでしたね。
これはPHPから接続した時に違う文字エンコーディングで認識された状態で取得してるからでしょうね。
ま、レコード数が3つであることとカラム名が合ってるようなので文字化けさえ解消できればよさそうです。

ということで次は接続後に文字エンコーディング指定してみましょ

<?php
// 親クラス呼び出し
require '/path/to/DB.inc';

$db = new DB("", "MYDB");
$db->query("SET NAMES utf8"); <-追加
$db->query("SELECT * FROM MYTAB");
$rows = $db->fetchAll(); // 全レコード取得
// UTF-8で取得したものをシフトJISで表示させる変換
mb_convert_variables("SJIS-win", "UTF-8", $rows);
var_dump($rows);


var_dumpの結果
array(3) {
  [0]=>
  array(2) {
    ["ID"]=>
    string(1) "1"
    ["NAME"]=>
    string(4) "富士"
  }
  [1]=>
  array(2) {
    ["ID"]=>
    string(1) "2"
    ["NAME"]=>
    string(2) "鷹"
  }
  [2]=>
  array(2) {
    ["ID"]=>
    string(1) "3"
    ["NAME"]=>
    string(6) "なすび"
  }
}


はい、バッチシです!!

でも一見解決したように見えて、これで少々問題勃発ですな。。
SQL発行する直前に毎回「SET NAMES utf8」も実行するのかと、

でも安心あれ
こういう毎度毎度の同じ処理は、せっかく作った親クラスに押し込めるべきでしょう!
ということで親クラスの修正をば。。


親クラス(修正版)
<?php
/**
* @filename DB.inc
*/
class DB extends PDO
{
  protected $_sth = null;

  public function __construct($host='', $dbname='') {
    list($host, $dbname, $user, $pass) = $this->_set_info($host, $dbname);
    try {
      parent::__construct("mysql:dbname={$dbname};host={$host}", $user, $pass);
      parent::setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      parent::setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
      $this->query("set names utf8"); <-追加

    } catch (PDOException $e) {
      die($e);
    }
  }
  protected function _set_info($host, $dbname) {
    if (empty($host)) $host = 'localhost';
    if (empty($dbname)) $dbname = 'mysql';
    return array($host, $dbname, 'root', '');
  }
  public function query($sql) {
    $this->_sth = parent::query($sql);
    return $this->_sth;
  }
  /* 継承メソッドと同じなので不要
  public function exec($sql) {
    return parent::exec($sql);
  }
   */
  public function prepare($sql) {
    $this->_sth = parent::prepare($sql);
    return $this->_sth;
  }
  public function execute($param=array()) {
    $this->_sth->execute($param);
    return $this->rowCount();
  }
  public function rowCount() {
    return $this->_sth->rowCount();
  }
  public function fetch() {
    return $this->_sth->fetch();
  }
  public function fetchAll() {
    return $this->_sth->fetchAll();
  }
}


はいーっ、こんな感じになりました。

ではこの後どうしましょ
とりあえずこれにいろいろいちゃもん付けてみましょ

うーん、セクシーさが足りないよね。
あと、まだINSERTやらUPDATE,DELETEとかって試してないよね。
となるとprepare()してexecute()しないといけないし面倒じゃないかい?

そうそう、レコードを大量に更新する場合にプリペアドステートメント使って連続バインドを簡単にできるのか?

あとエラー発生したらログ保存したりしてデバッグを楽にできないと使い物にならないよね。

そうだ肝心なこと忘れてた、接続設定を一箇所に共通でしておきたいけど、毎度の処理で必ずデータベース必要とするとは限らないよね。
そういう場合は必要なときだけ接続するようにしたいよね。接続の負荷とか考えるとね。

人間はわがままです。