在我们开始之前,让我们思考这样一些问题:这些类将放在什么地方,我们如何找到他们?我们创建的默认项目实例化一个自动加载器(autoloader)。我们可以把其他的自动加载器附加到它身上,这样它知道到哪里找到不同的类。典型的,我们想让我们大量的 MVC 类在同一个树状结构下分组--在本例中,是 application/--而且大部分情况使用一个通用前缀。
Zend_Controller_Front 有一个模块(modules)的理念,这些模块是独立的迷你应用程序。模块模仿 zf 工具在 application/ 下建立的目录结构,而且在它们(模块)内部的类被假设以一个通用前缀(模块的名字)开头。application/ 本身是一个模块--默认(default)或者应用(application)模块。这样,我们想为在这个目录内的资源创建自动加载。
Zend_Application_Module_Autoloader 提供了必要的功能来把一个模块下大量的资源映射到合适的目录,同时提供了一套标准的命名机制。类的一个实例将在 bootstrap 对象初始化的过程中被默认创建,你的应用程序的 bootstrap 将会默认的使用模块前缀 Application。这样,我们的模型,表单以及表格类将会以 Application 这个类前缀开头。
现在,让我们想一下什么组成了一个留言本。典型的,他们是一些简单的列表,包含有留言,时间,同时,经常的,电子邮件地址。假设我们把它们存储在一个数据库中,我们可能还想为每一个条目分配一个独一无二的标识符(unique identifier)。我们可能还希望可以保存每一个条目,捕获每一个条目,同时检索所有的条目。这样,一个简单的留言本模型 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__() 将为我们访问独立的列表的属性提供一个方便的机制,同时代理其它的 getter 和 setter。他们同样将帮助确保只有列入我们白名单的属性才能被对象使用。
find() 和 fetch() 提供了捕获一个单独的条目或者全部条目的功能,而 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/ 中。创建那些目录,同时使它们可写。在 unix 系统中,你可以这样做:
% mkdir -p data/db; chmod -R a+rwX data
在 Windows,你将在文件浏览器中创建这些目录,并设置权限,让任何人都可以写入这个目录。
至此,我们有了一个到数据库的连接,在我们的例子中,这是一个到位于我们 application/data 目录中的一个 Sqlite 数据库的链接。所以,让我们设计一个简单的表格来保存我们的留言本条目。
-- scripts/schema.sqlite.sql -- -- You will need load your database schema with this SQL. CREATE TABLE guestbook ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, email VARCHAR(32) NOT NULL DEFAULT '[email protected]', comment TEXT NULL, created DATETIME NOT NULL ); CREATE INDEX "id" ON "guestbook" ("id");
同时,为了我们能有一些可以操作的数据,让我们创建一些信息来使我们的应用程序更有趣。
-- scripts/data.sqlite.sql -- -- You can begin populating the database with the following SQL statements. INSERT INTO guestbook (email, comment, created) VALUES ('[email protected]', 'Hello! Hope you enjoy this sample zf application!', DATETIME('NOW')); INSERT INTO guestbook (email, comment, created) VALUES ('[email protected]', 'Baz baz baz, baz baz Baz baz baz - baz baz baz.', DATETIME('NOW'));
现在我们同时定义好了结构和一些数据。让我们创建一个脚本以便我们可以执行从而创建这个数据库。本来,这个在产品中不是必须的,但是这个脚本将帮助开发者创建本地数据库,以便他们拥有全套的工作应用程序。用以下的内容创建 scripts/load.sqlite.php 脚本:
<?php /** * Script for creating and loading database */ // Initialize the application path and autoloading 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(); // Define some CLI options $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) { // Bad options passed: report usage echo $e->getUsageMessage(); return false; } // If help requested, report usage message if ($getopt->getOption('h')) { echo $getopt->getUsageMessage(); return true; } // Initialize values based on presence or absence of CLI options $withData = $getopt->getOption('w'); $env = $getopt->getOption('e'); defined('APPLICATION_ENV') || define('APPLICATION_ENV', (null === $env) ? 'development' : $env); // Initialize Zend_Application $application = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini' ); // Initialize and retrieve DB resource $bootstrap = $application->getBootstrap(); $bootstrap->bootstrap('db'); $dbAdapter = $bootstrap->getResource('db'); // let the user know whats going on (we are actually creating a // database here) 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); } } // Check to see if we have a database file already $options = $bootstrap->getOption('resources'); $dbFile = $options['db']['params']['dbname']; if (file_exists($dbFile)) { unlink($dbFile); } // this block executes the actual statements that were loaded from // the schema file. 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; } // generally speaking, this script will be run from the command line return true;
现在我们的留言本应用程序有了一整套运转正常的数据库和表格。下面的几个步骤将创建我们的应用程序代码。包括创建一个数据源(在我们的例子中,我们将使用 Zend_Db_Table),和一个数据映射器来把数据源链接到我们的模型。最后我们还将创建控制器,来和这个模型互动,以便显示存在的条目,同时加入新的条目。
我们将使用一个 Table Data Gateway 来链接到我们的数据源;Zend_Db_Table 提供了这个功能。开始之前,让我们创建一个以 Zend_Db_Table 为基础的表格类。正如我们在 layout 和 database adapter 做的,我们可以使用 zf 工具来帮助我们,使用 create db-table 命令。它将接受至少两个参数,你想指向的类的名字,和它映射的数据库表格。
% 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 的时候,所需要提供的只是一个表格的名字,以及一个可选择的 primary key (如果它不是 id)。
现在让我们创建一个 Data Mapper。一个 Data Mapper 把一个对象映射到数据库。在我们的例子中,它将映射我们的模型,Application_Model_Guestbook,到我们的数据源,Application_Model_DbTable_Guestbook。一个典型的数据映射器(data mapper)的 API 将会是这样的:
// application/models/GuestbookMapper.php class Application_Model_GuestbookMapper { public function save($model); public function find($id, $model); public function fetchAll(); }
除了这些方法,我们将增加设置和检索 Table Data Gateway 的方法。为了创建最原始的类,使用 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 类,将会看到以下内容:
>?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 文件中,看起来象这样:
>?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; } }
最后,为了把这些元素链接起来,让我们创建一个 guestbook 控制器,它将列出数据库中现有的条目,同时可以添加新的条目进数据库。
为了创建一个新的控制器,使用 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'
这将创建一个新控制器,GuestbookController,在 application/controllers/GuestbookController.php 文件中,带有一个单独的行为方法,indexAction()。它同时为这个控制器创建一个视图脚本目录, application/views/scripts/guestbook/,内有与这个 index 行为相对应的视图脚本。
我们使用 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,如下显示:
<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,在你的浏览器中你应该能看到如下内容: