# モデルとデータベーステーブルの作成

始める前にちょっと考えてみましょう。これらのクラスはどこにあって、それをどのように探すのでしょうか? わたしたちが作成したデフォルトのプロジェクトでは、一つのオートローダーがインスタンス化されます。 そこへ、他のクラスを探せるよう別のオートローダーを付け加えることができます。 MVC の様々なクラスはふつう一つのツリー -- ここでは `application/` -- に集まっていて、ほとんどの場合には共通のプレフィックスを持っていてほしいものです。

Zend_Controller_Front は、独立したミニアプリケーション、 すなわち "モジュール" という考え方を採用しています。モジュールは `zf` ツールが `application/` 以下に設定するディレクトリ構造を模倣しており、 その中の全てのクラスはモジュール名を共通のプレフィックスとして持っているものと見なされます。 `application/` はそれ自体が一つのモジュール -- "default" または "application" モジュール -- です。以上を踏まえてこのディレクトリ下にあるリソースのオートロードを設定していきたいと思います。

Zend_Application_Module_Autoloader は、あるモジュールの様々なリソースを 適切なディレクトリに対応付けるために必要となる機能を提供し、同時に、名前の付け方の規約も提供します。 このクラスのインスタンスは、デフォルトではブートストラップ・オブジェクトの初期化時に作成され、 アプリケーションのブートストラップでは "Application" というプレフィックスをデフォルトで使用します。 そのため、モデル、フォーム、テーブル・クラスはどれも、プレフィックス "Application_" で始めます。

では、ゲストブックを作っていくことにしましょう。一般的にはコメント、 タイムスタンプ、それからたまにメールアドレスを持つ単純なリストになります。 それらをデータベースに保存するとしたら、各エントリーのユニークな識別子も欲しいかも知れません。 エントリーを保存したり特定のエントリーを取ってきたり全エントリーを読み出したくなることでしょう。 そうだとすると、簡単なゲストブックモデルの API はこのようになりそうです。

    // application/models/Guestbook.php

    class Application_Model_Guestbook
    {
        protected $_comment;
        protected $_created;
        protected $_email;
        protected $_id;

        public function __set($name, $value);
        public function __get($name);

        public function setComment($text);
        public function getComment();

        public function setEmail($email);
        public function getEmail();

        public function setCreated($ts);
        public function getCreated();

        public function setId($id);
        public function getId();
    }

    class Application_Model_GuestbookMapper
    {
        public function save(Application_Model_Guestbook $guestbook);
        public function find($id);
        public function fetchAll();
    }

__get() と __set() は、 各エントリーのプロパティにアクセスする便利な仕組みと、他のゲッター、セッターのプロキシを提供してくれます。 また、オブジェクト中の許可したプロパティのみアクセス可能にするのにも役立ちます。

find() と fetchAll() は単一のエントリーや 全てのエントリーをフェッチする機能を提供し、save() は 一つのエントリーをデータストアに保存する面倒を見ます。

ここでようやく、データベース設定について考え始めることができます。

まず Db リソースを初期化する必要があります。 Layout リソースと View リソースから Db リソースの設定を準備できます。これには `zf configure db-adapter` コマンドが使えます。

    % zf configure db-adapter \
    > 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook.db"' \
    > production
    A db configuration for the production has been written to the application config file.

    % zf configure db-adapter \
    > 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook-testing.db"' \
    > testing
    A db configuration for the production has been written to the application config file.

    % zf configure db-adapter \
    > 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook-dev.db"' \
    > development
    A db configuration for the production has been written to the application config file.

ここで `application/configs/application.ini` ファイルの相当する部分に 以下の行が追加されているのが見付かるので、編集します。

    ; application/configs/application.ini

    [production]
    ; ...
    resources.db.adapter = "PDO_SQLITE"
    resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook.db"

    [testing : production]
    ; ...
    resources.db.adapter = "PDO_SQLITE"
    resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-testing.db"

    [development : production]
    ; ...
    resources.db.adapter = "PDO_SQLITE"
    resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-dev.db"

