<?xml version="1.0" encoding="UTF-8"?>
<!-- Reviewed: no -->
<!-- EN-Revision: 24259 -->
<sect1 id="learning.quickstart.create-model">
    <title>モデルとデータベーステーブルの作成</title>

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

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

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

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

    <programlisting language="php"><![CDATA[
// 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();
}
]]></programlisting>

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

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

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

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

    <programlisting language="shell"><![CDATA[
% 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.
]]></programlisting>

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

    <programlisting language="ini"><![CDATA[
; 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"
]]></programlisting>

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

    <programlisting language="ini"><![CDATA[
; 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"
]]></programlisting>

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

    <programlisting language="shell"><![CDATA[
% mkdir -p data/db; chmod -R a+rwX data
]]></programlisting>

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

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

    <programlisting language="sql"><![CDATA[
-- 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");
]]></programlisting>

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

    <programlisting language="sql"><![CDATA[
-- 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'));
]]></programlisting>

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

    <programlisting language="php"><![CDATA[
// 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 を返す
]]></programlisting>

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

    <programlisting language="shell"><![CDATA[
% php scripts/load.sqlite.php --withdata
]]></programlisting>

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

    <programlisting language="text"><![CDATA[
path/to/ZendFrameworkQuickstart/scripts$ php load.sqlite.php --withdata
Writing Database Guestbook in (control-c to cancel):
1
Database Created
Data Loaded.
]]></programlisting>

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

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

    <programlisting language="shell"><![CDATA[
% zf create db-table Guestbook guestbook
Creating a DbTable at application/models/DbTable/Guestbook.php
Updating project profile 'zfproject.xml'
]]></programlisting>

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

    <programlisting language="php"><![CDATA[
// 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';
}
]]></programlisting>

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

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

    <para>
        では <ulink url="http://martinfowler.com/eaaCatalog/dataMapper.html">データマッパー</ulink>
        を作成しましょう。<emphasis>データマッパー</emphasis> はドメインオブジェクトをデータベースに対応付けます。
        ここではモデル <classname>Application_Model_Guestbook</classname> をデータソース
        <classname>Application_Model_DbTable_Guestbook</classname> に対応付けることになります。
        データマッパーの典型的な <acronym>API</acronym> は次のようになるでしょう。
    </para>

    <programlisting language="php"><![CDATA[
// application/models/GuestbookMapper.php

class Application_Model_GuestbookMapper
{
    public function save($model);
    public function find($id, $model);
    public function fetchAll();
}
]]></programlisting>

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

    <programlisting language="shell"><![CDATA[
% zf create model GuestbookMapper
Creating a model at application/models/GuestbookMapper.php
Updating project profile '.zfproject.xml'
]]></programlisting>

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

    <programlisting language="php"><![CDATA[
// 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;
    }
}
]]></programlisting>

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

    <programlisting language="shell"><![CDATA[
% zf create model Guestbook
Creating a model at application/models/Guestbook.php
Updating project profile '.zfproject.xml'
]]></programlisting>

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

    <programlisting language="php"><![CDATA[
// 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;
    }
}
]]></programlisting>

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

    <para>
        新しいコントローラを作るには <command>zf create controller</command> コマンドを使います。
    </para>

    <programlisting language="shell"><![CDATA[
% 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'
]]></programlisting>

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

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

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

    <programlisting language="php"><![CDATA[
// application/controllers/GuestbookController.php

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

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

    <programlisting language="php"><![CDATA[
<!-- 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>
]]></programlisting>

    <note>
        <title>チェックポイント</title>

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

        <para>
            <inlinegraphic width="525" scale="100" align="center" valign="middle"
                fileref="figures/learning.quickstart.create-model.png" format="PNG" />
        </para>
    </note>

    <note>
        <title>データローダースクリプトの使用</title>

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

        <programlisting language="php"><![CDATA[
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]]
]]></programlisting>

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