設定ファイルが最終的に以下のようになるようにしてください。

    ; application/configs/application.ini

    [production]
    phpSettings.display_startup_errors = 0
    phpSettings.display_errors = 0
    bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
    bootstrap.class = "Bootstrap"
    appnamespace = "Application"
    resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
    resources.frontController.params.displayExceptions = 0
    resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
    resources.view[] =
    resources.db.adapter = "PDO_SQLITE"
    resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook.db"

    [staging : production]

    [testing : production]
    phpSettings.display_startup_errors = 1
    phpSettings.display_errors = 1
    resources.db.adapter = "PDO_SQLITE"
    resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-testing.db"

    [development : production]
    phpSettings.display_startup_errors = 1
    phpSettings.display_errors = 1
    resources.db.adapter = "PDO_SQLITE"
    resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-dev.db"

データベースは `data/db/` に保存されることに注意しましょう。 ディレクトリを作って全ユーザーに書き込み権限を与えます。ユニックスライクなシステムでは、 次のようにすれば設定できます。

    % mkdir -p data/db; chmod -R a+rwX data

Windows では、エクスプローラでディレクトリを作り、 全ユーザーがそのディレクトリに書き込めるようアクセス権を設定する必要があります。

この段階でデータベース接続が行えます。今の例では `application/data/` ディレクトリ内にある Sqlite データベースへの接続です。では、ゲストブックのエントリーを入れる簡単なテーブルを設計しましょう。

    -- scripts/schema.sqlite.sql
    --
    -- この SQL からデータベーススキーマをロードする必要があります。

    CREATE TABLE guestbook (
        id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        email VARCHAR(32) NOT NULL DEFAULT 'noemail@test.com',
        comment TEXT NULL,
        created DATETIME NOT NULL
    );

    CREATE INDEX "id" ON "guestbook" ("id");

それから、素晴らしい仕事ができるように、数行、アプリケーションを面白くするする情報を作りましょう。

    -- scripts/data.sqlite.sql
    --
    -- 以下の SQL 文でデータベースに生命を吹き込めます。

    INSERT INTO guestbook (email, comment, created) VALUES
        ('ralph.schindler@zend.com',
        'Hello! Hope you enjoy this sample zf application!',
        DATETIME('NOW'));
    INSERT INTO guestbook (email, comment, created) VALUES
        ('foo@bar.com',
        'Baz baz baz, baz baz Baz baz baz - baz baz baz.',
        DATETIME('NOW'));

これでスキーマができ、データもいくらか定義できました。それでは一緒にスクリプトを書いてこのデータベースの構築を実行しましょう。 普通は、プロダクション環境でこういったことは必要ありませんが、このスクリプトがあれば開発者が必要なデータベースを手元で構築して、アプリケーションの作業に全力投球するのを助けてくれるでしょう。 以下の内容で、`scripts/load.sqlite.php` としてスクリプトを作ってください。

    // scripts/load.sqlite.php

    /**
     * データベースを作成して読み込むスクリプト
     */

    // アプリケーションパスとオートロードの初期化
    defined('APPLICATION_PATH')
        || define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));
    set_include_path(implode(PATH_SEPARATOR, array(
        APPLICATION_PATH . '/../library',
        get_include_path(),
    )));
    require_once 'Zend/Loader/Autoloader.php';
    Zend_Loader_Autoloader::getInstance();

    // CLI 用のオプション
    $getopt = new Zend_Console_Getopt(array(
        'withdata|w' => 'Load database with sample data',
        'env|e-s'    => 'Application environment for which to create database (defaults to development)',
        'help|h'     => 'Help -- usage message',
    ));
    try {
        $getopt->parse();
    } catch (Zend_Console_Getopt_Exception $e) {
        // オプションが不正な場合に使用法を表示
        echo $e->getUsageMessage();
        return false;
    }

    // ヘルプが要求された場合に使用法を表示
    if ($getopt->getOption('h')) {
        echo $getopt->getUsageMessage();
        return true;
    }

    // CLI オプションの有無に応じて値を初期化
    $withData = $getopt->getOption('w');
    $env      = $getopt->getOption('e');
    defined('APPLICATION_ENV')
        || define('APPLICATION_ENV', (null === $env) ? 'development' : $env);

    // Zend_Application の初期化
    $application = new Zend_Application(
        APPLICATION_ENV,
        APPLICATION_PATH . '/configs/application.ini'
    );

    // DB リソースの初期化と読み込み
    $bootstrap = $application->getBootstrap();
    $bootstrap->bootstrap('db');
    $dbAdapter = $bootstrap->getResource('db');

    // やっていることをユーザーに通知
    // （実際にここでデータベースを作る）
    if ('testing' != APPLICATION_ENV) {
        echo 'Writing Database Guestbook in (control-c to cancel): ' . PHP_EOL;
        for ($x = 5; $x > 0; $x--) {
            echo $x . "\r"; sleep(1);
        }
    }

    // データベースファイルが既にないかチェック
    $options = $bootstrap->getOption('resources');
    $dbFile  = $options['db']['params']['dbname'];
    if (file_exists($dbFile)) {
        unlink($dbFile);
    }

    // このブロックでスキーマファイルから読み込んだ実際のステートメントを実行
    try {
        $schemaSql = file_get_contents(dirname(__FILE__) . '/schema.sqlite.sql');
        // use the connection directly to load sql in batches
        $dbAdapter->getConnection()->exec($schemaSql);
        chmod($dbFile, 0666);

        if ('testing' != APPLICATION_ENV) {
            echo PHP_EOL;
            echo 'Database Created';
            echo PHP_EOL;
        }

        if ($withData) {
            $dataSql = file_get_contents(dirname(__FILE__) . '/data.sqlite.sql');
            // use the connection directly to load sql in batches
            $dbAdapter->getConnection()->exec($dataSql);
            if ('testing' != APPLICATION_ENV) {
                echo 'Data Loaded.';
                echo PHP_EOL;
            }
        }

    } catch (Exception $e) {
        echo 'AN ERROR HAS OCCURED:' . PHP_EOL;
        echo $e->getMessage() . PHP_EOL;
        return false;
    }

    // 大抵の場合、このスクリプトはコマンドラインから走らせて true を返す

このスクリプトを実行しましょう。ターミナルか DOS のコマンドラインから以下を実行してください。

    % php scripts/load.sqlite.php --withdata

以下のような出力を目にすると思います。

    path/to/ZendFrameworkQuickstart/scripts$ php load.sqlite.php --withdata
    Writing Database Guestbook in (control-c to cancel):
    1
    Database Created
    Data Loaded.

これでゲストブックアプリケーションのためにきちんと動くデータベースとテーブルができました。 次のステップはアプリケーションのコードを作成することです。 これにはデータソース（ここでは Zend_Db_Table を使います）と、 そのデータソースをドメインモデルに繋げる役目のデータマッパーを構築することが含まれます。 最後に、既存のエントリーの表示と新規エントリーの処理をモデルに結び付けるコントローラーも作ります。

ここではデータソースへの接続にテーブルデータゲートウェイ を使います。 Zend_Db_Table がこの機能を提供してくれます。 始めるにあたって Zend_Db_Table ベースのクラスを作りましょう。 レイアウトとデータベースアダプタでやった時と同じように、`zf` ツールの力を借りることができます。`create db-table` コマンドを使うのです。 これは最低で 2 つの引数をとります。参照させるクラスと、対応付けるデータベーステーブルの名前です。

    % zf create db-table Guestbook guestbook
    Creating a DbTable at application/models/DbTable/Guestbook.php
    Updating project profile 'zfproject.xml'

ディレクトリツリーを見てみると、新規ディレクトリ `application/models/DbTable/` が作られてファイル `Guestbook.php` が作られているのが分かります。 ファイルを開くと以下の内容になっています。

    // application/models/DbTable/Guestbook.php

    /**
     * This is the DbTable class for the guestbook table.
     */
    class Application_Model_DbTable_Guestbook extends Zend_Db_Table_Abstract
    {
        /** Table name */
        protected $_name    = 'guestbook';
    }

クラスのプレフィックス Application_Model_DbTable に注目しましょう。 最初の部分がモジュールのクラスプレフィックス "Application" で、その後にコンポーネント "Model_DbTable" がきます。後者はモジュールのディレクトリ `models/DbTable/` に対応付けられています。

Zend_Db_Table を拡張する際に必要なのはテーブル名と場合により主キーを ("id" でなければ)与えることだけです。

では データマッパー を作成しましょう。データマッパー はドメインオブジェクトをデータベースに対応付けます。 ここではモデル Application_Model_Guestbook をデータソース Application_Model_DbTable_Guestbook に対応付けることになります。 データマッパーの典型的な API は次のようになるでしょう。

    // application/models/GuestbookMapper.php

    class Application_Model_GuestbookMapper
    {
        public function save($model);
        public function find($id, $model);
        public function fetchAll();
    }

こうしたメソッドの他に、テーブルデータゲートウェイを設定してそこからデータを取り出すメソッドを追加します。 クラスの最初の形を作成するのに CLI の `zf` ツールを使います。

    % zf create model GuestbookMapper
    Creating a model at application/models/GuestbookMapper.php
    Updating project profile '.zfproject.xml'

次に `application/models/GuestbookMapper.php` にあるクラス Application_Model_GuestbookMapper を編集して下記の通りにします。

    // application/models/GuestbookMapper.php

    class Application_Model_GuestbookMapper
    {
        protected $_dbTable;

        public function setDbTable($dbTable)
        {
            if (is_string($dbTable)) {
                $dbTable = new $dbTable();
            }
            if (!$dbTable instanceof Zend_Db_Table_Abstract) {
                throw new Exception('Invalid table data gateway provided');
            }
            $this->_dbTable = $dbTable;
            return $this;
        }

        public function getDbTable()
        {
            if (null === $this->_dbTable) {
                $this->setDbTable('Application_Model_DbTable_Guestbook');
            }
            return $this->_dbTable;
        }

        public function save(Application_Model_Guestbook $guestbook)
        {
            $data = array(
                'email'   => $guestbook->getEmail(),
                'comment' => $guestbook->getComment(),
                'created' => date('Y-m-d H:i:s'),
            );

            if (null === ($id = $guestbook->getId())) {
                unset($data['id']);
                $this->getDbTable()->insert($data);
            } else {
                $this->getDbTable()->update($data, array('id = ?' => $id));
            }
        }

        public function find($id, Application_Model_Guestbook $guestbook)
        {
            $result = $this->getDbTable()->find($id);
            if (0 == count($result)) {
                return;
            }
            $row = $result->current();
            $guestbook->setId($row->id)
                      ->setEmail($row->email)
                      ->setComment($row->comment)
                      ->setCreated($row->created);
        }

        public function fetchAll()
        {
            $resultSet = $this->getDbTable()->fetchAll();
            $entries   = array();
            foreach ($resultSet as $row) {
                $entry = new Application_Model_Guestbook();
                $entry->setId($row->id)
                      ->setEmail($row->email)
                      ->setComment($row->comment)
                      ->setCreated($row->created);
                $entries[] = $entry;
            }
            return $entries;
        }
    }

これでモデルクラスが作れます。ここでもコマンド `zf create model` を使います。

    % zf create model Guestbook
    Creating a model at application/models/Guestbook.php
    Updating project profile '.zfproject.xml'

この空の PHP クラスを修正して、コンストラクタでも setOptions() メソッドでも、データの配列からモデルを生成するのを簡単にします。`application/models/Guestbook.php` 中の最終的なモデルクラスはこのようになるはずです。

    // application/models/Guestbook.php

    class Application_Model_Guestbook
    {
        protected $_comment;
        protected $_created;
        protected $_email;
        protected $_id;

        public function __construct(array $options = null)
        {
            if (is_array($options)) {
                $this->setOptions($options);
            }
        }

        public function __set($name, $value)
        {
            $method = 'set' . $name;
            if (('mapper' == $name) || !method_exists($this, $method)) {
                throw new Exception('Invalid guestbook property');
            }
            $this->$method($value);
        }

        public function __get($name)
        {
            $method = 'get' . $name;
            if (('mapper' == $name) || !method_exists($this, $method)) {
                throw new Exception('Invalid guestbook property');
            }
            return $this->$method();
        }

        public function setOptions(array $options)
        {
            $methods = get_class_methods($this);
            foreach ($options as $key => $value) {
                $method = 'set' . ucfirst($key);
                if (in_array($method, $methods)) {
                    $this->$method($value);
                }
            }
            return $this;
        }

        public function setComment($text)
        {
            $this->_comment = (string) $text;
            return $this;
        }

        public function getComment()
        {
            return $this->_comment;
        }

        public function setEmail($email)
        {
            $this->_email = (string) $email;
            return $this;
        }

        public function getEmail()
        {
            return $this->_email;
        }

        public function setCreated($ts)
        {
            $this->_created = $ts;
            return $this;
        }

        public function getCreated()
        {
            return $this->_created;
        }

        public function setId($id)
        {
            $this->_id = (int) $id;
            return $this;
        }

        public function getId()
        {
            return $this->_id;
        }
    }

最後に、以上の要素を全て一つに繋げます。データベース内の既存のエントリーを一覧表示する ゲストブック用のコントローラを作りましょう。

新しいコントローラを作るには `zf create controller` コマンドを使います。

    % zf create controller Guestbook
    Creating a controller at
        application/controllers/GuestbookController.php
    Creating an index action method in controller Guestbook
    Creating a view script for the index action method at
        application/views/scripts/guestbook/index.phtml
    Creating a controller test file at
        tests/application/controllers/GuestbookControllerTest.php
    Updating project profile '.zfproject.xml'

このコマンドによって `application/controllers/GuestbookController.php` の中に GuestbookController が作られ、そこには一つのアクションメソッド indexAction() が出来ています。また、このコントローラーの ビュースクリプト用ディレクトリ `application/views/scripts/guestbook/` とインデックスアクション用のビュースクリプトも作成されます。

ゲスブックの全エントリーを表示する入り口用のページとして "index" アクションを使います。

では、基本的なアプリケーションロジックを一息に作ってしまいましょう。indexAction() へやって来るとゲストブックの全エントリーを表示します。これは次のようになります。

    // application/controllers/GuestbookController.php

    class GuestbookController extends Zend_Controller_Action
    {
        public function indexAction()
        {
            $guestbook = new Application_Model_GuestbookMapper();
            $this->view->entries = $guestbook->fetchAll();
        }
    }

それからもちろんこれに使うビュースクリプトが必要です。 `application/views/scripts/guestbook/index.phtml` を以下のように編集します。

    <!-- application/views/scripts/guestbook/index.phtml -->

    <p><a href="<?php echo $this->url(
        array(
            'controller' => 'guestbook',
            'action'     => 'sign'
        ),
        'default',
        true) ?>">Sign Our Guestbook</a></p>

    Guestbook Entries: <br />
    <dl>
        <?php foreach ($this->entries as $entry): ?>
        <dt><?php echo $this->escape($entry->email) ?></dt>
        <dd><?php echo $this->escape($entry->comment) ?></dd>
        <?php endforeach ?>
    </dl>

チェックポイント

ここで "http://localhost/guestbook" にアクセスしてみましょう。 ブラウザには次のように表示されるはずです。

データローダースクリプトの使用

この節で導入したデータローダースクリプト(`scripts/load.sqlite.php`)は 定義した環境のそれぞれでデータベースを作りサンプルデータを読み込むのに使用できます。 内部では、多くのコマンドラインスイッチを提供できるようにしてくれる Zend_Console_Getopt を利用しています。"-h" または "--help" スイッチを渡すと使用可能なオプションを提示します。

    Usage: load.sqlite.php [ options ]
    --withdata|-w         Load database with sample data
    --env|-e [  ]         Application environment for which to create database
                          (defaults to development)
    --help|-h             Help -- usage message]]

"-e" スイッチを使うと `APPLICATION_ENV` 定数に使用する値を指定できます。 -- 定義した各環境で順に SQLite データベースを作れるようになるのです。 デプロイ時に、アプリケーション用に選んだ環境で確実にこのスクリプトを走らせるようにしてください。